From 483325a656e9884d4322d0ed38a9a518b9ea1f8f Mon Sep 17 00:00:00 2001
From: Valentin Palkovic
Date: Wed, 4 Sep 2024 10:20:57 +0200
Subject: [PATCH] Portable Stories: Improve Handling of React Updates and
Errors
Co-authored-by: Yann Braga
Co-authored-by: Jeppe Reinhold
---
.../modules/store/csf/portable-stories.ts | 4 +
.../react-dom-shim/src/preventActChecks.tsx | 17 --
code/lib/react-dom-shim/src/react-16.tsx | 6 +-
code/lib/react-dom-shim/src/react-18.tsx | 23 +-
code/renderers/react/package.json | 4 +
.../react/src/__test__/Button.stories.tsx | 9 +-
.../__test__/ComponentWithError.stories.tsx | 13 +
.../react/src/__test__/ComponentWithError.tsx | 4 +
.../portable-stories-legacy.test.tsx.snap | 34 +++
.../__test__/portable-stories-legacy.test.tsx | 6 +-
.../src/__test__/portable-stories.test.tsx | 90 +++---
code/renderers/react/src/act-compat.ts | 65 +++++
code/renderers/react/src/portable-stories.tsx | 80 +++++-
code/renderers/react/src/renderToCanvas.tsx | 7 +-
code/vitest-setup.ts | 1 +
code/yarn.lock | 269 +++++++++++++++++-
16 files changed, 557 insertions(+), 75 deletions(-)
delete mode 100644 code/lib/react-dom-shim/src/preventActChecks.tsx
create mode 100644 code/renderers/react/src/__test__/ComponentWithError.stories.tsx
create mode 100644 code/renderers/react/src/__test__/ComponentWithError.tsx
create mode 100644 code/renderers/react/src/act-compat.ts
diff --git a/code/core/src/preview-api/modules/store/csf/portable-stories.ts b/code/core/src/preview-api/modules/store/csf/portable-stories.ts
index 1525b6e3e6d8..7adc83196eb1 100644
--- a/code/core/src/preview-api/modules/store/csf/portable-stories.ts
+++ b/code/core/src/preview-api/modules/store/csf/portable-stories.ts
@@ -74,6 +74,10 @@ export function setProjectAnnotations(
| NamedOrDefaultProjectAnnotations[]
): NormalizedProjectAnnotations {
const annotations = Array.isArray(projectAnnotations) ? projectAnnotations : [projectAnnotations];
+ if (globalThis.defaultProjectAnnotations) {
+ annotations.push(globalThis.defaultProjectAnnotations);
+ }
+
globalThis.globalProjectAnnotations = composeConfigs(annotations.map(extractAnnotation));
return globalThis.globalProjectAnnotations;
diff --git a/code/lib/react-dom-shim/src/preventActChecks.tsx b/code/lib/react-dom-shim/src/preventActChecks.tsx
deleted file mode 100644
index f35e2fb25dc5..000000000000
--- a/code/lib/react-dom-shim/src/preventActChecks.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-export {};
-
-declare const globalThis: {
- IS_REACT_ACT_ENVIRONMENT?: boolean;
-};
-
-// TODO(9.0): We should actually wrap all those lines in `act`, but that might be a breaking change.
-// We should make that breaking change for SB 9.0
-export function preventActChecks(callback: () => void): void {
- const originalActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT;
- globalThis.IS_REACT_ACT_ENVIRONMENT = false;
- try {
- callback();
- } finally {
- globalThis.IS_REACT_ACT_ENVIRONMENT = originalActEnvironment;
- }
-}
diff --git a/code/lib/react-dom-shim/src/react-16.tsx b/code/lib/react-dom-shim/src/react-16.tsx
index a1e7b1e97009..8c7b2c8f5a67 100644
--- a/code/lib/react-dom-shim/src/react-16.tsx
+++ b/code/lib/react-dom-shim/src/react-16.tsx
@@ -2,14 +2,12 @@
import type { ReactElement } from 'react';
import * as ReactDOM from 'react-dom';
-import { preventActChecks } from './preventActChecks';
-
export const renderElement = async (node: ReactElement, el: Element) => {
return new Promise((resolve) => {
- preventActChecks(() => void ReactDOM.render(node, el, () => resolve(null)));
+ ReactDOM.render(node, el, () => resolve(null));
});
};
export const unmountElement = (el: Element) => {
- preventActChecks(() => void ReactDOM.unmountComponentAtNode(el));
+ ReactDOM.unmountComponentAtNode(el);
};
diff --git a/code/lib/react-dom-shim/src/react-18.tsx b/code/lib/react-dom-shim/src/react-18.tsx
index 5eb72b20eb17..f3398fc65ff0 100644
--- a/code/lib/react-dom-shim/src/react-18.tsx
+++ b/code/lib/react-dom-shim/src/react-18.tsx
@@ -1,15 +1,21 @@
/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */
-import type { FC, ReactElement } from 'react';
+import type { ReactElement } from 'react';
import * as React from 'react';
import type { Root as ReactRoot, RootOptions } from 'react-dom/client';
import * as ReactDOM from 'react-dom/client';
-import { preventActChecks } from './preventActChecks';
-
// A map of all rendered React 18 nodes
const nodes = new Map();
-const WithCallback: FC<{ callback: () => void; children: ReactElement }> = ({
+declare const globalThis: {
+ IS_REACT_ACT_ENVIRONMENT: boolean;
+};
+
+function getIsReactActEnvironment() {
+ return globalThis.IS_REACT_ACT_ENVIRONMENT;
+}
+
+const WithCallback: React.FC<{ callback: () => void; children: ReactElement }> = ({
callback,
children,
}) => {
@@ -43,8 +49,13 @@ export const renderElement = async (node: ReactElement, el: Element, rootOptions
// Create Root Element conditionally for new React 18 Root Api
const root = await getReactRoot(el, rootOptions);
+ if (getIsReactActEnvironment()) {
+ root.render(node);
+ return;
+ }
+
const { promise, resolve } = Promise.withResolvers();
- preventActChecks(() => root.render({node}));
+ root.render({node});
return promise;
};
@@ -52,7 +63,7 @@ export const unmountElement = (el: Element, shouldUseNewRootApi?: boolean) => {
const root = nodes.get(el);
if (root) {
- preventActChecks(() => root.unmount());
+ root.unmount();
nodes.delete(el);
}
};
diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json
index 4d370bbb8a1e..003466f1b182 100644
--- a/code/renderers/react/package.json
+++ b/code/renderers/react/package.json
@@ -94,12 +94,16 @@
"require-from-string": "^2.0.2"
},
"peerDependencies": {
+ "@storybook/test": "workspace:*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"storybook": "workspace:^",
"typescript": ">= 4.2.x"
},
"peerDependenciesMeta": {
+ "@storybook/test": {
+ "optional": true
+ },
"typescript": {
"optional": true
}
diff --git a/code/renderers/react/src/__test__/Button.stories.tsx b/code/renderers/react/src/__test__/Button.stories.tsx
index bde220fdf469..0e6e0d6e8c67 100644
--- a/code/renderers/react/src/__test__/Button.stories.tsx
+++ b/code/renderers/react/src/__test__/Button.stories.tsx
@@ -103,7 +103,6 @@ export const HooksStory: CSF3Story = {
);
},
play: async ({ canvasElement, step }) => {
- console.log('start of play function');
const canvas = within(canvasElement);
await step('Step label', async () => {
const inputEl = canvas.getByTestId('input');
@@ -112,8 +111,8 @@ export const HooksStory: CSF3Story = {
await userEvent.type(inputEl, 'Hello world!');
await expect(inputEl).toHaveValue('Hello world!');
+ await expect(buttonEl).toHaveTextContent('I am clicked');
});
- console.log('end of play function');
},
};
@@ -182,6 +181,12 @@ export const MountInPlayFunction: CSF3Story<{ mockFn: (val: string) => string }>
},
};
+export const MountInPlayFunctionThrow: CSF3Story<{ mockFn: (val: string) => string }> = {
+ play: async () => {
+ throw new Error('Error thrown in play');
+ },
+};
+
export const WithActionArg: CSF3Story<{ someActionArg: HandlerFunction }> = {
args: {
someActionArg: action('some-action-arg'),
diff --git a/code/renderers/react/src/__test__/ComponentWithError.stories.tsx b/code/renderers/react/src/__test__/ComponentWithError.stories.tsx
new file mode 100644
index 000000000000..627055e2d965
--- /dev/null
+++ b/code/renderers/react/src/__test__/ComponentWithError.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '..';
+import { ComponentWithError } from './ComponentWithError';
+
+const meta = {
+ title: 'Example/ComponentWithError',
+ component: ComponentWithError as any,
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const ThrowsError: Story = {};
diff --git a/code/renderers/react/src/__test__/ComponentWithError.tsx b/code/renderers/react/src/__test__/ComponentWithError.tsx
new file mode 100644
index 000000000000..37f667cb4f2c
--- /dev/null
+++ b/code/renderers/react/src/__test__/ComponentWithError.tsx
@@ -0,0 +1,4 @@
+export function ComponentWithError() {
+ // eslint-disable-next-line local-rules/no-uncategorized-errors
+ throw new Error('Error in render');
+}
diff --git a/code/renderers/react/src/__test__/__snapshots__/portable-stories-legacy.test.tsx.snap b/code/renderers/react/src/__test__/__snapshots__/portable-stories-legacy.test.tsx.snap
index b4753327aaf1..b690349bed8d 100644
--- a/code/renderers/react/src/__test__/__snapshots__/portable-stories-legacy.test.tsx.snap
+++ b/code/renderers/react/src/__test__/__snapshots__/portable-stories-legacy.test.tsx.snap
@@ -147,6 +147,40 @@ exports[`Legacy Portable Stories API > Renders Modal story 1`] = `
+
diff --git a/code/renderers/react/src/__test__/portable-stories-legacy.test.tsx b/code/renderers/react/src/__test__/portable-stories-legacy.test.tsx
index 3c7321cdfe63..5567b1fd9fbc 100644
--- a/code/renderers/react/src/__test__/portable-stories-legacy.test.tsx
+++ b/code/renderers/react/src/__test__/portable-stories-legacy.test.tsx
@@ -200,7 +200,11 @@ describe('Legacy Portable Stories API', () => {
it.each(testCases)('Renders %s story', async (_storyName, Story) => {
cleanup();
- if (_storyName === 'CSF2StoryWithLocale' || _storyName === 'MountInPlayFunction') {
+ if (
+ _storyName === 'CSF2StoryWithLocale' ||
+ _storyName === 'MountInPlayFunction' ||
+ _storyName === 'MountInPlayFunctionThrow'
+ ) {
return;
}
diff --git a/code/renderers/react/src/__test__/portable-stories.test.tsx b/code/renderers/react/src/__test__/portable-stories.test.tsx
index 90346edff991..94de89e093a5 100644
--- a/code/renderers/react/src/__test__/portable-stories.test.tsx
+++ b/code/renderers/react/src/__test__/portable-stories.test.tsx
@@ -2,7 +2,7 @@
/* eslint-disable import/namespace */
import { cleanup, render, screen } from '@testing-library/react';
-import { afterEach, describe, expect, it, vi } from 'vitest';
+import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import React from 'react';
@@ -16,23 +16,28 @@ import { expectTypeOf } from 'expect-type';
import { composeStories, composeStory, setProjectAnnotations } from '..';
import type { Button } from './Button';
-import * as stories from './Button.stories';
+import * as ButtonStories from './Button.stories';
+import * as ComponentWithErrorStories from './ComponentWithError.stories';
-setProjectAnnotations([]);
+const HooksStory = composeStory(ButtonStories.HooksStory, ButtonStories.default);
+
+const projectAnnotations = setProjectAnnotations([]);
// example with composeStories, returns an object with all stories composed with args/decorators
-const { CSF3Primary, LoaderStory, MountInPlayFunction } = composeStories(stories);
+const { CSF3Primary, LoaderStory, MountInPlayFunction, MountInPlayFunctionThrow } =
+ composeStories(ButtonStories);
+const { ThrowsError } = composeStories(ComponentWithErrorStories);
+
+beforeAll(async () => {
+ await projectAnnotations.beforeAll?.();
+});
afterEach(() => {
cleanup();
});
-declare const globalThis: {
- IS_REACT_ACT_ENVIRONMENT?: boolean;
-};
-
// example with composeStory, returns a single story composed with args/decorators
-const Secondary = composeStory(stories.CSF2Secondary, stories.default);
+const Secondary = composeStory(ButtonStories.CSF2Secondary, ButtonStories.default);
describe('renders', () => {
it('renders primary button', () => {
render(
Hello world);
@@ -60,6 +65,10 @@ describe('renders', () => {
expect(buttonElement).not.toBeNull();
});
+ it('should throw error when rendering a component with a render error', async () => {
+ await expect(() => ThrowsError.run()).rejects.toThrowError('Error in render');
+ });
+
it('should render component mounted in play function', async () => {
await MountInPlayFunction.run();
@@ -67,6 +76,10 @@ describe('renders', () => {
expect(screen.getByTestId('loaded-data').textContent).toEqual('loaded data');
});
+ it('should throw an error in play function', () => {
+ expect(() => MountInPlayFunctionThrow.run()).rejects.toThrowError('Error thrown in play');
+ });
+
it('should call and compose loaders data', async () => {
await LoaderStory.load();
const { getByTestId } = render(
);
@@ -78,10 +91,6 @@ describe('renders', () => {
});
describe('projectAnnotations', () => {
- afterEach(() => {
- cleanup();
- });
-
it('renders with default projectAnnotations', () => {
setProjectAnnotations([
{
@@ -91,7 +100,7 @@ describe('projectAnnotations', () => {
},
},
]);
- const WithEnglishText = composeStory(stories.CSF2StoryWithLocale, stories.default);
+ const WithEnglishText = composeStory(ButtonStories.CSF2StoryWithLocale, ButtonStories.default);
const { getByText } = render(
);
const buttonElement = getByText('Hello!');
expect(buttonElement).not.toBeNull();
@@ -99,24 +108,31 @@ describe('projectAnnotations', () => {
});
it('renders with custom projectAnnotations via composeStory params', () => {
- const WithPortugueseText = composeStory(stories.CSF2StoryWithLocale, stories.default, {
- initialGlobals: { locale: 'pt' },
- });
+ const WithPortugueseText = composeStory(
+ ButtonStories.CSF2StoryWithLocale,
+ ButtonStories.default,
+ {
+ initialGlobals: { locale: 'pt' },
+ }
+ );
const { getByText } = render(
);
const buttonElement = getByText('Olá!');
expect(buttonElement).not.toBeNull();
});
it('has action arg from argTypes when addon-actions annotations are added', () => {
- //@ts-expect-error our tsconfig.jsn#moduleResulution is set to 'node', which doesn't support this import
- const Story = composeStory(stories.WithActionArgType, stories.default, addonActionsPreview);
+ const Story = composeStory(
+ ButtonStories.WithActionArgType,
+ ButtonStories.default,
+ addonActionsPreview
+ );
expect(Story.args.someActionArg).toHaveProperty('isAction', true);
});
});
describe('CSF3', () => {
it('renders with inferred globalRender', () => {
- const Primary = composeStory(stories.CSF3Button, stories.default);
+ const Primary = composeStory(ButtonStories.CSF3Button, ButtonStories.default);
render(
Hello world);
const buttonElement = screen.getByText(/Hello world/i);
@@ -124,14 +140,17 @@ describe('CSF3', () => {
});
it('renders with custom render function', () => {
- const Primary = composeStory(stories.CSF3ButtonWithRender, stories.default);
+ const Primary = composeStory(ButtonStories.CSF3ButtonWithRender, ButtonStories.default);
render(
);
expect(screen.getByTestId('custom-render')).not.toBeNull();
});
it('renders with play function without canvas element', async () => {
- const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
+ const CSF3InputFieldFilled = composeStory(
+ ButtonStories.CSF3InputFieldFilled,
+ ButtonStories.default
+ );
await CSF3InputFieldFilled.run();
const input = screen.getByTestId('input') as HTMLInputElement;
@@ -139,7 +158,10 @@ describe('CSF3', () => {
});
it('renders with play function with canvas element', async () => {
- const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
+ const CSF3InputFieldFilled = composeStory(
+ ButtonStories.CSF3InputFieldFilled,
+ ButtonStories.default
+ );
const div = document.createElement('div');
document.body.appendChild(div);
@@ -153,21 +175,16 @@ describe('CSF3', () => {
});
it('renders with hooks', async () => {
- // TODO find out why act is not working here
- globalThis.IS_REACT_ACT_ENVIRONMENT = false;
- const HooksStory = composeStory(stories.HooksStory, stories.default);
-
await HooksStory.run();
const input = screen.getByTestId('input') as HTMLInputElement;
expect(input.value).toEqual('Hello world!');
- globalThis.IS_REACT_ACT_ENVIRONMENT = true;
});
});
// common in addons that need to communicate between manager and preview
it('should pass with decorators that need addons channel', () => {
- const PrimaryWithChannels = composeStory(stories.CSF3Primary, stories.default, {
+ const PrimaryWithChannels = composeStory(ButtonStories.CSF3Primary, ButtonStories.default, {
decorators: [
(StoryFn: any) => {
addons.getChannel();
@@ -186,27 +203,24 @@ describe('ComposeStories types', () => {
type ComposeStoriesParam = Parameters
[0];
expectTypeOf({
- ...stories,
- default: stories.default as Meta,
+ ...ButtonStories,
+ default: ButtonStories.default as Meta,
}).toMatchTypeOf();
expectTypeOf({
- ...stories,
- default: stories.default satisfies Meta,
+ ...ButtonStories,
+ default: ButtonStories.default satisfies Meta,
}).toMatchTypeOf();
});
});
-// Batch snapshot testing
-const testCases = Object.values(composeStories(stories)).map(
+const testCases = Object.values(composeStories(ButtonStories)).map(
(Story) => [Story.storyName, Story] as [string, typeof Story]
);
it.each(testCases)('Renders %s story', async (_storyName, Story) => {
- if (_storyName === 'CSF2StoryWithLocale') {
+ if (_storyName === 'CSF2StoryWithLocale' || _storyName === 'MountInPlayFunctionThrow') {
return;
}
- globalThis.IS_REACT_ACT_ENVIRONMENT = false;
await Story.run();
- globalThis.IS_REACT_ACT_ENVIRONMENT = true;
expect(document.body).toMatchSnapshot();
});
diff --git a/code/renderers/react/src/act-compat.ts b/code/renderers/react/src/act-compat.ts
new file mode 100644
index 000000000000..afe1cc902316
--- /dev/null
+++ b/code/renderers/react/src/act-compat.ts
@@ -0,0 +1,65 @@
+// Copied from
+// https://github.com/testing-library/react-testing-library/blob/3dcd8a9649e25054c0e650d95fca2317b7008576/src/act-compat.js
+import * as React from 'react';
+
+import * as DeprecatedReactTestUtils from 'react-dom/test-utils';
+
+declare const globalThis: {
+ IS_REACT_ACT_ENVIRONMENT: boolean;
+};
+
+// @ts-expect-error act might not be available in some versions of React
+const reactAct = typeof React.act === 'function' ? React.act : DeprecatedReactTestUtils.act;
+
+export function setReactActEnvironment(isReactActEnvironment: boolean) {
+ globalThis.IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment;
+}
+
+export function getReactActEnvironment() {
+ return globalThis.IS_REACT_ACT_ENVIRONMENT;
+}
+
+function withGlobalActEnvironment(actImplementation: (callback: () => void) => Promise) {
+ return (callback: () => any) => {
+ const previousActEnvironment = getReactActEnvironment();
+ setReactActEnvironment(true);
+ try {
+ // The return value of `act` is always a thenable.
+ let callbackNeedsToBeAwaited = false;
+ const actResult = actImplementation(() => {
+ const result = callback();
+ if (result !== null && typeof result === 'object' && typeof result.then === 'function') {
+ callbackNeedsToBeAwaited = true;
+ }
+ return result;
+ });
+ if (callbackNeedsToBeAwaited) {
+ const thenable: Promise = actResult;
+ return {
+ then: (resolve: (param: any) => void, reject: (param: any) => void) => {
+ thenable.then(
+ (returnValue) => {
+ setReactActEnvironment(previousActEnvironment);
+ resolve(returnValue);
+ },
+ (error) => {
+ setReactActEnvironment(previousActEnvironment);
+ reject(error);
+ }
+ );
+ },
+ };
+ } else {
+ setReactActEnvironment(previousActEnvironment);
+ return actResult;
+ }
+ } catch (error) {
+ // Can't be a `finally {}` block since we don't know if we have to immediately restore IS_REACT_ACT_ENVIRONMENT
+ // or if we have to await the callback first.
+ setReactActEnvironment(previousActEnvironment);
+ throw error;
+ }
+ };
+}
+
+export const act = withGlobalActEnvironment(reactAct);
diff --git a/code/renderers/react/src/portable-stories.tsx b/code/renderers/react/src/portable-stories.tsx
index 2ea196e85b4b..ced0bbd289e3 100644
--- a/code/renderers/react/src/portable-stories.tsx
+++ b/code/renderers/react/src/portable-stories.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import * as React from 'react';
import {
composeStories as originalComposeStories,
@@ -17,6 +17,7 @@ import type {
StoryAnnotationsOrFn,
} from 'storybook/internal/types';
+import { act, getReactActEnvironment, setReactActEnvironment } from './act-compat';
import * as reactProjectAnnotations from './entry-preview';
import type { Meta } from './public-types';
import type { ReactRenderer } from './types';
@@ -54,9 +55,66 @@ export function setProjectAnnotations(
// This will not be necessary once we have auto preset loading
export const INTERNAL_DEFAULT_PROJECT_ANNOTATIONS: ProjectAnnotations = {
...reactProjectAnnotations,
- renderToCanvas: (renderContext, canvasElement) => {
+ beforeAll: async function reactBeforeAll() {
+ try {
+ // copied from
+ // https://github.com/testing-library/react-testing-library/blob/3dcd8a9649e25054c0e650d95fca2317b7008576/src/pure.js
+ const { configure } = await import('@storybook/test');
+
+ configure({
+ unstable_advanceTimersWrapper: (cb) => {
+ return act(cb);
+ },
+ asyncWrapper: async (cb) => {
+ const previousActEnvironment = getReactActEnvironment();
+ setReactActEnvironment(false);
+ try {
+ const result = await cb();
+ // Drain microtask queue.
+ // Otherwise we'll restore the previous act() environment, before we resolve the `waitFor` call.
+ // The caller would have no chance to wrap the in-flight Promises in `act()`
+ await new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ }, 0);
+
+ if (jestFakeTimersAreEnabled()) {
+ // @ts-expect-error global jest
+ jest.advanceTimersByTime(0);
+ }
+ });
+
+ return result;
+ } finally {
+ setReactActEnvironment(previousActEnvironment);
+ }
+ },
+ eventWrapper: (cb) => {
+ let result;
+ act(() => {
+ result = cb();
+ });
+ return result;
+ },
+ });
+ } catch (e) {
+ console.log(e);
+ // @storybook/test might not be available
+ }
+ },
+ renderToCanvas: async (renderContext, canvasElement) => {
if (renderContext.storyContext.testingLibraryRender == null) {
- return reactProjectAnnotations.renderToCanvas(renderContext, canvasElement);
+ let unmount: () => void;
+
+ await act(async () => {
+ unmount = await reactProjectAnnotations.renderToCanvas(renderContext, canvasElement);
+ });
+
+ return async () => {
+ await act(() => {
+ unmount();
+ });
+ };
}
const {
storyContext: { context, unboundStoryFn: Story, testingLibraryRender: render },
@@ -149,3 +207,19 @@ export function composeStories;
}
+
+/** The function is used to configure jest's fake timers in environments where React's act is enabled */
+function jestFakeTimersAreEnabled() {
+ // @ts-expect-error global jest
+ if (typeof jest !== 'undefined' && jest !== null) {
+ return (
+ // legacy timers
+
+ // eslint-disable-next-line no-underscore-dangle
+ (setTimeout as any)._isMockFunction === true || // modern timers
+ Object.prototype.hasOwnProperty.call(setTimeout, 'clock')
+ );
+ }
+
+ return false;
+}
diff --git a/code/renderers/react/src/renderToCanvas.tsx b/code/renderers/react/src/renderToCanvas.tsx
index f3a4231d078c..3ae6136f9582 100644
--- a/code/renderers/react/src/renderToCanvas.tsx
+++ b/code/renderers/react/src/renderToCanvas.tsx
@@ -5,6 +5,7 @@ import type { RenderContext } from 'storybook/internal/types';
import { global } from '@storybook/global';
+import { getReactActEnvironment } from './act-compat';
import type { ReactRenderer, StoryContext } from './types';
const { FRAMEWORK_OPTIONS } = global;
@@ -57,7 +58,11 @@ export async function renderToCanvas(
const { renderElement, unmountElement } = await import('@storybook/react-dom-shim');
const Story = unboundStoryFn as FC>;
- const content = (
+ const isActEnabled = getReactActEnvironment();
+
+ const content = isActEnabled ? (
+
+ ) : (
diff --git a/code/vitest-setup.ts b/code/vitest-setup.ts
index 8edd64c36314..5eba16740d1d 100644
--- a/code/vitest-setup.ts
+++ b/code/vitest-setup.ts
@@ -7,6 +7,7 @@ const ignoreList = [
(error: any) => error.message.includes('":nth-child" is potentially unsafe'),
(error: any) => error.message.includes('":first-child" is potentially unsafe'),
(error: any) => error.message.match(/Browserslist: .* is outdated. Please run:/),
+ (error: any) => error.message.includes('Consider adding an error boundary'),
(error: any) =>
error.message.includes('react-async-component-lifecycle-hooks') &&
error.stack.includes('addons/knobs/src/components/__tests__/Options.js'),
diff --git a/code/yarn.lock b/code/yarn.lock
index 9b8a426f6016..397b6cd54ff5 100644
--- a/code/yarn.lock
+++ b/code/yarn.lock
@@ -2490,7 +2490,7 @@ __metadata:
languageName: node
linkType: hard
-"@emnapi/runtime@npm:^1.1.1":
+"@emnapi/runtime@npm:^1.1.1, @emnapi/runtime@npm:^1.2.0":
version: 1.2.0
resolution: "@emnapi/runtime@npm:1.2.0"
dependencies:
@@ -3424,6 +3424,18 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-darwin-arm64@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-darwin-arm64@npm:0.33.5"
+ dependencies:
+ "@img/sharp-libvips-darwin-arm64": "npm:1.0.4"
+ dependenciesMeta:
+ "@img/sharp-libvips-darwin-arm64":
+ optional: true
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@img/sharp-darwin-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-darwin-x64@npm:0.33.4"
@@ -3436,6 +3448,18 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-darwin-x64@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-darwin-x64@npm:0.33.5"
+ dependencies:
+ "@img/sharp-libvips-darwin-x64": "npm:1.0.4"
+ dependenciesMeta:
+ "@img/sharp-libvips-darwin-x64":
+ optional: true
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-darwin-arm64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-darwin-arm64@npm:1.0.2"
@@ -3443,6 +3467,13 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-darwin-arm64@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@img/sharp-libvips-darwin-arm64@npm:1.0.4"
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-darwin-x64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-darwin-x64@npm:1.0.2"
@@ -3450,6 +3481,13 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-darwin-x64@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@img/sharp-libvips-darwin-x64@npm:1.0.4"
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linux-arm64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linux-arm64@npm:1.0.2"
@@ -3457,6 +3495,13 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linux-arm64@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@img/sharp-libvips-linux-arm64@npm:1.0.4"
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linux-arm@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linux-arm@npm:1.0.2"
@@ -3464,6 +3509,13 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linux-arm@npm:1.0.5":
+ version: 1.0.5
+ resolution: "@img/sharp-libvips-linux-arm@npm:1.0.5"
+ conditions: os=linux & cpu=arm & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linux-s390x@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linux-s390x@npm:1.0.2"
@@ -3471,6 +3523,13 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linux-s390x@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@img/sharp-libvips-linux-s390x@npm:1.0.4"
+ conditions: os=linux & cpu=s390x & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linux-x64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linux-x64@npm:1.0.2"
@@ -3478,6 +3537,13 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linux-x64@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@img/sharp-libvips-linux-x64@npm:1.0.4"
+ conditions: os=linux & cpu=x64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linuxmusl-arm64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.0.2"
@@ -3485,6 +3551,13 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linuxmusl-arm64@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.0.4"
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linuxmusl-x64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.0.2"
@@ -3492,6 +3565,13 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linuxmusl-x64@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.0.4"
+ conditions: os=linux & cpu=x64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@img/sharp-linux-arm64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-arm64@npm:0.33.4"
@@ -3504,6 +3584,18 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-linux-arm64@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-linux-arm64@npm:0.33.5"
+ dependencies:
+ "@img/sharp-libvips-linux-arm64": "npm:1.0.4"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-arm64":
+ optional: true
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-linux-arm@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-arm@npm:0.33.4"
@@ -3516,6 +3608,18 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-linux-arm@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-linux-arm@npm:0.33.5"
+ dependencies:
+ "@img/sharp-libvips-linux-arm": "npm:1.0.5"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-arm":
+ optional: true
+ conditions: os=linux & cpu=arm & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-linux-s390x@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-s390x@npm:0.33.4"
@@ -3528,6 +3632,18 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-linux-s390x@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-linux-s390x@npm:0.33.5"
+ dependencies:
+ "@img/sharp-libvips-linux-s390x": "npm:1.0.4"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-s390x":
+ optional: true
+ conditions: os=linux & cpu=s390x & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-linux-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-x64@npm:0.33.4"
@@ -3540,6 +3656,18 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-linux-x64@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-linux-x64@npm:0.33.5"
+ dependencies:
+ "@img/sharp-libvips-linux-x64": "npm:1.0.4"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-x64":
+ optional: true
+ conditions: os=linux & cpu=x64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-linuxmusl-arm64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.4"
@@ -3552,6 +3680,18 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-linuxmusl-arm64@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.5"
+ dependencies:
+ "@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.4"
+ dependenciesMeta:
+ "@img/sharp-libvips-linuxmusl-arm64":
+ optional: true
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@img/sharp-linuxmusl-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linuxmusl-x64@npm:0.33.4"
@@ -3564,6 +3704,18 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-linuxmusl-x64@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-linuxmusl-x64@npm:0.33.5"
+ dependencies:
+ "@img/sharp-libvips-linuxmusl-x64": "npm:1.0.4"
+ dependenciesMeta:
+ "@img/sharp-libvips-linuxmusl-x64":
+ optional: true
+ conditions: os=linux & cpu=x64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@img/sharp-wasm32@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-wasm32@npm:0.33.4"
@@ -3573,6 +3725,15 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-wasm32@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-wasm32@npm:0.33.5"
+ dependencies:
+ "@emnapi/runtime": "npm:^1.2.0"
+ conditions: cpu=wasm32
+ languageName: node
+ linkType: hard
+
"@img/sharp-win32-ia32@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-win32-ia32@npm:0.33.4"
@@ -3580,6 +3741,13 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-win32-ia32@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-win32-ia32@npm:0.33.5"
+ conditions: os=win32 & cpu=ia32
+ languageName: node
+ linkType: hard
+
"@img/sharp-win32-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-win32-x64@npm:0.33.4"
@@ -3587,6 +3755,13 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-win32-x64@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-win32-x64@npm:0.33.5"
+ conditions: os=win32 & cpu=x64
+ languageName: node
+ linkType: hard
+
"@inquirer/confirm@npm:^3.0.0":
version: 3.1.20
resolution: "@inquirer/confirm@npm:3.1.20"
@@ -3903,13 +4078,20 @@ __metadata:
languageName: node
linkType: hard
-"@next/env@npm:14.2.5, @next/env@npm:^14.2.5":
+"@next/env@npm:14.2.5":
version: 14.2.5
resolution: "@next/env@npm:14.2.5"
checksum: 10c0/63d8b88ac450b3c37940a9e2119a63a1074aca89908574ade6157a8aa295275dcb3ac5f69e00883fc55d0f12963b73b74e87ba32a5768a489f9609c6be57b699
languageName: node
linkType: hard
+"@next/env@npm:^14.2.5":
+ version: 14.2.7
+ resolution: "@next/env@npm:14.2.7"
+ checksum: 10c0/1cda023007acda4d47036a25fba0e039d9b2df9c3770651dc289207e0537506675546c02b5b574fe92bb1adc1c887d948d5cb630673aa572754278b82d150b7e
+ languageName: node
+ linkType: hard
+
"@next/swc-darwin-arm64@npm:14.2.5":
version: 14.2.5
resolution: "@next/swc-darwin-arm64@npm:14.2.5"
@@ -6668,11 +6850,14 @@ __metadata:
type-fest: "npm:~2.19"
util-deprecate: "npm:^1.0.2"
peerDependencies:
+ "@storybook/test": "workspace:*"
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: "workspace:^"
typescript: ">= 4.2.x"
peerDependenciesMeta:
+ "@storybook/test":
+ optional: true
typescript:
optional: true
languageName: unknown
@@ -25319,6 +25504,15 @@ __metadata:
languageName: node
linkType: hard
+"semver@npm:^7.6.3":
+ version: 7.6.3
+ resolution: "semver@npm:7.6.3"
+ bin:
+ semver: bin/semver.js
+ checksum: 10c0/88f33e148b210c153873cb08cfe1e281d518aaa9a666d4d148add6560db5cd3c582f3a08ccb91f38d5f379ead256da9931234ed122057f40bb5766e65e58adaf
+ languageName: node
+ linkType: hard
+
"send@npm:0.18.0":
version: 0.18.0
resolution: "send@npm:0.18.0"
@@ -25470,7 +25664,7 @@ __metadata:
languageName: node
linkType: hard
-"sharp@npm:^0.33.3, sharp@npm:^0.33.4":
+"sharp@npm:^0.33.3":
version: 0.33.4
resolution: "sharp@npm:0.33.4"
dependencies:
@@ -25539,6 +25733,75 @@ __metadata:
languageName: node
linkType: hard
+"sharp@npm:^0.33.4":
+ version: 0.33.5
+ resolution: "sharp@npm:0.33.5"
+ dependencies:
+ "@img/sharp-darwin-arm64": "npm:0.33.5"
+ "@img/sharp-darwin-x64": "npm:0.33.5"
+ "@img/sharp-libvips-darwin-arm64": "npm:1.0.4"
+ "@img/sharp-libvips-darwin-x64": "npm:1.0.4"
+ "@img/sharp-libvips-linux-arm": "npm:1.0.5"
+ "@img/sharp-libvips-linux-arm64": "npm:1.0.4"
+ "@img/sharp-libvips-linux-s390x": "npm:1.0.4"
+ "@img/sharp-libvips-linux-x64": "npm:1.0.4"
+ "@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.4"
+ "@img/sharp-libvips-linuxmusl-x64": "npm:1.0.4"
+ "@img/sharp-linux-arm": "npm:0.33.5"
+ "@img/sharp-linux-arm64": "npm:0.33.5"
+ "@img/sharp-linux-s390x": "npm:0.33.5"
+ "@img/sharp-linux-x64": "npm:0.33.5"
+ "@img/sharp-linuxmusl-arm64": "npm:0.33.5"
+ "@img/sharp-linuxmusl-x64": "npm:0.33.5"
+ "@img/sharp-wasm32": "npm:0.33.5"
+ "@img/sharp-win32-ia32": "npm:0.33.5"
+ "@img/sharp-win32-x64": "npm:0.33.5"
+ color: "npm:^4.2.3"
+ detect-libc: "npm:^2.0.3"
+ semver: "npm:^7.6.3"
+ dependenciesMeta:
+ "@img/sharp-darwin-arm64":
+ optional: true
+ "@img/sharp-darwin-x64":
+ optional: true
+ "@img/sharp-libvips-darwin-arm64":
+ optional: true
+ "@img/sharp-libvips-darwin-x64":
+ optional: true
+ "@img/sharp-libvips-linux-arm":
+ optional: true
+ "@img/sharp-libvips-linux-arm64":
+ optional: true
+ "@img/sharp-libvips-linux-s390x":
+ optional: true
+ "@img/sharp-libvips-linux-x64":
+ optional: true
+ "@img/sharp-libvips-linuxmusl-arm64":
+ optional: true
+ "@img/sharp-libvips-linuxmusl-x64":
+ optional: true
+ "@img/sharp-linux-arm":
+ optional: true
+ "@img/sharp-linux-arm64":
+ optional: true
+ "@img/sharp-linux-s390x":
+ optional: true
+ "@img/sharp-linux-x64":
+ optional: true
+ "@img/sharp-linuxmusl-arm64":
+ optional: true
+ "@img/sharp-linuxmusl-x64":
+ optional: true
+ "@img/sharp-wasm32":
+ optional: true
+ "@img/sharp-win32-ia32":
+ optional: true
+ "@img/sharp-win32-x64":
+ optional: true
+ checksum: 10c0/6b81421ddfe6ee524d8d77e325c5e147fef22884e1c7b1656dfd89a88d7025894115da02d5f984261bf2e6daa16f98cadd1721c4ba408b4212b1d2a60f233484
+ languageName: node
+ linkType: hard
+
"shebang-command@npm:^1.2.0":
version: 1.2.0
resolution: "shebang-command@npm:1.2.0"