diff --git a/code/core/src/component-testing/components/test-fn.stories.tsx b/code/core/src/component-testing/components/test-fn.stories.tsx
deleted file mode 100644
index 6b926a961913..000000000000
--- a/code/core/src/component-testing/components/test-fn.stories.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-import React from 'react';
-
-import type { StoryContext } from '@storybook/react-vite';
-
-import { expect, fn } from 'storybook/test';
-
-import preview from '../../../../.storybook/preview';
-
-const Button = (args: React.ComponentProps<'button'>) => ;
-
-const meta = preview.meta({
- component: Button,
- render: (args, { name }) => (
-
- {name}
-
-
-
-
- ),
- args: {
- children: 'Default',
- onClick: fn(),
- },
- tags: ['some-tag', 'autodocs'],
-});
-
-export const WithNoTests = meta.story();
-
-export const TestFunctionTypes = meta.story({
- args: {
- children: 'Arg from story',
- },
-});
-
-export const PlayFunction = meta.story({
- play: async ({ canvas, userEvent }) => {
- const button = canvas.getByText('Default');
- await userEvent.click(button);
- },
-});
-
-TestFunctionTypes.test('simple', async ({ canvas, userEvent, args }) => {
- const button = canvas.getByText('Arg from story');
- await userEvent.click(button);
- await expect(args.onClick).toHaveBeenCalled();
-});
-
-const doTest = async ({
- canvas,
- userEvent,
- args,
-}: StoryContext>) => {
- const button = canvas.getByText('Arg from story');
- await userEvent.click(button);
- await expect(args.onClick).toHaveBeenCalled();
-};
-TestFunctionTypes.test('referring to function in file', doTest);
-
-TestFunctionTypes.test(
- 'with overrides',
- {
- args: {
- children: 'Arg from test override',
- },
- parameters: {
- viewport: {
- options: {
- sized: {
- name: 'Sized',
- styles: {
- width: '380px',
- height: '500px',
- },
- },
- },
- },
- chromatic: { viewports: [380] },
- },
- globals: { sb_theme: 'dark', viewport: { value: 'sized' } },
- },
- async ({ canvas }) => {
- const button = canvas.getByText('Arg from test override');
- await expect(button).toBeInTheDocument();
- expect(document.body.clientWidth).toBe(380);
- }
-);
-
-TestFunctionTypes.test(
- 'with play function',
- {
- play: async ({ canvas }) => {
- const button = canvas.getByText('Arg from story');
- await expect(button).toBeInTheDocument();
- },
- },
- async ({ canvas }) => {
- const button = canvas.getByText('Arg from story');
- await expect(button).toBeEnabled();
- }
-);
-
-export const ExtendedStorySinglePlayExample = TestFunctionTypes.extend({
- args: {
- children: 'Arg from extended story',
- },
- play: async ({ canvas }) => {
- const button = canvas.getByText('Arg from extended story');
- await expect(button).toBeEnabled();
- },
-});
-
-export const ExtendedStorySingleTestExample = TestFunctionTypes.extend({
- args: {
- children: 'Arg from extended story',
- },
-});
-
-ExtendedStorySingleTestExample.test(
- 'this is a very long test name to explain that this story test should guarantee that the args have been extended correctly',
- async ({ canvas }) => {
- const button = canvas.getByText('Arg from extended story');
- await expect(button).toBeEnabled();
- }
-);
-
-// This is intentionally defined out-of-order
-PlayFunction.test('should be clicked by play function', async ({ args }) => {
- await expect(args.onClick).toHaveBeenCalled();
-});
-
-export const TestNames = meta.story({
- args: {
- children: 'This story is no-op, just focus on the test names',
- },
-});
-TestNames.test(
- 'should display an error when login is attempted with an expired session token',
- () => {}
-);
-
-TestNames.test(
- 'should display an error when login is attempted with multiple invalid password attempts',
- () => {}
-);
-
-TestNames.test('should display an error when login is attempted with a revoked API key', () => {});
-
-TestNames.test(
- 'should display an error when login is attempted after exceeding the maximum session limit',
- () => {}
-);
-
-TestNames.test(
- 'should display an error when login is attempted with a disabled user account',
- () => {}
-);
-
-TestNames.test(
- 'should display an error when login is attempted with an unsupported authentication provider',
- () => {}
-);
-
-TestNames.test(
- 'should display an error when login is attempted after the password reset process is incomplete',
- () => {}
-);
-
-TestNames.test(
- 'should display an error when login is attempted with a malformed authentication request',
- () => {}
-);
-
-TestNames.test(
- 'should display an error when login is attempted with an unverified email address',
- () => {}
-);
diff --git a/code/core/src/test/preview.ts b/code/core/src/test/preview.ts
index 3277852dff6a..956719fcad4c 100644
--- a/code/core/src/test/preview.ts
+++ b/code/core/src/test/preview.ts
@@ -95,8 +95,11 @@ const enhanceContext: LoaderFunction = async (context) => {
// userEvent.setup() cannot be called in non browser environment and will attempt to access window.navigator.clipboard
// which will throw an error in react native for example.
+ const userEventDisabled = globalThis?.FEATURES?.userEventSetup === false;
+
const clipboard = globalThis.window?.navigator?.clipboard;
- if (clipboard) {
+ // TODO: Remove clipboard check in SB 11
+ if (!userEventDisabled && clipboard) {
context.userEvent = instrument(
{ userEvent: uninstrumentedUserEvent.setup() },
{
diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts
index 1f4406111ada..3d3f87973f85 100644
--- a/code/core/src/types/modules/core-common.ts
+++ b/code/core/src/types/modules/core-common.ts
@@ -451,6 +451,12 @@ export interface StorybookConfigRaw {
*/
actions?: boolean;
+ /**
+ * Control automatic setup of @testing-library/user-event in the preview. Disabled in non-DOM
+ * environments (e.g., React Native) or when you want to manage interaction utilities manually.
+ */
+ userEventSetup?: boolean;
+
/**
* @temporary This feature flag is a migration assistant, and is scheduled to be removed.
*
diff --git a/docs/_snippets/main-config-features-user-event-instrumentation.md b/docs/_snippets/main-config-features-user-event-instrumentation.md
new file mode 100644
index 000000000000..7204b8f33e2c
--- /dev/null
+++ b/docs/_snippets/main-config-features-user-event-instrumentation.md
@@ -0,0 +1,51 @@
+```js filename=".storybook/main.js" renderer="common" language="js" tabTitle="CSF 3"
+export default {
+ // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
+ framework: '@storybook/your-framework',
+ stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ features: {
+ userEventSetup: false,
+ },
+};
+```
+
+```ts filename=".storybook/main.ts" renderer="common" language="ts" tabTitle="CSF 3"
+// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
+import type { StorybookConfig } from '@storybook/your-framework';
+
+const config: StorybookConfig = {
+ framework: '@storybook/your-framework',
+ stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ features: {
+ userEventSetup: false,
+ },
+};
+
+export default config;
+```
+
+```ts filename=".storybook/main.ts" renderer="react" language="ts" tabTitle="CSF Next 🧪"
+// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
+import { defineMain } from '@storybook/your-framework/node';
+
+export default defineMain({
+ framework: '@storybook/your-framework',
+ stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ features: {
+ userEventSetup: false,
+ },
+});
+```
+
+```js filename=".storybook/main.js" renderer="react" language="js" tabTitle="CSF Next 🧪"
+// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
+import { defineMain } from '@storybook/your-framework/node';
+
+export default defineMain({
+ framework: '@storybook/your-framework',
+ stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ features: {
+ userEventSetup: false,
+ },
+});
+```
diff --git a/docs/api/main-config/main-config-features.mdx b/docs/api/main-config/main-config-features.mdx
index 270cd3e9f99e..79011a565d49 100644
--- a/docs/api/main-config/main-config-features.mdx
+++ b/docs/api/main-config/main-config-features.mdx
@@ -19,6 +19,7 @@ Type:
controls?: boolean;
developmentModeForBuild?: boolean;
experimentalTestSyntax?: boolean;
+ userEventSetup?: boolean;
highlight?: boolean;
interactions?: boolean;
legacyDecoratorFileOrder?: boolean;
@@ -40,6 +41,7 @@ Type:
backgrounds?: boolean;
controls?: boolean;
developmentModeForBuild?: boolean;
+ userEventSetup?: boolean;
highlight?: boolean;
interactions?: boolean;
legacyDecoratorFileOrder?: boolean;
@@ -60,6 +62,7 @@ Type:
backgrounds?: boolean;
controls?: boolean;
developmentModeForBuild?: boolean;
+ userEventSetup?: boolean;
highlight?: boolean;
interactions?: boolean;
legacyDecoratorFileOrder?: boolean;
@@ -146,6 +149,20 @@ Enable the [experimental `.test` method with the CSF Next format](../csf/csf-nex
+## `userEventSetup`
+
+Type: `boolean`
+
+Control automatic setup of Testing Library's `userEvent` in Storybook's preview runtime. When enabled, Storybook sets up `userEvent` and wires clipboard helpers for web-based environments. Disable this in non-DOM environments (e.g., React Native) or when you want full control over interaction utilities.
+
+Defaults to enabled for web renderers. Can be turned off globally via `features`.
+
+{/* prettier-ignore-start */}
+
+
+
+{/* prettier-ignore-end */}
+
## `highlight`
Type: `boolean`