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`] = ` `; +exports[`Legacy Portable Stories API > Renders MountInPlayFunction story 1`] = ` + +
+
+ loaded data +
+
+ mockFn return value +
+
+ +`; + +exports[`Legacy Portable Stories API > Renders MountInPlayFunctionThrow story 1`] = ` + +
+
+ loaded data +
+
+ mockFn return value +
+
+ +`; + exports[`Legacy Portable Stories API > Renders WithActionArg 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"