diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 0c77f07cefc2..7ad9ccf01312 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,8 @@ +## 8.5.0-beta.5 + +- Core: Fix `ERR_PACKAGE_PATH_NOT_EXPORTED` in `@storybook/node-logger` - [#30093](https://github.com/storybookjs/storybook/pull/30093), thanks @JReinhold! +- React: Use Act wrapper in Storybook for component rendering - [#30037](https://github.com/storybookjs/storybook/pull/30037), thanks @valentinpalkovic! + ## 8.5.0-beta.4 - Addon Themes: Deprecate useThemeParameters - [#30111](https://github.com/storybookjs/storybook/pull/30111), thanks @yannbf! diff --git a/MIGRATION.md b/MIGRATION.md index 28b4bc5d6ddc..7ab60984a274 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,7 @@

Migration

- [From version 8.4.x to 8.5.x](#from-version-84x-to-85x) + - [Introducing features.developmentModeForBuild](#introducing-featuresdevelopmentmodeforbuild) - [Added source code panel to docs](#added-source-code-panel-to-docs) - [Addon-a11y: Component test integration](#addon-a11y-component-test-integration) - [Addon-a11y: Deprecated `parameters.a11y.manual`](#addon-a11y-deprecated-parametersa11ymanual) @@ -110,17 +111,17 @@ - [Tab addons cannot manually route, Tool addons can filter their visibility via tabId](#tab-addons-cannot-manually-route-tool-addons-can-filter-their-visibility-via-tabid) - [Removed `config` preset](#removed-config-preset-1) - [From version 7.5.0 to 7.6.0](#from-version-750-to-760) - - [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated) - - [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated) - - [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated) - - [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop) - - [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react) + - [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated) + - [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated) + - [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated) + - [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop) + - [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react) - [From version 7.4.0 to 7.5.0](#from-version-740-to-750) - - [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated) - - [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers) + - [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated) + - [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers) - [From version 7.0.0 to 7.2.0](#from-version-700-to-720) - - [Addon API is more type-strict](#addon-api-is-more-type-strict) - - [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated) + - [Addon API is more type-strict](#addon-api-is-more-type-strict) + - [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated) - [From version 6.5.x to 7.0.0](#from-version-65x-to-700) - [7.0 breaking changes](#70-breaking-changes) - [Dropped support for Node 15 and below](#dropped-support-for-node-15-and-below) @@ -146,7 +147,7 @@ - [Deploying build artifacts](#deploying-build-artifacts) - [Dropped support for file URLs](#dropped-support-for-file-urls) - [Serving with nginx](#serving-with-nginx) - - [Ignore story files from node\_modules](#ignore-story-files-from-node_modules) + - [Ignore story files from node_modules](#ignore-story-files-from-node_modules) - [7.0 Core changes](#70-core-changes) - [7.0 feature flags removed](#70-feature-flags-removed) - [Story context is prepared before for supporting fine grained updates](#story-context-is-prepared-before-for-supporting-fine-grained-updates) @@ -160,7 +161,7 @@ - [Addon-interactions: Interactions debugger is now default](#addon-interactions-interactions-debugger-is-now-default) - [7.0 Vite changes](#70-vite-changes) - [Vite builder uses Vite config automatically](#vite-builder-uses-vite-config-automatically) - - [Vite cache moved to node\_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook) + - [Vite cache moved to node_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook) - [7.0 Webpack changes](#70-webpack-changes) - [Webpack4 support discontinued](#webpack4-support-discontinued) - [Babel mode v7 exclusively](#babel-mode-v7-exclusively) @@ -211,7 +212,7 @@ - [Dropped addon-docs manual babel configuration](#dropped-addon-docs-manual-babel-configuration) - [Dropped addon-docs manual configuration](#dropped-addon-docs-manual-configuration) - [Autoplay in docs](#autoplay-in-docs) - - [Removed STORYBOOK\_REACT\_CLASSES global](#removed-storybook_react_classes-global) + - [Removed STORYBOOK_REACT_CLASSES global](#removed-storybook_react_classes-global) - [7.0 Deprecations and default changes](#70-deprecations-and-default-changes) - [storyStoreV7 enabled by default](#storystorev7-enabled-by-default) - [`Story` type deprecated](#story-type-deprecated) @@ -426,6 +427,21 @@ ## From version 8.4.x to 8.5.x +### Introducing features.developmentModeForBuild + +As part of our ongoing efforts to improve the testability and debuggability of Storybook, we are introducing a new feature flag: `developmentModeForBuild`. This feature flag allows you to set `process.env.NODE_ENV` to `development` in built Storybooks, enabling development-related optimizations that are typically disabled in production builds. + +In development mode, React and other libraries often include additional checks and warnings that help catch potential issues early. These checks are usually stripped out in production builds to optimize performance. However, when running tests or debugging issues in a built Storybook, having these additional checks can be incredibly valuable. One such feature is React's `act`, which ensures that all updates related to a test are processed and applied before making assertions. `act` is crucial for reliable and predictable test results, but it only works correctly when `NODE_ENV` is set to `development`. + +```js +// main.js +export default { + features: { + developmentModeForBuild: true, + }, +}; +``` + ### Added source code panel to docs Starting in 8.5, Storybook Docs (`@storybook/addon-docs`) automatically adds a new addon panel to stories that displays a source snippet beneath each story. This works similarly to the existing [source snippet doc block](https://storybook.js.org/docs/writing-docs/doc-blocks#source), but in the story view. It is intended to replace the [Storysource addon](https://storybook.js.org/addons/@storybook/addon-storysource). diff --git a/code/.storybook/main.ts b/code/.storybook/main.ts index ab8af9af8f4a..623d6f5c525e 100644 --- a/code/.storybook/main.ts +++ b/code/.storybook/main.ts @@ -132,6 +132,7 @@ const config: StorybookConfig = { features: { viewportStoryGlobals: true, backgroundsStoryGlobals: true, + developmentModeForBuild: true, }, viteFinal: async (viteConfig, { configType }) => { const { mergeConfig } = await import('vite'); diff --git a/code/builders/builder-vite/src/build.ts b/code/builders/builder-vite/src/build.ts index fa7d1ee4f76e..9782081c0465 100644 --- a/code/builders/builder-vite/src/build.ts +++ b/code/builders/builder-vite/src/build.ts @@ -35,9 +35,22 @@ export async function build(options: Options) { } : {}), }, - }).build; + } as InlineConfig).build; - const finalConfig = await presets.apply('viteFinal', config, options); + const finalConfig = (await presets.apply('viteFinal', config, options)) as InlineConfig; + + if (options.features?.developmentModeForBuild) { + finalConfig.plugins?.push({ + name: 'storybook:define-env', + config: () => { + return { + define: { + 'process.env.NODE_ENV': JSON.stringify('development'), + }, + }; + }, + }); + } const turbosnapPluginName = 'rollup-plugin-turbosnap'; const hasTurbosnapPlugin = diff --git a/code/builders/builder-vite/src/index.ts b/code/builders/builder-vite/src/index.ts index 785db459cec4..7051cc116363 100644 --- a/code/builders/builder-vite/src/index.ts +++ b/code/builders/builder-vite/src/index.ts @@ -4,8 +4,6 @@ import { readFile } from 'node:fs/promises'; import { NoStatsForViteDevError } from 'storybook/internal/server-errors'; import type { Middleware, Options } from 'storybook/internal/types'; -import sirv from 'sirv'; -import { corePath } from 'storybook/core-path'; import type { ViteDevServer } from 'vite'; import { build as viteBuild } from './build'; diff --git a/code/builders/builder-webpack5/src/index.ts b/code/builders/builder-webpack5/src/index.ts index a8af6e699ad4..71be9982973a 100644 --- a/code/builders/builder-webpack5/src/index.ts +++ b/code/builders/builder-webpack5/src/index.ts @@ -16,13 +16,15 @@ import prettyTime from 'pretty-hrtime'; import sirv from 'sirv'; import { corePath } from 'storybook/core-path'; import type { Configuration, Stats, StatsOptions } from 'webpack'; -import webpack, { ProgressPlugin } from 'webpack'; +import webpackDep, { DefinePlugin, ProgressPlugin } from 'webpack'; import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; export * from './types'; export * from './preview/virtual-module-mapping'; +export const WebpackDefinePlugin = DefinePlugin; + export const printDuration = (startTime: [number, number]) => prettyTime(process.hrtime(startTime)) .replace(' ms', ' milliseconds') @@ -51,8 +53,8 @@ export const executor = { get: async (options: Options) => { const version = ((await options.presets.apply('webpackVersion')) || '5') as string; const webpackInstance = - (await options.presets.apply<{ default: typeof webpack }>('webpackInstance'))?.default || - webpack; + (await options.presets.apply<{ default: typeof webpackDep }>('webpackInstance'))?.default || + webpackDep; checkWebpackVersion({ version }, '5', 'builder-webpack5'); return webpackInstance; }, diff --git a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts index 763f2bf15646..aa9e34c73afe 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -195,7 +195,9 @@ export default async ( }), new DefinePlugin({ ...stringifyProcessEnvs(envs), - NODE_ENV: JSON.stringify(process.env.NODE_ENV), + NODE_ENV: JSON.stringify( + features?.developmentModeForBuild && isProd ? 'development' : process.env.NODE_ENV + ), }), new ProvidePlugin({ process: require.resolve('process/browser.js') }), isProd ? null : new HotModuleReplacementPlugin(), diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index 3254723fc4ee..2b4624a0564b 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -380,6 +380,8 @@ export interface StorybookConfigRaw { viewportStoryGlobals?: boolean; /** Use globals & globalTypes for configuring the backgrounds addon */ backgroundsStoryGlobals?: boolean; + /** Set NODE_ENV to development in built Storybooks for better testability and debuggability */ + developmentModeForBuild?: boolean; }; build?: TestBuildConfig; diff --git a/code/core/template/stories/rendering.stories.ts b/code/core/template/stories/rendering.stories.ts index b0d0a7b06a1d..77e9432ef512 100644 --- a/code/core/template/stories/rendering.stories.ts +++ b/code/core/template/stories/rendering.stories.ts @@ -42,6 +42,38 @@ export const ForceRemount = { tags: ['!test', '!vitest'], }; +let loadedLabel = 'Initial'; + +/** + * This story demonstrates what happens when rendering (loaders) have side effects, and can possibly + * interleave with each other Triggering multiple force remounts quickly should only result in a + * single remount in the end and the label should be 'Loaded. Click Me' at the end. If loaders are + * interleaving it would result in a label of 'Error: Interleaved loaders. Click Me' Similarly, + * changing args rapidly should only cause one rerender at a time, producing the same result. + */ +export const SlowLoader = { + parameters: { + chromatic: { disable: true }, + }, + loaders: [ + async () => { + loadedLabel = 'Loading...'; + await new Promise((resolve) => setTimeout(resolve, 1000)); + loadedLabel = loadedLabel === 'Loading...' ? 'Loaded.' : 'Error: Interleaved loaders.'; + return { label: loadedLabel }; + }, + ], + decorators: [ + (storyFn: any, context: any) => + storyFn({ + args: { + ...context.args, + label: `${context.loaded.label} ${context.args.label}`, + }, + }), + ], +}; + export const ChangeArgs = { play: async ({ canvasElement, id }: PlayFunctionContext) => { const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; @@ -74,35 +106,3 @@ export const ChangeArgs = { await expect(button).toHaveFocus(); }, }; - -let loadedLabel = 'Initial'; - -/** - * This story demonstrates what happens when rendering (loaders) have side effects, and can possibly - * interleave with each other Triggering multiple force remounts quickly should only result in a - * single remount in the end and the label should be 'Loaded. Click Me' at the end. If loaders are - * interleaving it would result in a label of 'Error: Interleaved loaders. Click Me' Similarly, - * changing args rapidly should only cause one rerender at a time, producing the same result. - */ -export const SlowLoader = { - parameters: { - chromatic: { disable: true }, - }, - loaders: [ - async () => { - loadedLabel = 'Loading...'; - await new Promise((resolve) => setTimeout(resolve, 1000)); - loadedLabel = loadedLabel === 'Loading...' ? 'Loaded.' : 'Error: Interleaved loaders.'; - return { label: loadedLabel }; - }, - ], - decorators: [ - (storyFn: any, context: any) => - storyFn({ - args: { - ...context.args, - label: `${context.loaded.label} ${context.args.label}`, - }, - }), - ], -}; diff --git a/code/deprecated/node-logger/package.json b/code/deprecated/node-logger/package.json index 2308a724860b..334463b54dcc 100644 --- a/code/deprecated/node-logger/package.json +++ b/code/deprecated/node-logger/package.json @@ -23,7 +23,7 @@ "exports": { ".": { "types": "./shim.d.ts", - "module": "./shim.mjs", + "import": "./shim.mjs", "require": "./shim.js" }, "./package.json": "./package.json" diff --git a/code/frameworks/react-webpack5/src/preset.ts b/code/frameworks/react-webpack5/src/preset.ts index 9e233459c10b..08f870a23054 100644 --- a/code/frameworks/react-webpack5/src/preset.ts +++ b/code/frameworks/react-webpack5/src/preset.ts @@ -2,6 +2,8 @@ import { dirname, join } from 'node:path'; import type { PresetProperty } from 'storybook/internal/types'; +import { WebpackDefinePlugin } from '@storybook/builder-webpack5'; + import type { StorybookConfig } from './types'; const getAbsolutePath = (input: I): I => @@ -24,12 +26,23 @@ export const core: PresetProperty<'core'> = async (config, options) => { }; }; -export const webpack: StorybookConfig['webpack'] = async (config) => { +export const webpack: StorybookConfig['webpack'] = async (config, options) => { config.resolve = config.resolve || {}; config.resolve.alias = { ...config.resolve?.alias, '@storybook/react': getAbsolutePath('@storybook/react'), }; + + if (options.features?.developmentModeForBuild) { + config.plugins = [ + // @ts-expect-error Ignore this error, because in the `webpack` preset the user actually hasn't defined a config yet. + ...config.plugins, + new WebpackDefinePlugin({ + NODE_ENV: JSON.stringify('development'), + }), + ]; + } + return config; }; diff --git a/code/lib/cli-storybook/src/sandbox-templates.ts b/code/lib/cli-storybook/src/sandbox-templates.ts index 26ce0de96aa4..0324635b1ffe 100644 --- a/code/lib/cli-storybook/src/sandbox-templates.ts +++ b/code/lib/cli-storybook/src/sandbox-templates.ts @@ -150,7 +150,10 @@ const baseTemplates = { }, modifications: { mainConfig: { - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, extraDependencies: ['server-only', 'prop-types'], }, @@ -167,7 +170,10 @@ const baseTemplates = { }, modifications: { mainConfig: { - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, extraDependencies: ['server-only', 'prop-types'], }, @@ -184,7 +190,10 @@ const baseTemplates = { }, modifications: { mainConfig: { - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, extraDependencies: ['server-only', 'prop-types'], }, @@ -200,10 +209,13 @@ const baseTemplates = { builder: '@storybook/builder-webpack5', }, modifications: { - extraDependencies: ['server-only', 'prop-types'], mainConfig: { - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, + extraDependencies: ['server-only', 'prop-types'], }, skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'], }, @@ -219,7 +231,10 @@ const baseTemplates = { modifications: { mainConfig: { framework: '@storybook/experimental-nextjs-vite', - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, extraDependencies: [ 'server-only', @@ -242,7 +257,10 @@ const baseTemplates = { modifications: { mainConfig: { framework: '@storybook/experimental-nextjs-vite', - features: { experimentalRSC: true }, + features: { + experimentalRSC: true, + developmentModeForBuild: true, + }, }, extraDependencies: [ 'server-only', @@ -263,6 +281,11 @@ const baseTemplates = { }, modifications: { extraDependencies: ['prop-types'], + mainConfig: { + features: { + developmentModeForBuild: true, + }, + }, }, skipTasks: ['e2e-tests-dev', 'bench'], }, @@ -276,6 +299,11 @@ const baseTemplates = { }, modifications: { extraDependencies: ['prop-types'], + mainConfig: { + features: { + developmentModeForBuild: true, + }, + }, }, skipTasks: ['bench'], }, @@ -302,6 +330,11 @@ const baseTemplates = { }, modifications: { extraDependencies: ['prop-types'], + mainConfig: { + features: { + developmentModeForBuild: true, + }, + }, }, skipTasks: ['e2e-tests-dev', 'bench'], }, diff --git a/code/package.json b/code/package.json index 380e92675b7f..9c51049b2b56 100644 --- a/code/package.json +++ b/code/package.json @@ -294,5 +294,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.5.0-beta.5" } diff --git a/code/renderers/react/src/act-compat.ts b/code/renderers/react/src/act-compat.ts index 36e56712e02b..7d64e0f7c3be 100644 --- a/code/renderers/react/src/act-compat.ts +++ b/code/renderers/react/src/act-compat.ts @@ -40,15 +40,15 @@ function withGlobalActEnvironment(actImplementation: (callback: () => void) => P return result; }); if (callbackNeedsToBeAwaited) { - const thenable: Promise = actResult; + const thenable = actResult; return { then: (resolve: (param: any) => void, reject: (param: any) => void) => { thenable.then( - (returnValue) => { + (returnValue: any) => { setReactActEnvironment(previousActEnvironment); resolve(returnValue); }, - (error) => { + (error: any) => { setReactActEnvironment(previousActEnvironment); reject(error); } @@ -68,4 +68,7 @@ function withGlobalActEnvironment(actImplementation: (callback: () => void) => P }; } -export const act = withGlobalActEnvironment(reactAct); +export const act = + process.env.NODE_ENV === 'production' + ? (cb: (...args: any[]) => any) => cb() + : withGlobalActEnvironment(reactAct); diff --git a/code/renderers/react/src/entry-preview.tsx b/code/renderers/react/src/entry-preview.tsx index e1b214e449c0..b2e6c1de5b18 100644 --- a/code/renderers/react/src/entry-preview.tsx +++ b/code/renderers/react/src/entry-preview.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import semver from 'semver'; +import { act, getReactActEnvironment, setReactActEnvironment } from './act-compat'; import type { Decorator } from './public-types'; export const parameters = { renderer: 'react' }; @@ -24,3 +25,70 @@ export const decorators: Decorator[] = [ return {story()}; }, ]; + +export const beforeAll = async () => { + 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); + }, + // For more context about why we need disable act warnings in waitFor: + // https://github.com/reactwg/react-18/discussions/102 + 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; + }); + return result; + }, + }); + } catch (e) { + // no-op + // @storybook/test might not be available + } +}; + +/** 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/portable-stories.tsx b/code/renderers/react/src/portable-stories.tsx index 7b906c9f4bde..ca29c8c7de72 100644 --- a/code/renderers/react/src/portable-stories.tsx +++ b/code/renderers/react/src/portable-stories.tsx @@ -17,7 +17,6 @@ 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'; @@ -55,67 +54,14 @@ export function setProjectAnnotations( // This will not be necessary once we have auto preset loading export const INTERNAL_DEFAULT_PROJECT_ANNOTATIONS: ProjectAnnotations = { ...reactProjectAnnotations, - 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); - }, - // For more context about why we need disable act warnings in waitFor: - // https://github.com/reactwg/react-18/discussions/102 - 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) { - // no-op - // @storybook/test might not be available - } - }, renderToCanvas: async (renderContext, canvasElement) => { if (renderContext.storyContext.testingLibraryRender == null) { - let unmount: () => void; - - await act(async () => { - unmount = await reactProjectAnnotations.renderToCanvas(renderContext, canvasElement); - }); + // eslint-disable-next-line no-underscore-dangle + renderContext.storyContext.parameters.__isPortableStory = true; + const unmount = await reactProjectAnnotations.renderToCanvas(renderContext, canvasElement); return async () => { - await act(() => { - unmount(); - }); + await unmount(); }; } const { @@ -209,19 +155,3 @@ 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 3ae6136f9582..4ae1acbb7fe9 100644 --- a/code/renderers/react/src/renderToCanvas.tsx +++ b/code/renderers/react/src/renderToCanvas.tsx @@ -5,7 +5,7 @@ import type { RenderContext } from 'storybook/internal/types'; import { global } from '@storybook/global'; -import { getReactActEnvironment } from './act-compat'; +import { act } from './act-compat'; import type { ReactRenderer, StoryContext } from './types'; const { FRAMEWORK_OPTIONS } = global; @@ -58,9 +58,10 @@ export async function renderToCanvas( const { renderElement, unmountElement } = await import('@storybook/react-dom-shim'); const Story = unboundStoryFn as FC>; - const isActEnabled = getReactActEnvironment(); + // eslint-disable-next-line no-underscore-dangle + const isPortableStory = storyContext.parameters.__isPortableStory; - const content = isActEnabled ? ( + const content = isPortableStory ? ( ) : ( @@ -80,7 +81,13 @@ export async function renderToCanvas( unmountElement(canvasElement); } - await renderElement(element, canvasElement, storyContext?.parameters?.react?.rootOptions); + await act(async () => { + await renderElement(element, canvasElement, storyContext?.parameters?.react?.rootOptions); + }); - return () => unmountElement(canvasElement); + return async () => { + await act(() => { + unmountElement(canvasElement); + }); + }; } diff --git a/code/renderers/react/template/cli/js/Button.jsx b/code/renderers/react/template/cli/js/Button.jsx index 5b36a6347d07..dabe38e0e82a 100644 --- a/code/renderers/react/template/cli/js/Button.jsx +++ b/code/renderers/react/template/cli/js/Button.jsx @@ -5,7 +5,13 @@ import PropTypes from 'prop-types'; import './button.css'; /** Primary UI component for user interaction */ -export const Button = ({ primary, backgroundColor, size, label, ...props }) => { +export const Button = ({ + primary = false, + backgroundColor = null, + size = 'medium', + label, + ...props +}) => { const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; return (