diff --git a/.circleci/config.yml b/.circleci/config.yml index e124f267f1b..f67b712e04d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -836,19 +836,19 @@ workflows: requires: - unit-tests - create-sandboxes: - parallelism: 13 + parallelism: 14 requires: - build - build-sandboxes: - parallelism: 13 + parallelism: 14 requires: - create-sandboxes - chromatic-sandboxes: - parallelism: 10 + parallelism: 11 requires: - build-sandboxes - e2e-production: - parallelism: 8 + parallelism: 9 requires: - build-sandboxes - e2e-dev: @@ -856,7 +856,7 @@ workflows: requires: - create-sandboxes - test-runner-production: - parallelism: 8 + parallelism: 9 requires: - build-sandboxes - vitest-integration: @@ -912,19 +912,19 @@ workflows: requires: - unit-tests - create-sandboxes: - parallelism: 19 + parallelism: 20 requires: - build - build-sandboxes: - parallelism: 19 + parallelism: 20 requires: - create-sandboxes - chromatic-sandboxes: - parallelism: 16 + parallelism: 17 requires: - build-sandboxes - e2e-production: - parallelism: 14 + parallelism: 15 requires: - build-sandboxes - e2e-dev: @@ -932,7 +932,7 @@ workflows: requires: - create-sandboxes - test-runner-production: - parallelism: 14 + parallelism: 15 requires: - build-sandboxes - vitest-integration: @@ -986,22 +986,22 @@ workflows: requires: - build - create-sandboxes: - parallelism: 36 + parallelism: 37 requires: - build # - smoke-test-sandboxes: # disabled for now # requires: # - create-sandboxes - build-sandboxes: - parallelism: 36 + parallelism: 37 requires: - create-sandboxes - chromatic-sandboxes: - parallelism: 33 + parallelism: 34 requires: - build-sandboxes - e2e-production: - parallelism: 31 + parallelism: 32 requires: - build-sandboxes - e2e-dev: @@ -1009,7 +1009,7 @@ workflows: requires: - create-sandboxes - test-runner-production: - parallelism: 31 + parallelism: 32 requires: - build-sandboxes - vitest-integration: diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index d50dc7efdf2..c0e166bea16 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -17,6 +17,8 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 2 - name: Set node version uses: actions/setup-node@v4 with: diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index bfd6341e785..2a2eb7b7453 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,72 @@ +## 8.5.0-beta.5 + +- Addon Test: Only reset story count on file change when watch mode is enabled - [#30121](https://github.com/storybookjs/storybook/pull/30121), thanks @ghengeveld! +- Build: Revert Downgrade to esbuild 0.24.0 - [#30120](https://github.com/storybookjs/storybook/pull/30120), thanks @yannbf! +- 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! +- Vite: Add extra entries to `optimizeDeps` - [#30117](https://github.com/storybookjs/storybook/pull/30117), thanks @ndelangen! + +## 8.5.0-beta.4 + +- Addon Themes: Deprecate useThemeParameters - [#30111](https://github.com/storybookjs/storybook/pull/30111), thanks @yannbf! +- Build: Downgrade to esbuild 0.24.0 - [#30116](https://github.com/storybookjs/storybook/pull/30116), thanks @yannbf! +- CLI: Re-Add Nuxt support - [#28607](https://github.com/storybookjs/storybook/pull/28607), thanks @valentinpalkovic! +- Core: Prevent infinite rerendering caused by comparison by reference - [#30081](https://github.com/storybookjs/storybook/pull/30081), thanks @ghengeveld! + +## 8.5.0-beta.3 + +- Addon A11y: Fix skipped status handling in Testing Module - [#30077](https://github.com/storybookjs/storybook/pull/30077), thanks @valentinpalkovic! +- Core: Float context menu button on top of story titles in sidebar - [#30080](https://github.com/storybookjs/storybook/pull/30080), thanks @ghengeveld! +- Onboarding: Replace `react-confetti` with `@neoconfetti/react` - [#30098](https://github.com/storybookjs/storybook/pull/30098), thanks @ndelangen! + +## 8.5.0-beta.2 + +- Addon Test: Clear coverage data when starting or watching - [#30072](https://github.com/storybookjs/storybook/pull/30072), thanks @ghengeveld! +- Addon Test: Improve error message on missing coverage package - [#30088](https://github.com/storybookjs/storybook/pull/30088), thanks @JReinhold! +- UI: Fix test provider event handling on startup - [#30083](https://github.com/storybookjs/storybook/pull/30083), thanks @ghengeveld! +- UI: Keep failing stories in the sidebar, disregarding filters - [#30086](https://github.com/storybookjs/storybook/pull/30086), thanks @JReinhold! + +## 8.5.0-beta.1 + +- Addon A11y: Add conditional rendering for a11y violation number in Testing Module - [#30073](https://github.com/storybookjs/storybook/pull/30073), thanks @valentinpalkovic! +- Addon A11y: Remove warnings API - [#30049](https://github.com/storybookjs/storybook/pull/30049), thanks @kasperpeulen! +- Addon A11y: Show errors of axe properly - [#30050](https://github.com/storybookjs/storybook/pull/30050), thanks @kasperpeulen! +- Addon Test: Fix printing null% for coverage - [#30061](https://github.com/storybookjs/storybook/pull/30061), thanks @ghengeveld! +- Telemetry: Add metadata distinguishing "apps" from "design systems" - [#30070](https://github.com/storybookjs/storybook/pull/30070), thanks @tmeasday! + +## 8.5.0-beta.0 + +- Automigration: Improve setup file transformation and version range handling for a11y migration - [#30060](https://github.com/storybookjs/storybook/pull/30060), thanks @valentinpalkovic! +- Next.js: Support v15.1.1 - [#30068](https://github.com/storybookjs/storybook/pull/30068), thanks @valentinpalkovic! + +## 8.5.0-alpha.22 + +- Addon Docs: Dynamically import rehype - [#29544](https://github.com/storybookjs/storybook/pull/29544), thanks @valentinpalkovic! +- Addon Test: Fix duplicate `test.include` patterns - [#30029](https://github.com/storybookjs/storybook/pull/30029), thanks @JReinhold! +- Addon Test: Fix environment variable for Vitest Storybook integration - [#30054](https://github.com/storybookjs/storybook/pull/30054), thanks @valentinpalkovic! +- Addon Test: Use local storybook binary instead - [#30021](https://github.com/storybookjs/storybook/pull/30021), thanks @kasperpeulen! +- Addon Test: Wait for 2 seconds before showing result mismatch warning - [#30002](https://github.com/storybookjs/storybook/pull/30002), thanks @ghengeveld! +- Angular: Support statsJson in angular schemas - [#29233](https://github.com/storybookjs/storybook/pull/29233), thanks @yannbf! +- Core: Fix `scrollIntoView` behavior and reimplement testing module time rendering - [#30044](https://github.com/storybookjs/storybook/pull/30044), thanks @ghengeveld! +- Docs: Add code snippet to addons panel - [#29253](https://github.com/storybookjs/storybook/pull/29253), thanks @larsrickert! +- Next.js: Fix webpack fsCache not working - [#29654](https://github.com/storybookjs/storybook/pull/29654), thanks @sentience! +- Nextjs-Vite: Add TS docgen support - [#29824](https://github.com/storybookjs/storybook/pull/29824), thanks @yannbf! +- Nextjs-Vite: Fix docgen types in main config - [#30042](https://github.com/storybookjs/storybook/pull/30042), thanks @yannbf! +- React: Fix RSC compatibility with addon-themes and hooks - [#26243](https://github.com/storybookjs/storybook/pull/26243), thanks @shilman! +- UI: Fix controls and parameters on tag-filtered stories - [#30038](https://github.com/storybookjs/storybook/pull/30038), thanks @shilman! + +## 8.5.0-alpha.21 + +- Addon A11y: Add typesVersions support for TypeScript definitions in a11y package - [#30005](https://github.com/storybookjs/storybook/pull/30005), thanks @valentinpalkovic! +- Addon A11y: Refactor environment variable handling for Vitest integration - [#30022](https://github.com/storybookjs/storybook/pull/30022), thanks @valentinpalkovic! +- Addon A11y: Run the a11y automigration on postInstall - [#30004](https://github.com/storybookjs/storybook/pull/30004), thanks @kasperpeulen! +- Addon A11y: Update accessibility status handling in TestProviderRender - [#30027](https://github.com/storybookjs/storybook/pull/30027), thanks @valentinpalkovic! +- Addon Onboarding: Prebundle react-confetti - [#29996](https://github.com/storybookjs/storybook/pull/29996), thanks @yannbf! +- Addon Test: Correctly stop Storybook when Vitest closes - [#30012](https://github.com/storybookjs/storybook/pull/30012), thanks @JReinhold! +- Addon Test: Show sub test provider toggle state in main testing module - [#30019](https://github.com/storybookjs/storybook/pull/30019), thanks @ghengeveld! +- Addon Test: Wrap sub-paths exported with `require.resolve` - [#30026](https://github.com/storybookjs/storybook/pull/30026), thanks @ndelangen! +- Core: Fix bundling of React - [#30003](https://github.com/storybookjs/storybook/pull/30003), thanks @yannbf! + ## 8.5.0-alpha.20 - Addon Test: Add `@vitest/coverage-v8` during postinstall if no coverage reporter is installed - [#29993](https://github.com/storybookjs/storybook/pull/29993), thanks @ghengeveld! diff --git a/MIGRATION.md b/MIGRATION.md index fd3ce9e10be..7ab60984a27 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,8 @@

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) - [Indexing behavior of @storybook/experimental-addon-test is changed](#indexing-behavior-of-storybookexperimental-addon-test-is-changed) @@ -109,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) @@ -145,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) @@ -159,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) @@ -210,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) @@ -425,14 +427,47 @@ ## 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). + +If you wish to disable this panel globally, add the following line to your `.storybook/preview.js` project configuration. You can also selectively disable/enable at the story level. + +```js +export default { + parameters: { + docs: { + codePanel: false, + }, + }, +}; +``` + ### Addon-a11y: Component test integration -In Storybook 8.4, we introduced a new addon called [addon test](https://storybook.js.org/docs/writing-tests/test-addon). Powered by Vitest under the hood, this addon lets you watch, run, and debug your component tests directly in Storybook. +In Storybook 8.4, we introduced the [Test addon](https://storybook.js.org/docs/writing-tests/test-addon) (`@storybook/experimental-addon-test`). Powered by Vitest under the hood, this addon lets you watch, run, and debug your component tests directly in Storybook. -In Storybook 8.5, we revamped the Accessibility addon (`@storybook/addon-a11y`) to integrate it with the component tests feature. This means you can now extend your component tests to include accessibility tests. If you upgrade to Storybook 8.5 via `npx storybook@latest upgrade`, the Accessibility addon will be automatically configured to work with the component tests. However, if you're upgrading manually and you have the [addon test](https://storybook.js.org/docs/writing-tests/test-addon) installed, adjust your configuration as follows: +In Storybook 8.5, we revamped the [Accessibility addon](https://storybook.js.org/docs/writing-tests/accessibility-testing) (`@storybook/addon-a11y`) to integrate it with the component tests feature. This means you can now extend your component tests to include accessibility tests. + +If you upgrade to Storybook 8.5 via `npx storybook@latest upgrade`, the Accessibility addon will be automatically configured to work with the component tests. However, if you're upgrading manually and you have the Test addon installed, adjust your configuration as follows: ```diff -// .storybook/vitest.config.ts +// .storybook/vitest.setup.ts ... +import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'; diff --git a/code/.storybook/main.ts b/code/.storybook/main.ts index ab8af9af8f4..9870741de29 100644 --- a/code/.storybook/main.ts +++ b/code/.storybook/main.ts @@ -4,6 +4,7 @@ import type { StorybookConfig } from '../frameworks/react-vite'; const componentsPath = join(__dirname, '../core/src/components'); const managerApiPath = join(__dirname, '../core/src/manager-api'); +const imageContextPath = join(__dirname, '..//frameworks/nextjs/src/image-context.ts'); const config: StorybookConfig = { stories: [ @@ -132,6 +133,7 @@ const config: StorybookConfig = { features: { viewportStoryGlobals: true, backgroundsStoryGlobals: true, + developmentModeForBuild: true, }, viteFinal: async (viteConfig, { configType }) => { const { mergeConfig } = await import('vite'); @@ -145,6 +147,7 @@ const config: StorybookConfig = { 'storybook/internal/components': componentsPath, '@storybook/manager-api': managerApiPath, 'storybook/internal/manager-api': managerApiPath, + 'sb-original/image-context': imageContextPath, } : {}), }, diff --git a/code/.storybook/preview.tsx b/code/.storybook/preview.tsx index 017419318b0..23f95a0c5d5 100644 --- a/code/.storybook/preview.tsx +++ b/code/.storybook/preview.tsx @@ -358,9 +358,6 @@ export const parameters = { opacity: 0.4, }, }, - a11y: { - warnings: ['minor', 'moderate', 'serious', 'critical'], - }, }; -export const tags = ['test', 'vitest']; +export const tags = ['test', 'vitest', '!a11ytest']; diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json index 3d686e99ed6..b6a82ae028f 100644 --- a/code/addons/a11y/package.json +++ b/code/addons/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-a11y", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Test component compliance with web accessibility standards", "keywords": [ "a11y", @@ -38,7 +38,8 @@ }, "./manager": "./dist/manager.js", "./register": "./dist/manager.js", - "./package.json": "./package.json" + "./package.json": "./package.json", + "./postinstall": "./dist/postinstall.js" }, "main": "dist/index.js", "module": "dist/index.mjs", @@ -74,6 +75,7 @@ "@storybook/global": "^5.0.0", "@storybook/icons": "^1.2.12", "@testing-library/react": "^14.0.0", + "execa": "^9.5.2", "picocolors": "^1.1.0", "pretty-format": "^29.7.0", "react": "^18.2.0", @@ -97,6 +99,9 @@ ], "previewEntries": [ "./src/preview.tsx" + ], + "nodeEntries": [ + "./src/postinstall.ts" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16", diff --git a/code/addons/a11y/src/components/A11YPanel.tsx b/code/addons/a11y/src/components/A11YPanel.tsx index dc048d97b94..61ea9a663af 100644 --- a/code/addons/a11y/src/components/A11YPanel.tsx +++ b/code/addons/a11y/src/components/A11YPanel.tsx @@ -133,7 +133,11 @@ export const A11YPanel: React.FC = () => { <> The accessibility scan encountered an error.
- {typeof error === 'string' ? error : JSON.stringify(error)} + {typeof error === 'string' + ? error + : error instanceof Error + ? error.toString() + : JSON.stringify(error)} )} diff --git a/code/addons/a11y/src/params.ts b/code/addons/a11y/src/params.ts index dd435768734..e66a0813a42 100644 --- a/code/addons/a11y/src/params.ts +++ b/code/addons/a11y/src/params.ts @@ -6,8 +6,6 @@ export interface Setup { options: RunOptions; } -type Impact = NonNullable; - export interface A11yParameters { element?: ElementContext; config?: Spec; @@ -15,5 +13,4 @@ export interface A11yParameters { /** @deprecated Use globals.a11y.manual instead */ manual?: boolean; disable?: boolean; - warnings?: Impact[]; } diff --git a/code/addons/a11y/src/postinstall.ts b/code/addons/a11y/src/postinstall.ts new file mode 100644 index 00000000000..9a64796f5e1 --- /dev/null +++ b/code/addons/a11y/src/postinstall.ts @@ -0,0 +1,15 @@ +import type { PostinstallOptions } from '@storybook/cli/src/add'; + +// eslint-disable-next-line depend/ban-dependencies +import { execa } from 'execa'; + +const $ = execa({ + preferLocal: true, + stdio: 'inherit', + // we stream the stderr to the console + reject: false, +}); + +export default async function postinstall(options: PostinstallOptions) { + await $`storybook automigrate addonA11yAddonTest ${options.yes ? '--yes' : ''}`; +} diff --git a/code/addons/a11y/src/preview.test.tsx b/code/addons/a11y/src/preview.test.tsx index 334f7f924ae..d09ba89462e 100644 --- a/code/addons/a11y/src/preview.test.tsx +++ b/code/addons/a11y/src/preview.test.tsx @@ -156,60 +156,6 @@ describe('afterEach', () => { }); }); - it('should report warning status when there are only warnings', async () => { - const context = createContext({ - parameters: { - a11y: { - warnings: ['minor'], - }, - }, - }); - const result = { - violations: [ - { impact: 'minor', nodes: [] }, - { impact: 'critical', nodes: [] }, - ], - }; - mockedRun.mockResolvedValue(result as any); - - await expect(async () => experimental_afterEach(context)).rejects.toThrow(); - - expect(mockedRun).toHaveBeenCalledWith(context.parameters.a11y); - expect(context.reporting.addReport).toHaveBeenCalledWith({ - type: 'a11y', - version: 1, - result, - status: 'failed', - }); - }); - - it('should report error status when there are warnings and errors', async () => { - const context = createContext({ - parameters: { - a11y: { - warnings: ['minor'], - }, - }, - }); - const result = { - violations: [ - { impact: 'minor', nodes: [] }, - { impact: 'critical', nodes: [] }, - ], - }; - mockedRun.mockResolvedValue(result as any); - - await expect(async () => experimental_afterEach(context)).rejects.toThrow(); - - expect(mockedRun).toHaveBeenCalledWith(context.parameters.a11y); - expect(context.reporting.addReport).toHaveBeenCalledWith({ - type: 'a11y', - version: 1, - result, - status: 'failed', - }); - }); - it('should run accessibility checks if "a11ytest" flag is not available and is not running in Vitest', async () => { const context = createContext({ tags: [], diff --git a/code/addons/a11y/src/preview.tsx b/code/addons/a11y/src/preview.tsx index f7d2f9aa43f..e496894cb11 100644 --- a/code/addons/a11y/src/preview.tsx +++ b/code/addons/a11y/src/preview.tsx @@ -21,7 +21,6 @@ export const experimental_afterEach: AfterEach = async ({ }) => { const a11yParameter: A11yParameters | undefined = parameters.a11y; const a11yGlobals = globals.a11y; - const warnings = a11yParameter?.warnings ?? []; const shouldRunEnvironmentIndependent = a11yParameter?.manual !== true && @@ -38,15 +37,11 @@ export const experimental_afterEach: AfterEach = async ({ if (result) { const hasViolations = (result?.violations.length ?? 0) > 0; - const hasErrors = result?.violations.some( - (violation) => !warnings.includes(violation.impact!) - ); - reporting.addReport({ type: 'a11y', version: 1, result: result, - status: hasErrors ? 'failed' : hasViolations ? 'warning' : 'passed', + status: hasViolations ? 'failed' : 'passed', }); /** @@ -58,7 +53,7 @@ export const experimental_afterEach: AfterEach = async ({ * implement proper try catch handling. */ if (getIsVitestStandaloneRun()) { - if (hasErrors) { + if (hasViolations) { // @ts-expect-error - todo - fix type extension of expect from @storybook/test expect(result).toHaveNoViolations(); } diff --git a/code/addons/a11y/src/utils.ts b/code/addons/a11y/src/utils.ts index 0864a2e3b2f..f0612d5dfc7 100644 --- a/code/addons/a11y/src/utils.ts +++ b/code/addons/a11y/src/utils.ts @@ -1,25 +1,17 @@ export function getIsVitestStandaloneRun() { try { - return process.env.VITEST_STORYBOOK === 'false'; - } catch { - try { - // @ts-expect-error Suppress TypeScript warning about wrong setting. Doesn't matter, because we don't use tsc for bundling. - return import.meta.env.VITEST_STORYBOOK === 'false'; - } catch (e) { - return false; - } + // @ts-expect-error Suppress TypeScript warning about wrong setting. Doesn't matter, because we don't use tsc for bundling. + return import.meta.env.VITEST_STORYBOOK === 'false'; + } catch (e) { + return false; } } export function getIsVitestRunning() { try { - return process?.env.MODE === 'test'; - } catch { - try { - // @ts-expect-error Suppress TypeScript warning about wrong setting. Doesn't matter, because we don't use tsc for bundling. - return import.meta.env.MODE === 'test'; - } catch (e) { - return false; - } + // @ts-expect-error Suppress TypeScript warning about wrong setting. Doesn't matter, because we don't use tsc for bundling. + return import.meta.env.MODE === 'test'; + } catch (e) { + return false; } } diff --git a/code/addons/actions/package.json b/code/addons/actions/package.json index 45fc3815ec5..f5032604dcf 100644 --- a/code/addons/actions/package.json +++ b/code/addons/actions/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-actions", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Get UI feedback when an action is performed on an interactive element", "keywords": [ "storybook", diff --git a/code/addons/backgrounds/package.json b/code/addons/backgrounds/package.json index ef24a496f8b..aecb2b2a990 100644 --- a/code/addons/backgrounds/package.json +++ b/code/addons/backgrounds/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-backgrounds", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Switch backgrounds to view components in different settings", "keywords": [ "addon", diff --git a/code/addons/controls/package.json b/code/addons/controls/package.json index 0162f7ed1c0..adf5675c056 100644 --- a/code/addons/controls/package.json +++ b/code/addons/controls/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-controls", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Interact with component inputs dynamically in the Storybook UI", "keywords": [ "addon", diff --git a/code/addons/docs/package.json b/code/addons/docs/package.json index 2cb49f00af6..0b416f5bf77 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-docs", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Document component usage and properties in Markdown", "keywords": [ "addon", @@ -71,7 +71,12 @@ "./angular": "./angular/index.js", "./angular/index.js": "./angular/index.js", "./web-components/index.js": "./web-components/index.js", - "./package.json": "./package.json" + "./package.json": "./package.json", + "./manager": { + "types": "./dist/manager.d.ts", + "import": "./dist/manager.mjs", + "require": "./dist/manager.js" + } }, "main": "dist/index.js", "module": "dist/index.mjs", @@ -129,7 +134,11 @@ "./src/preview.ts", "./src/blocks.ts", "./src/shims/mdx-react-shim.ts", - "./src/mdx-loader.ts" + "./src/mdx-loader.ts", + "./src/manager.tsx" + ], + "managerEntries": [ + "./src/manager.tsx" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16", diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx new file mode 100644 index 00000000000..0b84cd6e36f --- /dev/null +++ b/code/addons/docs/src/manager.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { AddonPanel, type SyntaxHighlighterFormatTypes } from 'storybook/internal/components'; +import { ADDON_ID, PANEL_ID, PARAM_KEY, SNIPPET_RENDERED } from 'storybook/internal/docs-tools'; +import { addons, types, useAddonState, useChannel } from 'storybook/internal/manager-api'; + +import { Source } from '@storybook/blocks'; + +addons.register(ADDON_ID, (api) => { + addons.add(PANEL_ID, { + title: 'Code', + type: types.PANEL, + paramKey: PARAM_KEY, + /** + * This code panel can be disabled by the user by adding this parameter: + * + * @example + * + * ```ts + * parameters: { + * docs: { + * codePanel: false, + * }, + * }, + * ``` + */ + disabled: (parameters) => { + return ( + !!parameters && + typeof parameters[PARAM_KEY] === 'object' && + parameters[PARAM_KEY].codePanel === false + ); + }, + match: ({ viewMode }) => viewMode === 'story', + render: ({ active }) => { + const [codeSnippet, setSourceCode] = useAddonState<{ + source: string; + format: SyntaxHighlighterFormatTypes; + }>(ADDON_ID, { + source: '', + format: 'html', + }); + + useChannel({ + [SNIPPET_RENDERED]: ({ source, format }) => { + setSourceCode({ source, format }); + }, + }); + + return ( + + + + ); + }, + }); +}); diff --git a/code/addons/docs/src/plugins/mdx-plugin.ts b/code/addons/docs/src/plugins/mdx-plugin.ts index 40d8e88c3f3..36fe3b691e9 100644 --- a/code/addons/docs/src/plugins/mdx-plugin.ts +++ b/code/addons/docs/src/plugins/mdx-plugin.ts @@ -3,8 +3,6 @@ import { dirname, join } from 'node:path'; import type { Options } from 'storybook/internal/types'; import { createFilter } from '@rollup/pluginutils'; -import rehypeExternalLinks from 'rehype-external-links'; -import rehypeSlug from 'rehype-slug'; import type { Plugin } from 'vite'; import type { CompileOptions } from '../compiler'; @@ -24,6 +22,9 @@ export async function mdxPlugin(options: Options): Promise { const presetOptions = await presets.apply>('options', {}); const mdxPluginOptions = presetOptions?.mdxPluginOptions as CompileOptions; + const rehypeSlug = (await import('rehype-slug')).default; + const rehypeExternalLinks = (await import('rehype-external-links')).default; + return { name: 'storybook:mdx-plugin', enforce: 'pre', diff --git a/code/addons/docs/src/preset.ts b/code/addons/docs/src/preset.ts index e418b5e2e8a..9cff707e847 100644 --- a/code/addons/docs/src/preset.ts +++ b/code/addons/docs/src/preset.ts @@ -5,9 +5,6 @@ import type { DocsOptions, Options, PresetProperty } from 'storybook/internal/ty import type { CsfPluginOptions } from '@storybook/csf-plugin'; -import rehypeExternalLinks from 'rehype-external-links'; -import rehypeSlug from 'rehype-slug'; - import type { CompileOptions } from './compiler'; /** @@ -42,6 +39,9 @@ async function webpack( const { csfPluginOptions = {}, mdxPluginOptions = {} } = options; + const rehypeSlug = (await import('rehype-slug')).default; + const rehypeExternalLinks = (await import('rehype-external-links')).default; + const mdxLoaderOptions: CompileOptions = await options.presets.apply('mdxLoaderOptions', { ...mdxPluginOptions, mdxCompileOptions: { @@ -175,6 +175,9 @@ export const viteFinal = async (config: any, options: Options) => { const { plugins = [] } = config; const { mdxPlugin } = await import('./plugins/mdx-plugin'); + const rehypeSlug = (await import('rehype-slug')).default; + const rehypeExternalLinks = (await import('rehype-external-links')).default; + // Use the resolvedReact preset to alias react and react-dom to either the users version or the version shipped with addon-docs const { react, reactDom, mdx } = await getResolvedReact(options); diff --git a/code/addons/docs/template/stories/sourcePanel/index.stories.tsx b/code/addons/docs/template/stories/sourcePanel/index.stories.tsx new file mode 100644 index 00000000000..9958096cb81 --- /dev/null +++ b/code/addons/docs/template/stories/sourcePanel/index.stories.tsx @@ -0,0 +1,23 @@ +export default { + component: globalThis.Components.Button, + tags: ['autodocs'], + parameters: { + chromatic: { disable: true }, + docs: { + codePanel: false, + }, + }, +}; + +export const One = { args: { label: 'One' } }; + +export const Two = { args: { label: 'Two' } }; + +export const WithSource = { + args: { label: 'Three' }, + parameters: { + docs: { + codePanel: true, + }, + }, +}; diff --git a/code/addons/essentials/package.json b/code/addons/essentials/package.json index bb428b31887..0d8baa2a609 100644 --- a/code/addons/essentials/package.json +++ b/code/addons/essentials/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-essentials", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Curated addons to bring out the best of Storybook", "keywords": [ "addon", @@ -40,6 +40,7 @@ }, "./backgrounds/manager": "./dist/backgrounds/manager.js", "./controls/manager": "./dist/controls/manager.js", + "./docs/manager": "./dist/docs/manager.js", "./docs/preview": { "types": "./dist/docs/preview.d.ts", "import": "./dist/docs/preview.mjs", @@ -114,10 +115,14 @@ "./src/docs/preset.ts", "./src/docs/mdx-react-shim.ts" ], + "entries": [ + "./src/docs/manager.ts" + ], "managerEntries": [ "./src/actions/manager.ts", "./src/backgrounds/manager.ts", "./src/controls/manager.ts", + "./src/docs/manager.ts", "./src/measure/manager.ts", "./src/outline/manager.ts", "./src/toolbars/manager.ts", diff --git a/code/addons/essentials/src/docs/manager.ts b/code/addons/essentials/src/docs/manager.ts new file mode 100644 index 00000000000..6101f7d7926 --- /dev/null +++ b/code/addons/essentials/src/docs/manager.ts @@ -0,0 +1,2 @@ +// @ts-expect-error (no types needed for this) +export * from '@storybook/addon-docs/manager'; diff --git a/code/addons/essentials/src/index.ts b/code/addons/essentials/src/index.ts index 5809420bc1b..a72554227ba 100644 --- a/code/addons/essentials/src/index.ts +++ b/code/addons/essentials/src/index.ts @@ -88,9 +88,9 @@ export function addons(options: PresetOptions) { // NOTE: The order of these addons is important. return [ - 'docs', 'controls', 'actions', + 'docs', 'backgrounds', 'viewport', 'toolbars', diff --git a/code/addons/gfm/package.json b/code/addons/gfm/package.json index 95bd0aa7e64..cd39a40b45b 100644 --- a/code/addons/gfm/package.json +++ b/code/addons/gfm/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-mdx-gfm", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "GitHub Flavored Markdown in Storybook", "keywords": [ "addon", diff --git a/code/addons/highlight/package.json b/code/addons/highlight/package.json index 00c3280e990..c096a643d8c 100644 --- a/code/addons/highlight/package.json +++ b/code/addons/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-highlight", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Highlight DOM nodes within your stories", "keywords": [ "storybook-addons", diff --git a/code/addons/interactions/package.json b/code/addons/interactions/package.json index 3891ce1ce74..e11acb7b795 100644 --- a/code/addons/interactions/package.json +++ b/code/addons/interactions/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-interactions", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Automate, test and debug user interactions", "keywords": [ "storybook-addons", diff --git a/code/addons/jest/package.json b/code/addons/jest/package.json index 158008a17ba..49eda099837 100644 --- a/code/addons/jest/package.json +++ b/code/addons/jest/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-jest", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "React storybook addon that show component jest report", "keywords": [ "addon", diff --git a/code/addons/links/package.json b/code/addons/links/package.json index 9acb23a278c..553f715bce4 100644 --- a/code/addons/links/package.json +++ b/code/addons/links/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-links", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Link stories together to build demos and prototypes with your UI components", "keywords": [ "storybook-addons", diff --git a/code/addons/measure/package.json b/code/addons/measure/package.json index 12e16226234..9833652a6fa 100644 --- a/code/addons/measure/package.json +++ b/code/addons/measure/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-measure", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Inspect layouts by visualizing the box model", "keywords": [ "storybook-addons", diff --git a/code/addons/onboarding/package.json b/code/addons/onboarding/package.json index 6992d234f2e..43723a24d6e 100644 --- a/code/addons/onboarding/package.json +++ b/code/addons/onboarding/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-onboarding", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook Addon Onboarding - Introduces a new onboarding experience", "keywords": [ "storybook-addons", @@ -44,10 +44,8 @@ "check": "jiti ../../../scripts/prepare/check.ts", "prep": "jiti ../../../scripts/prepare/addon-bundle.ts" }, - "dependencies": { - "react-confetti": "^6.1.0" - }, "devDependencies": { + "@neoconfetti/react": "^1.0.0", "@radix-ui/react-dialog": "^1.0.5", "@storybook/icons": "^1.2.12", "@storybook/react": "workspace:*", diff --git a/code/addons/onboarding/src/Onboarding.tsx b/code/addons/onboarding/src/Onboarding.tsx index b861d918204..54e264f6725 100644 --- a/code/addons/onboarding/src/Onboarding.tsx +++ b/code/addons/onboarding/src/Onboarding.tsx @@ -268,17 +268,7 @@ export default function Onboarding({ api }: { api: API }) { return ( - {showConfetti && ( - { - confetti?.reset(); - setShowConfetti(false); - }} - /> - )} + {showConfetti && } {step === '1:Intro' ? ( setStep('2:Controls')} /> ) : ( diff --git a/code/addons/onboarding/src/components/Confetti/Confetti.stories.tsx b/code/addons/onboarding/src/components/Confetti/Confetti.stories.tsx index b55fdf783b3..3540aadc2a8 100644 --- a/code/addons/onboarding/src/components/Confetti/Confetti.stories.tsx +++ b/code/addons/onboarding/src/components/Confetti/Confetti.stories.tsx @@ -8,11 +8,19 @@ const meta: Meta = { component: Confetti, parameters: { chromatic: { disableSnapshot: true }, + layout: 'fullscreen', }, decorators: [ (StoryFn) => ( -
- +
+ Falling confetti! 🎉
), @@ -23,41 +31,4 @@ export default meta; type Story = StoryObj; -export const Default: Story = { - args: { - recycle: true, - numberOfPieces: 200, - top: undefined, - left: undefined, - width: undefined, - height: undefined, - friction: 0.99, - wind: 0, - gravity: 0.1, - initialVelocityX: 4, - initialVelocityY: 10, - tweenDuration: 5000, - }, -}; - -export const OneTimeConfetti: Story = { - args: { - ...Default.args, - numberOfPieces: 800, - recycle: false, - tweenDuration: 20000, - onConfettiComplete: (confetti) => { - confetti?.reset(); - }, - }, -}; - -export const Positioned: Story = { - args: { - ...Default.args, - top: 100, - left: 300, - width: 300, - height: 250, - }, -}; +export const Default: Story = {}; diff --git a/code/addons/onboarding/src/components/Confetti/Confetti.tsx b/code/addons/onboarding/src/components/Confetti/Confetti.tsx index b9b816d3e05..cebc4045490 100644 --- a/code/addons/onboarding/src/components/Confetti/Confetti.tsx +++ b/code/addons/onboarding/src/components/Confetti/Confetti.tsx @@ -1,131 +1,34 @@ -import React, { useEffect } from 'react'; -import { useState } from 'react'; -import { createPortal } from 'react-dom'; +import React, { type ComponentProps } from 'react'; import { styled } from 'storybook/internal/theming'; -import ReactConfetti from 'react-confetti'; +import { Confetti as ReactConfetti } from '@neoconfetti/react'; -interface ConfettiProps extends Omit, 'drawShape'> { - top?: number; - left?: number; - width?: number; - height?: number; - numberOfPieces?: number; - recycle?: boolean; - colors?: string[]; -} +const Wrapper = styled.div({ + zIndex: 9999, + position: 'fixed', + top: 0, + left: '50%', + width: '50%', + height: '100%', +}); -const Wrapper = styled.div<{ - width: number; - height: number; - top: number; - left: number; -}>(({ width, height, left, top }) => ({ - width: `${width}px`, - height: `${height}px`, - left: `${left}px`, - top: `${top}px`, - position: 'relative', - overflow: 'hidden', -})); - -export function Confetti({ - top = 0, - left = 0, - width = window.innerWidth, - height = window.innerHeight, +export const Confetti = React.memo(function Confetti({ + timeToFade = 5000, colors = ['#CA90FF', '#FC521F', '#66BF3C', '#FF4785', '#FFAE00', '#1EA7FD'], ...confettiProps -}: ConfettiProps): React.ReactPortal { - const [confettiContainer] = useState(() => { - const container = document.createElement('div'); - container.setAttribute('id', 'confetti-container'); - container.setAttribute( - 'style', - 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;' - ); - - return container; - }); - - useEffect(() => { - document.body.appendChild(confettiContainer); - - return () => { - document.body.removeChild(confettiContainer); - }; - }, []); - - return createPortal( - - - , - confettiContainer +}: ComponentProps & { timeToFade?: number }) { + return ( + + + ); -} - -enum ParticleShape { - Circle = 1, - Square = 2, - TShape = 3, - LShape = 4, - Triangle = 5, - QuarterCircle = 6, -} - -function getRandomInt(min: number, max: number) { - return Math.floor(Math.random() * (max - min)) + min; -} - -function draw(this: any, context: CanvasRenderingContext2D) { - this.shape = this.shape || getRandomInt(1, 6); - - switch (this.shape) { - case ParticleShape.Square: { - const cornerRadius = 2; - const width = this.w / 2; - const height = this.h / 2; - - context.moveTo(-width + cornerRadius, -height); - context.lineTo(width - cornerRadius, -height); - context.arcTo(width, -height, width, -height + cornerRadius, cornerRadius); - context.lineTo(width, height - cornerRadius); - context.arcTo(width, height, width - cornerRadius, height, cornerRadius); - context.lineTo(-width + cornerRadius, height); - context.arcTo(-width, height, -width, height - cornerRadius, cornerRadius); - context.lineTo(-width, -height + cornerRadius); - context.arcTo(-width, -height, -width + cornerRadius, -height, cornerRadius); - - break; - } - case ParticleShape.TShape: { - context.rect(-4, -4, 8, 16); - context.rect(-12, -4, 24, 8); - break; - } - case ParticleShape.LShape: { - context.rect(-4, -4, 8, 16); - context.rect(-4, -4, 24, 8); - break; - } - case ParticleShape.Circle: { - context.arc(0, 0, this.radius, 0, 2 * Math.PI); - break; - } - case ParticleShape.Triangle: { - context.moveTo(16, 4); - context.lineTo(4, 24); - context.lineTo(24, 24); - break; - } - case ParticleShape.QuarterCircle: { - context.arc(4, -4, 4, -Math.PI / 2, 0); - context.lineTo(4, 0); - break; - } - } - - context.closePath(); - context.fill(); -} +}); diff --git a/code/addons/outline/package.json b/code/addons/outline/package.json index b28d642cffa..949927f567e 100644 --- a/code/addons/outline/package.json +++ b/code/addons/outline/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-outline", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Outline all elements with CSS to help with layout placement and alignment", "keywords": [ "storybook-addons", diff --git a/code/addons/storysource/package.json b/code/addons/storysource/package.json index f5e0067ff5f..80bf2c7bba1 100644 --- a/code/addons/storysource/package.json +++ b/code/addons/storysource/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-storysource", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "View a story’s source code to see how it works and paste into your app", "keywords": [ "addon", diff --git a/code/addons/test/package.json b/code/addons/test/package.json index 35a4077926e..22add74762d 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/experimental-addon-test", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Integrate Vitest with Storybook", "keywords": [ "storybook-addons", diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx index 52c3b98061f..229a396b538 100644 --- a/code/addons/test/src/components/ContextMenuItem.tsx +++ b/code/addons/test/src/components/ContextMenuItem.tsx @@ -67,7 +67,7 @@ export const ContextMenuItem: FC<{ padding="small" disabled={state.crashed || isDisabled} > - + } /> diff --git a/code/addons/test/src/components/Description.tsx b/code/addons/test/src/components/Description.tsx index a94b775b5c9..58a80dbfdcc 100644 --- a/code/addons/test/src/components/Description.tsx +++ b/code/addons/test/src/components/Description.tsx @@ -12,7 +12,7 @@ export const Wrapper = styled.div(({ theme }) => ({ whiteSpace: 'nowrap', textOverflow: 'ellipsis', fontSize: theme.typography.size.s1, - color: theme.barTextColor, + color: theme.textMutedColor, })); const PositiveText = styled.span(({ theme }) => ({ @@ -60,10 +60,10 @@ export function Description({ state, ...props }: DescriptionProps) { ); } else if (state.progress?.finishedAt) { description = ( - + <> + Ran {state.progress.numTotalTests} {state.progress.numTotalTests === 1 ? 'test' : 'tests'}{' '} + + ); } else if (state.watching) { description = 'Watching for file changes'; diff --git a/code/addons/test/src/components/Interaction.tsx b/code/addons/test/src/components/Interaction.tsx index 4ceef384d02..75797c65c5a 100644 --- a/code/addons/test/src/components/Interaction.tsx +++ b/code/addons/test/src/components/Interaction.tsx @@ -23,7 +23,7 @@ const MethodCallWrapper = styled.div(() => ({ const RowContainer = styled('div', { shouldForwardProp: (prop) => !['call', 'pausedAt'].includes(prop.toString()), -})<{ call: Call; pausedAt: Call['id'] }>( +})<{ call: Call; pausedAt: Call['id'] | undefined }>( ({ theme, call }) => ({ position: 'relative', display: 'flex', @@ -117,6 +117,9 @@ const RowMessage = styled('div')(({ theme }) => ({ export const Exception = ({ exception }: { exception: Call['exception'] }) => { const filter = useAnsiToHtmlFilter(); + if (!exception) { + return null; + } if (isJestError(exception)) { return ; } @@ -187,7 +190,7 @@ export const Interaction = ({ - {childCallIds?.length > 0 && ( + {(childCallIds?.length ?? 0) > 0 && ( } diff --git a/code/addons/test/src/components/InteractionsPanel.stories.tsx b/code/addons/test/src/components/InteractionsPanel.stories.tsx index 24eefed2028..f8cabbe24d5 100644 --- a/code/addons/test/src/components/InteractionsPanel.stories.tsx +++ b/code/addons/test/src/components/InteractionsPanel.stories.tsx @@ -58,7 +58,6 @@ const meta = { endRef: null, // prop for the AddonPanel used as wrapper of Panel active: true, - storyId: 'story-id', }, } as Meta; diff --git a/code/addons/test/src/components/InteractionsPanel.tsx b/code/addons/test/src/components/InteractionsPanel.tsx index 164e28b782f..896350b926c 100644 --- a/code/addons/test/src/components/InteractionsPanel.tsx +++ b/code/addons/test/src/components/InteractionsPanel.tsx @@ -44,8 +44,6 @@ interface InteractionsPanelProps { onScrollToEnd?: () => void; hasResultMismatch?: boolean; browserTestStatus?: CallStates; - storyId: StoryId; - testRunId: string; } const Container = styled.div(({ theme }) => ({ @@ -105,20 +103,12 @@ export const InteractionsPanel: React.FC = React.memo( endRef, hasResultMismatch, browserTestStatus, - storyId, - testRunId, }) { const filter = useAnsiToHtmlFilter(); return ( - {hasResultMismatch && ( - - )} + {hasResultMismatch && } {(interactions.length > 0 || hasException) && ( { if (line.startsWith('expect(')) { const received = getParams(line, 7); - const remainderIndex = received && 7 + received.length; + const remainderIndex = received ? 7 + received.length : 0; const matcher = received && line.slice(remainderIndex).match(/\.(to|last|nth)[A-Z]\w+\(/); if (matcher) { - const expectedIndex = remainderIndex + matcher.index + matcher[0].length; + const expectedIndex = remainderIndex + (matcher.index ?? 0) + matcher[0].length; const expected = getParams(line, expectedIndex); if (expected) { return [ diff --git a/code/addons/test/src/components/MethodCall.tsx b/code/addons/test/src/components/MethodCall.tsx index 59b907d13da..34d1e6bb6f5 100644 --- a/code/addons/test/src/components/MethodCall.tsx +++ b/code/addons/test/src/components/MethodCall.tsx @@ -139,7 +139,7 @@ export const Node = ({ case Object.prototype.hasOwnProperty.call(value, '__class__'): return ; case Object.prototype.hasOwnProperty.call(value, '__callId__'): - return ; + return ; /* eslint-enable no-underscore-dangle */ case Object.prototype.toString.call(value) === '[object Object]': @@ -418,7 +418,7 @@ export const MethodCall = ({ callsById, }: { call?: Call; - callsById: Map; + callsById?: Map; }) => { // Call might be undefined during initial render, can be safely ignored. if (!call) { @@ -434,7 +434,7 @@ export const MethodCall = ({ const callId = (elem as CallRef).__callId__; return [ callId ? ( - + ) : ( {elem as any} ), diff --git a/code/addons/test/src/components/Panel.tsx b/code/addons/test/src/components/Panel.tsx index 584872bad8c..d6ea7484315 100644 --- a/code/addons/test/src/components/Panel.tsx +++ b/code/addons/test/src/components/Panel.tsx @@ -19,17 +19,9 @@ import { global } from '@storybook/global'; import { type Call, CallStates, EVENTS, type LogItem } from '@storybook/instrumenter'; import type { API_StatusValue } from '@storybook/types'; -import { ADDON_ID, TEST_PROVIDER_ID } from '../constants'; +import { ADDON_ID, STORYBOOK_ADDON_TEST_CHANNEL, TEST_PROVIDER_ID } from '../constants'; import { InteractionsPanel } from './InteractionsPanel'; -interface Interaction extends Call { - status: Call['status']; - childCallIds: Call['id'][]; - isHidden: boolean; - isCollapsed: boolean; - toggleCollapsed: () => void; -} - const INITIAL_CONTROL_STATES = { start: false, back: false, @@ -60,7 +52,7 @@ export const getInteractions = ({ const childCallMap = new Map(); return log - .map(({ callId, ancestors, status }) => { + .map(({ callId, ancestors, status }) => { let isHidden = false; ancestors.forEach((ancestor) => { if (collapsed.has(ancestor)) { @@ -68,11 +60,12 @@ export const getInteractions = ({ } childCallMap.set(ancestor, (childCallMap.get(ancestor) || []).concat(callId)); }); - return { ...calls.get(callId), status, isHidden }; + return { ...calls.get(callId)!, status, isHidden }; }) - .map((call) => { + .map((call) => { const status = call.status === CallStates.ERROR && + call.ancestors && callsById.get(call.ancestors.slice(-1)[0])?.status === CallStates.ACTIVE ? CallStates.ACTIVE : call.status; @@ -114,6 +107,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId // local state const [scrollTarget, setScrollTarget] = useState(undefined); const [collapsed, setCollapsed] = useState>(new Set()); + const [hasResultMismatch, setResultMismatch] = useState(false); const { controlStates = INITIAL_CONTROL_STATES, @@ -130,7 +124,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId const calls = useRef>>(new Map()); const setCall = ({ status, ...call }: Call) => calls.current.set(call.id, call); - const endRef = useRef(); + const endRef = useRef(); useEffect(() => { let observer: IntersectionObserver; if (global.IntersectionObserver) { @@ -150,6 +144,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId { [EVENTS.CALL]: setCall, [EVENTS.SYNC]: (payload) => { + // @ts-expect-error TODO set((s) => { const list = getInteractions({ log: payload.logItems, @@ -213,6 +208,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId ); useEffect(() => { + // @ts-expect-error TODO set((s) => { const list = getInteractions({ log: log.current, @@ -226,7 +222,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId interactionsCount: list.filter(({ method }) => method !== 'step').length, }; }); - }, [collapsed]); + }, [set, collapsed]); const controls = useMemo( () => ({ @@ -239,7 +235,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId emit(FORCE_REMOUNT, { storyId }); }, }), - [storyId] + [emit, storyId] ); const storyFilePath = useParameter('fileName', ''); @@ -249,25 +245,52 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId const hasException = !!caughtException || !!unhandledErrors || + // @ts-expect-error TODO interactions.some((v) => v.status === CallStates.ERROR); const storyStatus = storyStatuses[storyId]?.[TEST_PROVIDER_ID]; + const storyTestStatus = storyStatus?.status; - const browserTestStatus = React.useMemo(() => { + const browserTestStatus = useMemo(() => { if (!isPlaying && (interactions.length > 0 || hasException)) { return hasException ? CallStates.ERROR : CallStates.DONE; } - return isPlaying ? CallStates.ACTIVE : null; + return isPlaying ? CallStates.ACTIVE : undefined; }, [isPlaying, interactions, hasException]); - const hasResultMismatch = React.useMemo(() => { - return ( - browserTestStatus !== null && - browserTestStatus !== CallStates.ACTIVE && - storyStatus?.status !== undefined && - statusMap[browserTestStatus] !== storyStatus.status - ); - }, [browserTestStatus, storyStatus]); + const { testRunId } = storyStatus?.data || {}; + + useEffect(() => { + const isMismatch = + browserTestStatus && + storyTestStatus && + storyTestStatus !== 'pending' && + storyTestStatus !== statusMap[browserTestStatus]; + + if (isMismatch) { + const timeout = setTimeout( + () => + setResultMismatch((currentValue) => { + if (!currentValue) { + emit(STORYBOOK_ADDON_TEST_CHANNEL, { + type: 'test-discrepancy', + payload: { + browserStatus: browserTestStatus === CallStates.DONE ? 'PASS' : 'FAIL', + cliStatus: browserTestStatus === CallStates.DONE ? 'FAIL' : 'PASS', + storyId, + testRunId, + }, + }); + } + return true; + }), + 2000 + ); + return () => clearTimeout(timeout); + } else { + setResultMismatch(false); + } + }, [emit, browserTestStatus, storyTestStatus, storyId, testRunId]); if (isErrored) { return ; @@ -288,10 +311,9 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId unhandledErrors={unhandledErrors} isPlaying={isPlaying} pausedAt={pausedAt} + // @ts-expect-error TODO endRef={endRef} onScrollToEnd={scrollTarget && scrollToTarget} - storyId={storyId} - testRunId={storyStatus?.data?.testRunId} /> ); diff --git a/code/addons/test/src/components/RelativeTime.stories.tsx b/code/addons/test/src/components/RelativeTime.stories.tsx new file mode 100644 index 00000000000..4d3c6af0f6d --- /dev/null +++ b/code/addons/test/src/components/RelativeTime.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { RelativeTime } from './RelativeTime'; + +const meta = { + component: RelativeTime, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const JustNow: Story = { + args: { + timestamp: Date.now() - 1000 * 10, + }, +}; + +export const AMinuteAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60, + }, +}; + +export const MinutesAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 2, + }, +}; + +export const HoursAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 60 * 3, + }, +}; + +export const Yesterday: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 60 * 24, + }, +}; + +export const DaysAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 60 * 24 * 3, + }, +}; diff --git a/code/addons/test/src/components/RelativeTime.tsx b/code/addons/test/src/components/RelativeTime.tsx index fa9e7cf6d54..4d4cf3c4869 100644 --- a/code/addons/test/src/components/RelativeTime.tsx +++ b/code/addons/test/src/components/RelativeTime.tsx @@ -1,41 +1,35 @@ import { useEffect, useState } from 'react'; -export function getRelativeTimeString(date: Date): string { - const delta = Math.round((date.getTime() - Date.now()) / 1000); - const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]; - const units: Intl.RelativeTimeFormatUnit[] = [ - 'second', - 'minute', - 'hour', - 'day', - 'week', - 'month', - 'year', - ]; - - const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(delta)); - const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1; - const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); - return rtf.format(Math.floor(delta / divisor), units[unitIndex]); -} - -export const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: number }) => { - const [relativeTimeString, setRelativeTimeString] = useState(null); +export const RelativeTime = ({ timestamp }: { timestamp?: number }) => { + const [timeAgo, setTimeAgo] = useState(null); useEffect(() => { if (timestamp) { - setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now')); - - const interval = setInterval(() => { - setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now')); - }, 10000); - + setTimeAgo(Date.now() - timestamp); + const interval = setInterval(() => setTimeAgo(Date.now() - timestamp), 10000); return () => clearInterval(interval); } }, [timestamp]); - return ( - relativeTimeString && - `Ran ${testCount} ${testCount === 1 ? 'test' : 'tests'} ${relativeTimeString}` - ); + if (timeAgo === null) { + return null; + } + + const seconds = Math.round(timeAgo / 1000); + if (seconds < 60) { + return `just now`; + } + + const minutes = Math.floor(seconds / 60); + if (minutes < 60) { + return minutes === 1 ? `a minute ago` : `${minutes} minutes ago`; + } + + const hours = Math.floor(minutes / 60); + if (hours < 24) { + return hours === 1 ? `an hour ago` : `${hours} hours ago`; + } + + const days = Math.floor(hours / 24); + return days === 1 ? `yesterday` : `${days} days ago`; }; diff --git a/code/addons/test/src/components/StatusBadge.tsx b/code/addons/test/src/components/StatusBadge.tsx index d730b8ef985..a906b501a93 100644 --- a/code/addons/test/src/components/StatusBadge.tsx +++ b/code/addons/test/src/components/StatusBadge.tsx @@ -14,7 +14,7 @@ const StyledBadge = styled.div(({ theme, status }) => { [CallStates.ERROR]: theme.color.negative, [CallStates.ACTIVE]: theme.color.warning, [CallStates.WAITING]: theme.color.warning, - }[status]; + }[status!]; return { padding: '4px 6px 4px 8px;', borderRadius: '4px', @@ -36,7 +36,7 @@ export const StatusBadge: React.FC = ({ status }) => { [CallStates.ERROR]: 'Fail', [CallStates.ACTIVE]: 'Runs', [CallStates.WAITING]: 'Runs', - }[status]; + }[status!]; return ( {badgeText} diff --git a/code/addons/test/src/components/Subnav.tsx b/code/addons/test/src/components/Subnav.tsx index bf9d8436cee..88fcbd5c452 100644 --- a/code/addons/test/src/components/Subnav.tsx +++ b/code/addons/test/src/components/Subnav.tsx @@ -109,7 +109,7 @@ const RerunButton = styled(StyledIconButton)< >(({ theme, animating, disabled }) => ({ opacity: disabled ? 0.5 : 1, svg: { - animation: animating && `${theme.animation.rotate360} 200ms ease-out`, + animation: animating ? `${theme.animation.rotate360} 200ms ease-out` : undefined, }, })); diff --git a/code/addons/test/src/components/TestDiscrepancyMessage.stories.tsx b/code/addons/test/src/components/TestDiscrepancyMessage.stories.tsx index 840c08cdf3d..81553cdc5a3 100644 --- a/code/addons/test/src/components/TestDiscrepancyMessage.stories.tsx +++ b/code/addons/test/src/components/TestDiscrepancyMessage.stories.tsx @@ -23,9 +23,6 @@ export default { parameters: { layout: 'fullscreen', }, - args: { - storyId: 'story-id', - }, decorators: [ (storyFn) => ( {storyFn()} diff --git a/code/addons/test/src/components/TestDiscrepancyMessage.tsx b/code/addons/test/src/components/TestDiscrepancyMessage.tsx index bbdf74e36a6..2ff2e97c9f7 100644 --- a/code/addons/test/src/components/TestDiscrepancyMessage.tsx +++ b/code/addons/test/src/components/TestDiscrepancyMessage.tsx @@ -1,13 +1,12 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Link } from 'storybook/internal/components'; import { useStorybookApi } from 'storybook/internal/manager-api'; import { styled } from 'storybook/internal/theming'; -import type { StoryId } from 'storybook/internal/types'; import { CallStates } from '@storybook/instrumenter'; -import { DOCUMENTATION_DISCREPANCY_LINK, STORYBOOK_ADDON_TEST_CHANNEL } from '../constants'; +import { DOCUMENTATION_DISCREPANCY_LINK } from '../constants'; const Wrapper = styled.div(({ theme: { color, typography, background } }) => ({ textAlign: 'start', @@ -32,40 +31,24 @@ const Wrapper = styled.div(({ theme: { color, typography, background } }) => ({ })); interface TestDiscrepancyMessageProps { - browserTestStatus: CallStates; - storyId: StoryId; - testRunId: string; + browserTestStatus?: CallStates; } -export const TestDiscrepancyMessage = ({ - browserTestStatus, - storyId, - testRunId, -}: TestDiscrepancyMessageProps) => { + +export const TestDiscrepancyMessage = ({ browserTestStatus }: TestDiscrepancyMessageProps) => { const api = useStorybookApi(); const docsUrl = api.getDocsUrl({ subpath: DOCUMENTATION_DISCREPANCY_LINK, versioned: true, renderer: true, }); - const message = `This component test passed in ${browserTestStatus === CallStates.DONE ? 'this browser' : 'CLI'}, but the tests failed in ${browserTestStatus === CallStates.ERROR ? 'this browser' : 'CLI'}.`; - - useEffect( - () => - api.emit(STORYBOOK_ADDON_TEST_CHANNEL, { - type: 'test-discrepancy', - payload: { - browserStatus: browserTestStatus === CallStates.DONE ? 'PASS' : 'FAIL', - cliStatus: browserTestStatus === CallStates.DONE ? 'FAIL' : 'PASS', - storyId, - testRunId, - }, - }), - [api, browserTestStatus, storyId, testRunId] - ); + const [passed, failed] = + browserTestStatus === CallStates.ERROR + ? ['the CLI', 'this browser'] + : ['this browser', 'the CLI']; return ( - {message}{' '} + This component test passed in {passed}, but the tests failed in {failed}.{' '} Learn what could cause this diff --git a/code/addons/test/src/components/TestProviderRender.stories.tsx b/code/addons/test/src/components/TestProviderRender.stories.tsx index dead913949c..5111c1507a4 100644 --- a/code/addons/test/src/components/TestProviderRender.stories.tsx +++ b/code/addons/test/src/components/TestProviderRender.stories.tsx @@ -43,7 +43,7 @@ const baseState: TestProviderState = { cancellable: true, cancelling: false, crashed: false, - error: null, + error: undefined, failed: false, running: false, watching: false, @@ -52,6 +52,10 @@ const baseState: TestProviderState = { coverage: false, }, details: { + config: { + a11y: false, + coverage: false, + }, testResults: [ { endTime: 0, @@ -141,6 +145,10 @@ export const WithCoverageNegative: Story = { ...config, ...baseState, details: { + config: { + a11y: false, + coverage: true, + }, testResults: [], coverageSummary: { percentage: 20, @@ -162,6 +170,10 @@ export const WithCoverageWarning: Story = { ...baseState, details: { testResults: [], + config: { + a11y: false, + coverage: true, + }, coverageSummary: { percentage: 50, status: 'warning', @@ -182,6 +194,10 @@ export const WithCoveragePositive: Story = { ...baseState, details: { testResults: [], + config: { + a11y: false, + coverage: true, + }, coverageSummary: { percentage: 80, status: 'positive', @@ -206,6 +222,10 @@ export const Editing: Story = { }, details: { testResults: [], + config: { + a11y: false, + coverage: false, + }, }, }, }, @@ -229,6 +249,10 @@ export const EditingAndWatching: Story = { }, details: { testResults: [], + config: { + a11y: true, + coverage: true, // should be automatically disabled in the UI + }, }, }, }, diff --git a/code/addons/test/src/components/TestProviderRender.tsx b/code/addons/test/src/components/TestProviderRender.tsx index 017ecc2cf27..4b1f4c35bc4 100644 --- a/code/addons/test/src/components/TestProviderRender.tsx +++ b/code/addons/test/src/components/TestProviderRender.tsx @@ -76,6 +76,16 @@ const StopIcon = styled(StopAltIcon)({ width: 10, }); +const ItemTitle = styled.span<{ enabled?: boolean }>( + ({ enabled, theme }) => + !enabled && { + color: theme.textMutedColor, + '&:after': { + content: '" (disabled)"', + }, + } +); + const statusOrder: TestStatus[] = ['failed', 'warning', 'pending', 'passed', 'skipped']; const statusMap: Record['status']> = { failed: 'negative', @@ -104,6 +114,8 @@ export const TestProviderRender: FC< state.config || { a11y: false, coverage: false } ); + const isStoryEntry = entryId?.includes('--') ?? false; + const a11yResults = useMemo(() => { if (!isA11yAddon) { return []; @@ -118,16 +130,22 @@ export const TestProviderRender: FC< }, [isA11yAddon, state.details?.testResults, entryId]); const a11yStatus = useMemo<'positive' | 'warning' | 'negative' | 'unknown'>(() => { + if (state.running) { + return 'unknown'; + } + if (!isA11yAddon || config.a11y === false) { return 'unknown'; } - if (!a11yResults) { + const definedA11yResults = a11yResults?.filter(Boolean) ?? []; + + if (!definedA11yResults || definedA11yResults.length === 0) { return 'unknown'; } - const failed = a11yResults.some((result) => result?.status === 'failed'); - const warning = a11yResults.some((result) => result?.status === 'warning'); + const failed = definedA11yResults.some((result) => result?.status === 'failed'); + const warning = definedA11yResults.some((result) => result?.status === 'warning'); if (failed) { return 'negative'; @@ -136,13 +154,26 @@ export const TestProviderRender: FC< } return 'positive'; - }, [a11yResults, isA11yAddon, config.a11y]); + }, [state.running, isA11yAddon, config.a11y, a11yResults]); - const a11yNotPassedAmount = a11yResults?.filter( - (result) => result?.status === 'failed' || result?.status === 'warning' - ).length; + const a11yNotPassedAmount = state.config?.a11y + ? a11yResults?.filter((result) => result?.status === 'failed' || result?.status === 'warning') + .length + : undefined; + + const a11ySkippedAmount = + state.running || !state?.details.config?.a11y || !state.config?.a11y + ? null + : a11yResults?.filter((result) => !result).length; + + const a11ySkippedLabel = a11ySkippedAmount + ? a11ySkippedAmount === 1 && isStoryEntry + ? '(skipped)' + : `(${a11ySkippedAmount} skipped)` + : ''; + + const storyId = isStoryEntry ? entryId : undefined; - const storyId = entryId?.includes('--') ? entryId : undefined; const results = (state.details?.testResults || []) .flatMap((test) => { if (!entryId) { @@ -154,7 +185,11 @@ export const TestProviderRender: FC< }) .sort((a, b) => statusOrder.indexOf(a.status) - statusOrder.indexOf(b.status)); - const status = (state.failed ? 'failed' : results[0]?.status) || 'unknown'; + const status = state.running + ? 'unknown' + : state.failed + ? 'failed' + : (results[0]?.status ?? 'unknown'); const openPanel = (id: string, panelId: string) => { api.selectStory(id); @@ -233,7 +268,7 @@ export const TestProviderRender: FC< /> Coverage} icon={} right={ Accessibility} icon={} right={ r.status === 'failed' || r.status === 'warning' ); - openPanel(firstNotPassed.storyId, PANEL_ID); + if (firstNotPassed) { + openPanel(firstNotPassed.storyId, PANEL_ID); + } } - : null + : undefined } icon={ state.crashed ? ( @@ -285,10 +322,11 @@ export const TestProviderRender: FC< /> {coverageSummary ? ( Coverage} href={'/coverage/index.html'} // @ts-expect-error ListItem doesn't include all anchor attributes in types, but it is an achor element target="_blank" + aria-label="Open coverage report" icon={ } - right={`${coverageSummary.percentage}%`} + right={ + coverageSummary.percentage ? ( + + {coverageSummary.percentage} % + + ) : null + } /> ) : ( Coverage} icon={} /> )} {isA11yAddon && ( Accessibility {a11ySkippedLabel}} onClick={ (a11yStatus === 'negative' || a11yStatus === 'warning') && a11yResults.length ? () => { @@ -317,12 +361,14 @@ export const TestProviderRender: FC< (report) => report.status === 'failed' || report.status === 'warning' ) ); - openPanel(firstNotPassed.storyId, A11y_ADDON_PANEL_ID); + if (firstNotPassed) { + openPanel(firstNotPassed.storyId, A11y_ADDON_PANEL_ID); + } } - : null + : undefined } icon={} - right={a11yNotPassedAmount || null} + right={isStoryEntry ? null : a11yNotPassedAmount || null} /> )} diff --git a/code/addons/test/src/constants.ts b/code/addons/test/src/constants.ts index 0453930e375..58463326dcd 100644 --- a/code/addons/test/src/constants.ts +++ b/code/addons/test/src/constants.ts @@ -12,6 +12,13 @@ export const DOCUMENTATION_FATAL_ERROR_LINK = `${DOCUMENTATION_LINK}#what-happen export const COVERAGE_DIRECTORY = 'coverage'; +export const SUPPORTED_FRAMEWORKS = [ + '@storybook/nextjs', + '@storybook/experimental-nextjs-vite', + '@storybook/sveltekit', +]; + +export const SUPPORTED_RENDERERS = ['@storybook/react', '@storybook/svelte', '@storybook/vue3']; export interface Config { coverage: boolean; a11y: boolean; @@ -19,6 +26,7 @@ export interface Config { export type Details = { testResults: TestResult[]; + config: Config; coverageSummary?: { status: 'positive' | 'warning' | 'negative' | 'unknown'; percentage: number; diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 4db0c5defcb..7264e5fe49c 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -38,6 +38,7 @@ addons.register(ADDON_ID, (api) => { runnable: true, watchable: true, name: 'Component tests', + // @ts-expect-error: TODO: Fix types render: (state) => { const [isModalOpen, setModalOpen] = useState(false); return ( @@ -55,6 +56,7 @@ addons.register(ADDON_ID, (api) => { ); }, + // @ts-expect-error: TODO: Fix types sidebarContextMenu: ({ context, state }) => { if (context.type === 'docs') { return null; @@ -72,67 +74,81 @@ addons.register(ADDON_ID, (api) => { ); }, + // @ts-expect-error: TODO: Fix types stateUpdater: (state, update) => { - if (!update.details?.testResults) { - return; + const updated = { + ...state, + ...update, + details: { ...state.details, ...update.details }, + }; + + if ((!state.running && update.running) || (!state.watching && update.watching)) { + // Clear coverage data when starting test run or enabling watch mode + delete updated.details.coverageSummary; } - (async () => { - await api.experimental_updateStatus( - TEST_PROVIDER_ID, - Object.fromEntries( - update.details.testResults.flatMap((testResult) => - testResult.results - .filter(({ storyId }) => storyId) - .map(({ storyId, status, testRunId, ...rest }) => [ - storyId, - { - title: 'Component tests', - status: statusMap[status], - description: - 'failureMessages' in rest && rest.failureMessages - ? rest.failureMessages.join('\n') - : '', - data: { testRunId }, - onClick: openTestsPanel, - sidebarContextMenu: false, - } satisfies API_StatusObject, - ]) - ) - ) - ); - - await api.experimental_updateStatus( - 'storybook/addon-a11y/test-provider', - Object.fromEntries( - update.details.testResults.flatMap((testResult) => - testResult.results - .filter(({ storyId }) => storyId) - .map(({ storyId, testRunId, reports }) => { - const a11yReport = reports.find((r: any) => r.type === 'a11y'); - return [ + if (update.details?.testResults) { + (async () => { + await api.experimental_updateStatus( + TEST_PROVIDER_ID, + Object.fromEntries( + // @ts-expect-error: TODO: Fix types + update.details.testResults.flatMap((testResult) => + testResult.results + .filter(({ storyId }) => storyId) + .map(({ storyId, status, testRunId, ...rest }) => [ storyId, - a11yReport - ? ({ - title: 'Accessibility tests', - description: '', - status: statusMap[a11yReport.status], - data: { testRunId }, - onClick: () => { - api.setSelectedPanel('storybook/a11y/panel'); - api.togglePanel(true); - }, - sidebarContextMenu: false, - } satisfies API_StatusObject) - : null, - ]; - }) + { + title: 'Component tests', + status: statusMap[status], + description: + 'failureMessages' in rest && rest.failureMessages + ? rest.failureMessages.join('\n') + : '', + data: { testRunId }, + onClick: openTestsPanel, + sidebarContextMenu: false, + } satisfies API_StatusObject, + ]) + ) ) - ) - ); - })(); + ); + + await api.experimental_updateStatus( + 'storybook/addon-a11y/test-provider', + Object.fromEntries( + // @ts-expect-error: TODO: Fix types + update.details.testResults.flatMap((testResult) => + testResult.results + .filter(({ storyId }) => storyId) + .map(({ storyId, testRunId, reports }) => { + const a11yReport = reports.find((r: any) => r.type === 'a11y'); + return [ + storyId, + a11yReport + ? ({ + title: 'Accessibility tests', + description: '', + status: statusMap[a11yReport.status], + data: { testRunId }, + onClick: () => { + api.setSelectedPanel('storybook/a11y/panel'); + api.togglePanel(true); + }, + sidebarContextMenu: false, + } satisfies API_StatusObject) + : null, + ]; + }) + ) + ) + ); + })(); + } + + return updated; }, - } as Addon_TestProviderType); + } satisfies Omit, 'id'>); } const filter = ({ state }: Combo) => { @@ -147,7 +163,7 @@ addons.register(ADDON_ID, (api) => { match: ({ viewMode }) => viewMode === 'story', render: ({ active }) => { return ( - + {({ storyId }) => } ); diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts index 3f0329807e9..f1fe3ddc94f 100644 --- a/code/addons/test/src/node/boot-test-runner.ts +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -24,7 +24,7 @@ const MAX_START_TIME = 30000; const vitestModulePath = join(__dirname, 'node', 'vitest.mjs'); // Events that were triggered before Vitest was ready are queued up and resent once it's ready -const eventQueue: { type: string; args: any[] }[] = []; +const eventQueue: { type: string; args?: any[] }[] = []; let child: null | ChildProcess; let ready = false; @@ -87,7 +87,7 @@ const bootTestRunner = async (channel: Channel) => { if (result.type === 'ready') { // Resend events that triggered (during) the boot sequence, now that Vitest is ready while (eventQueue.length) { - const { type, args } = eventQueue.shift(); + const { type, args } = eventQueue.shift()!; child?.send({ type, args, from: 'server' }); } diff --git a/code/addons/test/src/node/coverage-reporter.ts b/code/addons/test/src/node/coverage-reporter.ts index 452643cd9d6..cc1dd548000 100644 --- a/code/addons/test/src/node/coverage-reporter.ts +++ b/code/addons/test/src/node/coverage-reporter.ts @@ -8,7 +8,7 @@ import type { TestManager } from './test-manager'; export type StorybookCoverageReporterOptions = { testManager: TestManager; - coverageOptions: ResolvedCoverageOptions<'v8'>; + coverageOptions: ResolvedCoverageOptions<'v8'> | undefined; }; export default class StorybookCoverageReporter extends ReportBase implements Partial { @@ -32,7 +32,7 @@ export default class StorybookCoverageReporter extends ReportBase implements Par // Fallback to Vitest's default watermarks https://vitest.dev/config/#coverage-watermarks const [lowWatermark = 50, highWatermark = 80] = - this.#coverageOptions.watermarks?.statements ?? []; + this.#coverageOptions?.watermarks?.statements ?? []; const coverageDetails: Details['coverageSummary'] = { percentage, diff --git a/code/addons/test/src/node/reporter.ts b/code/addons/test/src/node/reporter.ts index 43191cbc2fd..1f71e1bf967 100644 --- a/code/addons/test/src/node/reporter.ts +++ b/code/addons/test/src/node/reporter.ts @@ -173,6 +173,7 @@ export class StorybookReporter implements Reporter { } as TestingModuleProgressReportProgress, details: { testResults, + config: this.testManager.config, }, }; } @@ -219,7 +220,7 @@ export class StorybookReporter implements Reporter { (t) => t.status === 'failed' && t.results.length === 0 ); - const reducedTestSuiteFailures = new Set(); + const reducedTestSuiteFailures = new Set(); testSuiteFailures.forEach((t) => { reducedTestSuiteFailures.add(t.message); @@ -239,7 +240,7 @@ export class StorybookReporter implements Reporter { message: Array.from(reducedTestSuiteFailures).reduce( (acc, curr) => `${acc}\n${curr}`, '' - ), + )!, } : { name: `${unhandledErrors.length} unhandled error${unhandledErrors?.length > 1 ? 's' : ''}`, diff --git a/code/addons/test/src/node/test-manager.test.ts b/code/addons/test/src/node/test-manager.test.ts index db77ab2f3e2..985f74c9759 100644 --- a/code/addons/test/src/node/test-manager.test.ts +++ b/code/addons/test/src/node/test-manager.test.ts @@ -22,10 +22,11 @@ const vitest = vi.hoisted(() => ({ configOverride: { actualTestNamePattern: undefined, get testNamePattern() { - return this.actualTestNamePattern; + return this.actualTestNamePattern!; }, set testNamePattern(value: string) { setTestNamePattern(value); + // @ts-expect-error Ignore for testing this.actualTestNamePattern = value; }, }, @@ -105,11 +106,11 @@ describe('TestManager', () => { it('should handle watch mode request', async () => { const testManager = await TestManager.start(mockChannel, options); - expect(testManager.watchMode).toBe(false); + expect(testManager.config.watchMode).toBe(false); expect(createVitest).toHaveBeenCalledTimes(1); await testManager.handleWatchModeRequest({ providerId: TEST_PROVIDER_ID, watchMode: true }); - expect(testManager.watchMode).toBe(true); + expect(testManager.config.watchMode).toBe(true); expect(createVitest).toHaveBeenCalledTimes(1); // shouldn't restart vitest }); @@ -149,7 +150,7 @@ describe('TestManager', () => { it('should handle coverage toggling', async () => { const testManager = await TestManager.start(mockChannel, options); - expect(testManager.coverage).toBe(false); + expect(testManager.config.coverage).toBe(false); expect(createVitest).toHaveBeenCalledTimes(1); createVitest.mockClear(); @@ -157,7 +158,7 @@ describe('TestManager', () => { providerId: TEST_PROVIDER_ID, config: { coverage: true, a11y: false }, }); - expect(testManager.coverage).toBe(true); + expect(testManager.config.coverage).toBe(true); expect(createVitest).toHaveBeenCalledTimes(1); createVitest.mockClear(); @@ -165,21 +166,21 @@ describe('TestManager', () => { providerId: TEST_PROVIDER_ID, config: { coverage: false, a11y: false }, }); - expect(testManager.coverage).toBe(false); + expect(testManager.config.coverage).toBe(false); expect(createVitest).toHaveBeenCalledTimes(1); }); it('should temporarily disable coverage on focused tests', async () => { vitest.globTestSpecs.mockImplementation(() => tests); const testManager = await TestManager.start(mockChannel, options); - expect(testManager.coverage).toBe(false); + expect(testManager.config.coverage).toBe(false); expect(createVitest).toHaveBeenCalledTimes(1); await testManager.handleConfigChange({ providerId: TEST_PROVIDER_ID, config: { coverage: true, a11y: false }, }); - expect(testManager.coverage).toBe(true); + expect(testManager.config.coverage).toBe(true); expect(createVitest).toHaveBeenCalledTimes(2); await testManager.handleRunRequest({ diff --git a/code/addons/test/src/node/test-manager.ts b/code/addons/test/src/node/test-manager.ts index 4770f2b5a17..1a19e587eee 100644 --- a/code/addons/test/src/node/test-manager.ts +++ b/code/addons/test/src/node/test-manager.ts @@ -18,9 +18,11 @@ import { VitestManager } from './vitest-manager'; export class TestManager { vitestManager: VitestManager; - watchMode = false; - - coverage = false; + config = { + watchMode: false, + coverage: false, + a11y: false, + }; constructor( private channel: Channel, @@ -44,23 +46,22 @@ export class TestManager { return; } + const previousConfig = this.config; + + this.config = { + ...this.config, + ...payload.config, + } satisfies Config; + process.env.VITEST_STORYBOOK_CONFIG = JSON.stringify(payload.config); - if (this.coverage !== payload.config.coverage) { - this.coverage = payload.config.coverage; + if (previousConfig.coverage !== payload.config.coverage) { try { await this.vitestManager.restartVitest({ - coverage: this.coverage, + coverage: this.config.coverage, }); } catch (e) { - const isV8 = e.message?.includes('@vitest/coverage-v8'); - const isIstanbul = e.message?.includes('@vitest/coverage-istanbul'); - - if (e.message?.includes('Error: Failed to load url') && (isIstanbul || isV8)) { - const coveragePackage = isIstanbul ? 'coverage-istanbul' : 'coverage-v8'; - e.message = `Please install the @vitest/${coveragePackage} package to run with coverage`; - } - this.reportFatalError('Failed to change coverage mode', e); + this.reportFatalError('Failed to change coverage configuration', e); } } } @@ -69,7 +70,7 @@ export class TestManager { if (payload.providerId !== TEST_PROVIDER_ID) { return; } - this.watchMode = payload.watchMode; + this.config.watchMode = payload.watchMode; if (payload.config) { this.handleConfigChange({ @@ -78,14 +79,14 @@ export class TestManager { }); } - if (this.coverage) { + if (this.config.coverage) { try { if (payload.watchMode) { // if watch mode is toggled on and coverage is already enabled, restart vitest without coverage to automatically disable it await this.vitestManager.restartVitest({ coverage: false }); } else { // if watch mode is toggled off and coverage is already enabled, restart vitest with coverage to automatically re-enable it - await this.vitestManager.restartVitest({ coverage: this.coverage }); + await this.vitestManager.restartVitest({ coverage: this.config.coverage }); } } catch (e) { this.reportFatalError('Failed to change watch mode while coverage was enabled', e); @@ -111,7 +112,7 @@ export class TestManager { as a coverage report for a subset of stories is not useful. */ const temporarilyDisableCoverage = - this.coverage && !this.watchMode && (payload.storyIds ?? []).length > 0; + this.config.coverage && !this.config.watchMode && (payload.storyIds ?? []).length > 0; if (temporarilyDisableCoverage) { await this.vitestManager.restartVitest({ coverage: false, @@ -124,7 +125,7 @@ export class TestManager { if (temporarilyDisableCoverage) { // Re-enable coverage if it was temporarily disabled because of a subset of stories was run - await this.vitestManager.restartVitest({ coverage: this.coverage }); + await this.vitestManager.restartVitest({ coverage: this.config.coverage }); } } catch (e) { this.reportFatalError('Failed to run tests', e); diff --git a/code/addons/test/src/node/vitest-manager.ts b/code/addons/test/src/node/vitest-manager.ts index 7267f3dcea3..4145acf18a3 100644 --- a/code/addons/test/src/node/vitest-manager.ts +++ b/code/addons/test/src/node/vitest-manager.ts @@ -14,7 +14,7 @@ import type { TestingModuleRunRequestPayload } from 'storybook/internal/core-eve import type { DocsIndexEntry, StoryIndex, StoryIndexEntry } from '@storybook/types'; -import path, { normalize } from 'pathe'; +import path, { dirname, join, normalize } from 'pathe'; import slash from 'slash'; import { COVERAGE_DIRECTORY, type Config } from '../constants'; @@ -29,6 +29,11 @@ type TagsFilter = { skip: string[]; }; +const packageDir = dirname(require.resolve('@storybook/experimental-addon-test/package.json')); + +// We have to tell Vitest that it runs as part of Storybook +process.env.VITEST_STORYBOOK = 'true'; + export class VitestManager { vitest: Vitest | null = null; @@ -44,10 +49,10 @@ export class VitestManager { const { createVitest } = await import('vitest/node'); const storybookCoverageReporter: [string, StorybookCoverageReporterOptions] = [ - '@storybook/experimental-addon-test/internal/coverage-reporter', + join(packageDir, 'dist/node/coverage-reporter.js'), { testManager: this.testManager, - coverageOptions: this.vitest?.config?.coverage as ResolvedCoverageOptions<'v8'>, + coverageOptions: this.vitest?.config?.coverage as ResolvedCoverageOptions<'v8'> | undefined, }, ]; const coverageOptions = ( @@ -63,31 +68,17 @@ export class VitestManager { : { enabled: false } ) as CoverageOptions; - this.vitest = await createVitest( - 'test', - { - watch: true, - passWithNoTests: false, - // TODO: - // Do we want to enable Vite's default reporter? - // The output in the terminal might be too spamy and it might be better to - // find a way to just show errors and warnings for example - // Otherwise it might be hard for the user to discover Storybook related logs - reporters: ['default', new StorybookReporter(this.testManager)], - coverage: coverageOptions, - }, - { - define: { - // polyfilling process.env.VITEST_STORYBOOK to 'true' in the browser - 'process.env.VITEST_STORYBOOK': 'true', - }, - } - ); - - this.vitest.configOverride.env = { - // We signal to the test runner that we are running it via Storybook - VITEST_STORYBOOK: 'true', - }; + this.vitest = await createVitest('test', { + watch: true, + passWithNoTests: false, + // TODO: + // Do we want to enable Vite's default reporter? + // The output in the terminal might be too spamy and it might be better to + // find a way to just show errors and warnings for example + // Otherwise it might be hard for the user to discover Storybook related logs + reporters: ['default', new StorybookReporter(this.testManager)], + coverage: coverageOptions, + }); if (this.vitest) { this.vitest.onCancel(() => { @@ -97,16 +88,22 @@ export class VitestManager { try { await this.vitest.init(); - } catch (e) { + } catch (e: any) { + let message = 'Failed to initialize Vitest'; const isV8 = e.message?.includes('@vitest/coverage-v8'); const isIstanbul = e.message?.includes('@vitest/coverage-istanbul'); - if (e.message?.includes('Error: Failed to load url') && (isIstanbul || isV8)) { + if ( + (e.message?.includes('Failed to load url') && (isIstanbul || isV8)) || + // Vitest will sometimes not throw the correct missing-package-detection error, so we have to check for this as well + (e instanceof TypeError && + e?.message === "Cannot read properties of undefined (reading 'name')") + ) { const coveragePackage = isIstanbul ? 'coverage-istanbul' : 'coverage-v8'; - e.message = `Please install the @vitest/${coveragePackage} package to run with coverage`; + message += `\n\nPlease install the @vitest/${coveragePackage} package to collect coverage\n`; } - - this.testManager.reportFatalError('Failed to init Vitest', e); + this.testManager.reportFatalError(message, e); + return; } await this.setupWatchers(); @@ -151,7 +148,7 @@ export class VitestManager { ])) as StoryIndex; const storyIds = requestStoryIds || Object.keys(index.entries); return storyIds.map((id) => index.entries[id]).filter((story) => story.type === 'story'); - } catch (e) { + } catch (e: any) { log('Failed to fetch story index: ' + e.message); return []; } @@ -202,7 +199,7 @@ export class VitestManager { this.filterStories(story, spec.moduleId, { include, exclude, skip }) ); if (matches.length) { - if (!this.testManager.watchMode) { + if (!this.testManager.config.watchMode) { // Clear the file cache if watch mode is not enabled this.updateLastChanged(spec.moduleId); } @@ -319,20 +316,21 @@ export class VitestManager { const id = slash(file); this.vitest?.logger.clearHighlightCache(id); this.updateLastChanged(id); - this.storyCountForCurrentRun = 0; // when watch mode is disabled, don't trigger any tests (below) // but still invalidate the cache for the changed file, which is handled above - if (!this.testManager.watchMode) { + if (!this.testManager.config.watchMode) { return; } + + this.storyCountForCurrentRun = 0; await this.runAffectedTests(file); } async registerVitestConfigListener() { this.vitest?.server?.watcher.on('change', async (file) => { file = normalize(file); - const isConfig = file === this.vitest.server.config.configFile; + const isConfig = file === this.vitest?.server.config.configFile; if (isConfig) { log('Restarting Vitest due to config change'); await this.closeVitest(); diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index 55f97eb6b0d..b3c3dba9594 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -16,7 +16,7 @@ import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; import { colors, logger } from 'storybook/internal/node-logger'; // eslint-disable-next-line depend/ban-dependencies -import { execa } from 'execa'; +import { $ } from 'execa'; import { findUp } from 'find-up'; import { dirname, extname, join, relative, resolve } from 'pathe'; import picocolors from 'picocolors'; @@ -25,6 +25,7 @@ import { coerce, satisfies } from 'semver'; import { dedent } from 'ts-dedent'; import { type PostinstallOptions } from '../../../lib/cli-storybook/src/add'; +import { SUPPORTED_FRAMEWORKS, SUPPORTED_RENDERERS } from './constants'; import { printError, printInfo, printSuccess, step } from './postinstall-logger'; import { getAddonNames } from './utils'; @@ -55,8 +56,7 @@ export default async function postInstall(options: PostinstallOptions) { const allDeps = await packageManager.getAllDependencies(); // only install these dependencies if they are not already installed const dependencies = ['vitest', '@vitest/browser', 'playwright'].filter((p) => !allDeps[p]); - const vitestVersionSpecifier = - allDeps.vitest || (await packageManager.getInstalledVersion('vitest')); + const vitestVersionSpecifier = await packageManager.getInstalledVersion('vitest'); const coercedVitestVersion = vitestVersionSpecifier ? coerce(vitestVersionSpecifier) : null; // if Vitest is installed, we use the same version to keep consistency across Vitest packages const vitestVersionToInstall = vitestVersionSpecifier ?? 'latest'; @@ -106,18 +106,11 @@ export default async function postInstall(options: PostinstallOptions) { } } - const annotationsImport = [ - '@storybook/nextjs', - '@storybook/experimental-nextjs-vite', - '@storybook/sveltekit', - ].includes(info.frameworkPackageName) + const annotationsImport = SUPPORTED_FRAMEWORKS.includes(info.frameworkPackageName) ? info.frameworkPackageName === '@storybook/nextjs' ? '@storybook/experimental-nextjs-vite' : info.frameworkPackageName - : info.rendererPackageName && - ['@storybook/react', '@storybook/svelte', '@storybook/vue3'].includes( - info.rendererPackageName - ) + : info.rendererPackageName && SUPPORTED_RENDERERS.includes(info.rendererPackageName) ? info.rendererPackageName : null; @@ -177,14 +170,14 @@ export default async function postInstall(options: PostinstallOptions) { reasons.push( dedent` Please check the documentation for more information about its requirements and installation: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon`)} ` ); } else { reasons.push( dedent` Fear not, however, you can follow the manual installation process instead at: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin#manual`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon#manual-setup`)} ` ); } @@ -227,22 +220,9 @@ export default async function postInstall(options: PostinstallOptions) { } if (shouldUninstall) { - await execa( - packageManager.getRemoteRunCommand(), - [ - 'storybook', - 'remove', - addonInteractionsName, - '--package-manager', - options.packageManager, - '--config-dir', - options.configDir, - ], - { - shell: true, - stdio: 'inherit', - } - ); + await $({ + stdio: 'inherit', + })`storybook remove ${addonInteractionsName} --package-manager ${options.packageManager} --config-dir ${options.configDir}`; } } @@ -321,7 +301,7 @@ export default async function postInstall(options: PostinstallOptions) { ${colors.gray(vitestSetupFile)} Please refer to the documentation to complete the setup manually: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin#manual`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon#manual-setup`)} ` ); logger.line(1); @@ -381,7 +361,7 @@ export default async function postInstall(options: PostinstallOptions) { your existing workspace file automatically, you must do it yourself. This was the last step. Please refer to the documentation to complete the setup manually: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin#manual`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon#manual-setup`)} ` ); logger.line(1); @@ -397,13 +377,13 @@ export default async function postInstall(options: PostinstallOptions) { '🚨 Oh no!', dedent` You seem to have an existing test configuration in your Vite config file: - ${colors.gray(vitestWorkspaceFile || '')} + ${colors.gray(viteConfigFile || '')} I was able to configure most of the addon but could not safely extend your existing workspace file automatically, you must do it yourself. This was the last step. Please refer to the documentation to complete the setup manually: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin#manual`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon#manual-setup`)} ` ); logger.line(1); @@ -429,14 +409,14 @@ export default async function postInstall(options: PostinstallOptions) { import { defineWorkspace } from 'vitest/config'; import { storybookTest } from '@storybook/experimental-addon-test/vitest-plugin';${vitestInfo.frameworkPluginImport} - // More info at: https://storybook.js.org/docs/writing-tests/vitest-plugin + // More info at: https://storybook.js.org/docs/writing-tests/test-addon export default defineWorkspace([ '${relative(dirname(browserWorkspaceFile), rootConfig)}', { extends: '${viteConfigFile ? relative(dirname(browserWorkspaceFile), viteConfigFile) : ''}', plugins: [ // The plugin will run tests for the stories defined in your Storybook config - // See options at: https://storybook.js.org/docs/writing-tests/vitest-plugin#storybooktest + // See options at: https://storybook.js.org/docs/writing-tests/test-addon#storybooktest storybookTest({ configDir: '${options.configDir}' }),${vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall} ], test: { @@ -469,11 +449,11 @@ export default async function postInstall(options: PostinstallOptions) { import { defineConfig } from 'vitest/config'; import { storybookTest } from '@storybook/experimental-addon-test/vitest-plugin';${vitestInfo.frameworkPluginImport} - // More info at: https://storybook.js.org/docs/writing-tests/vitest-plugin + // More info at: https://storybook.js.org/docs/writing-tests/test-addon export default defineConfig({ plugins: [ // The plugin will run tests for the stories defined in your Storybook config - // See options at: https://storybook.js.org/docs/writing-tests/vitest-plugin#storybooktest + // See options at: https://storybook.js.org/docs/writing-tests/test-addon#storybooktest storybookTest({ configDir: '${options.configDir}' }),${vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall} ], test: { @@ -503,7 +483,7 @@ export default async function postInstall(options: PostinstallOptions) { • When using the Vitest extension in your editor, all of your stories will be shown as tests! Check the documentation for more information about its features and options at: - ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/vitest-plugin`)} + ${picocolors.cyan(`https://storybook.js.org/docs/writing-tests/test-addon`)} ` ); logger.line(1); diff --git a/code/addons/test/src/vitest-plugin/global-setup.ts b/code/addons/test/src/vitest-plugin/global-setup.ts index ca287c105e2..8526c48e245 100644 --- a/code/addons/test/src/vitest-plugin/global-setup.ts +++ b/code/addons/test/src/vitest-plugin/global-setup.ts @@ -74,13 +74,15 @@ export const teardown = async () => { logger.verbose('Stopping Storybook process'); await new Promise((resolve, reject) => { // Storybook starts multiple child processes, so we need to kill the whole tree - treeKill(storybookProcess.pid, 'SIGTERM', (error) => { - if (error) { - logger.error('Failed to stop Storybook process:'); - reject(error); - return; - } - resolve(); - }); + if (storybookProcess?.pid) { + treeKill(storybookProcess.pid, 'SIGTERM', (error) => { + if (error) { + logger.error('Failed to stop Storybook process:'); + reject(error); + return; + } + resolve(); + }); + } }); }; diff --git a/code/addons/test/src/vitest-plugin/index.ts b/code/addons/test/src/vitest-plugin/index.ts index eadadb4440e..180075f9f89 100644 --- a/code/addons/test/src/vitest-plugin/index.ts +++ b/code/addons/test/src/vitest-plugin/index.ts @@ -1,4 +1,6 @@ /* eslint-disable no-underscore-dangle */ +import { dirname } from 'node:path'; + import type { Plugin } from 'vitest/config'; import { mergeConfig } from 'vitest/config'; import type { ViteUserConfig } from 'vitest/config'; @@ -23,7 +25,6 @@ import sirv from 'sirv'; import { convertPathToPattern } from 'tinyglobby'; import { dedent } from 'ts-dedent'; -import { TestManager } from '../node/test-manager'; import type { InternalOptions, UserOptions } from './types'; const WORKING_DIR = process.cwd(); @@ -63,6 +64,8 @@ const getStoryGlobsAndFiles = async ( }; }; +const packageDir = dirname(require.resolve('@storybook/experimental-addon-test/package.json')); + export const storybookTest = async (options?: UserOptions): Promise => { const finalOptions = { ...defaultOptions, @@ -124,7 +127,7 @@ export const storybookTest = async (options?: UserOptions): Promise => { .replace(' ', `${headHtmlSnippet ?? ''} `) .replace('', `${bodyHtmlSnippet ?? ''}`); }, - async config(inputConfig_DoNotMutate) { + async config(inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED) { // ! We're not mutating the input config, instead we're returning a new partial config // ! see https://vite.dev/guide/api-plugin.html#config try { @@ -143,18 +146,23 @@ export const storybookTest = async (options?: UserOptions): Promise => { // plugin.name?.startsWith('vitest:browser') // ) + // We signal the test runner that we are not running it via Storybook + // We are overriding the environment variable to 'true' if vitest runs via @storybook/addon-test's backend + const vitestStorybook = process.env.VITEST_STORYBOOK ?? 'false'; + const baseConfig: Omit = { test: { setupFiles: [ - '@storybook/experimental-addon-test/internal/setup-file', + join(packageDir, 'dist/vitest-plugin/setup-file.mjs'), // if the existing setupFiles is a string, we have to include it otherwise we're overwriting it - typeof inputConfig_DoNotMutate.test?.setupFiles === 'string' && - inputConfig_DoNotMutate.test?.setupFiles, - ].filter(Boolean), + typeof inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test + ?.setupFiles === 'string' && + inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.setupFiles, + ].filter(Boolean) as string[], ...(finalOptions.storybookScript ? { - globalSetup: ['@storybook/experimental-addon-test/internal/global-setup'], + globalSetup: [join(packageDir, 'dist/vitest-plugin/global-setup.mjs')], } : {}), @@ -162,9 +170,8 @@ export const storybookTest = async (options?: UserOptions): Promise => { ...storybookEnv, // To be accessed by the setup file __STORYBOOK_URL__: finalOptions.storybookUrl, - // We signal the test runner that we are not running it via Storybook - // We are overriding the environment variable to 'true' if vitest runs via @storybook/addon-test's backend - VITEST_STORYBOOK: 'false', + + VITEST_STORYBOOK: vitestStorybook, __VITEST_INCLUDE_TAGS__: finalOptions.tags.include.join(','), __VITEST_EXCLUDE_TAGS__: finalOptions.tags.exclude.join(','), __VITEST_SKIP_TAGS__: finalOptions.tags.skip.join(','), @@ -175,7 +182,8 @@ export const storybookTest = async (options?: UserOptions): Promise => { .map((path) => convertPathToPattern(path)), // if the existing deps.inline is true, we keep it as-is, because it will inline everything - ...(inputConfig_DoNotMutate.test?.server?.deps?.inline !== true + ...(inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.server?.deps + ?.inline !== true ? { server: { deps: { @@ -185,8 +193,9 @@ export const storybookTest = async (options?: UserOptions): Promise => { } : {}), + // @ts-expect-error: TODO browser: { - ...inputConfig_DoNotMutate.test?.browser, + ...inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.browser, commands: { getInitialGlobals: () => { const envConfig = JSON.parse(process.env.VITEST_STORYBOOK_CONFIG ?? '{}'); @@ -203,8 +212,9 @@ export const storybookTest = async (options?: UserOptions): Promise => { }, }, // if there is a test.browser config AND test.browser.screenshotFailures is not explicitly set, we set it to false - ...(inputConfig_DoNotMutate.test?.browser && - inputConfig_DoNotMutate.test.browser.screenshotFailures === undefined + ...(inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.browser && + inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test.browser + .screenshotFailures === undefined ? { screenshotFailures: false, } @@ -213,7 +223,11 @@ export const storybookTest = async (options?: UserOptions): Promise => { }, envPrefix: Array.from( - new Set([...(inputConfig_DoNotMutate.envPrefix || []), 'STORYBOOK_', 'VITE_']) + new Set([ + ...(inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.envPrefix || []), + 'STORYBOOK_', + 'VITE_', + ]) ), resolve: { @@ -239,8 +253,6 @@ export const storybookTest = async (options?: UserOptions): Promise => { }, define: { - // polyfilling process.env.VITEST_STORYBOOK to 'false' in the browser - 'process.env.VITEST_STORYBOOK': JSON.stringify('false'), ...(frameworkName?.includes('vue3') ? { __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false' } : {}), @@ -254,8 +266,14 @@ export const storybookTest = async (options?: UserOptions): Promise => { ); // alert the user of problems - if (inputConfig_DoNotMutate.test.include?.length > 0) { - console.warn( + if ( + (inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.include?.length ?? + 0) > 0 + ) { + // remove the user's existing include, because we're replacing it with our own heuristic based on main.ts#stories + // @ts-expect-error: Ignore + inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test.include = []; + console.log( picocolors.yellow(dedent` Warning: Starting in Storybook 8.5.0-alpha.18, the "test.include" option in Vitest is discouraged in favor of just using the "stories" field in your Storybook configuration. @@ -270,19 +288,21 @@ export const storybookTest = async (options?: UserOptions): Promise => { return config; }, async configureServer(server) { - for (const staticDir of staticDirs) { - try { - const { staticPath, targetEndpoint } = mapStaticDir(staticDir, directories.configDir); - server.middlewares.use( - targetEndpoint, - sirv(staticPath, { - dev: true, - etag: true, - extensions: [], - }) - ); - } catch (e) { - console.warn(e); + if (staticDirs) { + for (const staticDir of staticDirs) { + try { + const { staticPath, targetEndpoint } = mapStaticDir(staticDir, directories.configDir); + server.middlewares.use( + targetEndpoint, + sirv(staticPath, { + dev: true, + etag: true, + extensions: [], + }) + ); + } catch (e) { + console.warn(e); + } } } }, diff --git a/code/addons/test/src/vitest-plugin/viewports.ts b/code/addons/test/src/vitest-plugin/viewports.ts index a8bcc90bc40..905ee44fc93 100644 --- a/code/addons/test/src/vitest-plugin/viewports.ts +++ b/code/addons/test/src/vitest-plugin/viewports.ts @@ -85,7 +85,7 @@ export const setViewport = async (parameters: Parameters = {}, globals: Globals let viewportWidth = DEFAULT_VIEWPORT_DIMENSIONS.width; let viewportHeight = DEFAULT_VIEWPORT_DIMENSIONS.height; - if (defaultViewport in viewports) { + if (defaultViewport && defaultViewport in viewports) { const styles = viewports[defaultViewport].styles as ViewportStyles; if (styles?.width && styles?.height) { const { width, height } = styles; diff --git a/code/addons/test/tsconfig.json b/code/addons/test/tsconfig.json index 060b5d432fc..e8a15eafa0b 100644 --- a/code/addons/test/tsconfig.json +++ b/code/addons/test/tsconfig.json @@ -5,7 +5,7 @@ "module": "Preserve", "moduleResolution": "Bundler", "types": ["vitest"], - "strict": false + "strict": true }, "include": ["src/**/*", "./typings.d.ts"] } diff --git a/code/addons/themes/docs/api.md b/code/addons/themes/docs/api.md index a45818ab724..d9c2d2285fc 100644 --- a/code/addons/themes/docs/api.md +++ b/code/addons/themes/docs/api.md @@ -109,6 +109,9 @@ export const myCustomDecorator = ### `useThemeParameters` +(⛔️ **Deprecated**) +_Do not use this hook anymore. Access the theme directly via the context instead e.g. `context.parameters.themes`_ + Returns the theme parameters for this addon. ```js @@ -152,14 +155,14 @@ Let's use Vuetify as an example. Vuetify uses it's own global state to know whic import { DecoratorHelpers } from '@storybook/addon-themes'; import { useTheme } from 'vuetify'; -const { initializeThemeState, pluckThemeFromContext, useThemeParameters } = DecoratorHelpers; +const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers; export const withVuetifyTheme = ({ themes, defaultTheme }) => { initializeThemeState(Object.keys(themes), defaultTheme); return (story, context) => { const selectedTheme = pluckThemeFromContext(context); - const { themeOverride } = useThemeParameters(); + const { themeOverride } = context.parameters.themes ?? {}; const selected = themeOverride || selectedTheme || defaultTheme; diff --git a/code/addons/themes/package.json b/code/addons/themes/package.json index 3acb32417fb..acfd4e156a9 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-themes", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Switch between multiple themes for you components in Storybook", "keywords": [ "css", diff --git a/code/addons/themes/src/decorators/class-name.decorator.tsx b/code/addons/themes/src/decorators/class-name.decorator.tsx index 4bc56202f03..42b403a5b38 100644 --- a/code/addons/themes/src/decorators/class-name.decorator.tsx +++ b/code/addons/themes/src/decorators/class-name.decorator.tsx @@ -1,7 +1,8 @@ import { useEffect } from 'storybook/internal/preview-api'; import type { DecoratorFunction, Renderer } from 'storybook/internal/types'; -import { initializeThemeState, pluckThemeFromContext, useThemeParameters } from './helpers'; +import { PARAM_KEY } from '../constants'; +import { initializeThemeState, pluckThemeFromContext } from './helpers'; export interface ClassNameStrategyConfiguration { themes: Record; @@ -22,7 +23,7 @@ export const withThemeByClassName = ({ initializeThemeState(Object.keys(themes), defaultTheme); return (storyFn, context) => { - const { themeOverride } = useThemeParameters(); + const { themeOverride } = context.parameters[PARAM_KEY] ?? {}; const selected = pluckThemeFromContext(context); useEffect(() => { diff --git a/code/addons/themes/src/decorators/data-attribute.decorator.tsx b/code/addons/themes/src/decorators/data-attribute.decorator.tsx index 6d4b1cfe26f..55e4f258bfd 100644 --- a/code/addons/themes/src/decorators/data-attribute.decorator.tsx +++ b/code/addons/themes/src/decorators/data-attribute.decorator.tsx @@ -1,7 +1,8 @@ import { useEffect } from 'storybook/internal/preview-api'; import type { DecoratorFunction, Renderer } from 'storybook/internal/types'; -import { initializeThemeState, pluckThemeFromContext, useThemeParameters } from './helpers'; +import { PARAM_KEY } from '../constants'; +import { initializeThemeState, pluckThemeFromContext } from './helpers'; export interface DataAttributeStrategyConfiguration { themes: Record; @@ -22,7 +23,7 @@ export const withThemeByDataAttribute = ({ }: DataAttributeStrategyConfiguration): DecoratorFunction => { initializeThemeState(Object.keys(themes), defaultTheme); return (storyFn, context) => { - const { themeOverride } = useThemeParameters(); + const { themeOverride } = context.parameters[PARAM_KEY] ?? {}; const selected = pluckThemeFromContext(context); useEffect(() => { diff --git a/code/addons/themes/src/decorators/helpers.ts b/code/addons/themes/src/decorators/helpers.ts index 237b0943568..97c70dd1f0a 100644 --- a/code/addons/themes/src/decorators/helpers.ts +++ b/code/addons/themes/src/decorators/helpers.ts @@ -1,6 +1,9 @@ +import { deprecate } from 'storybook/internal/client-logger'; import { addons, useParameter } from 'storybook/internal/preview-api'; import type { StoryContext } from 'storybook/internal/types'; +import dedent from 'ts-dedent'; + import type { ThemeParameters } from '../constants'; import { DEFAULT_THEME_PARAMETERS, GLOBAL_KEY, PARAM_KEY, THEMING_EVENTS } from '../constants'; @@ -12,8 +15,18 @@ export function pluckThemeFromContext({ globals }: StoryContext): string { return globals[GLOBAL_KEY] || ''; } -export function useThemeParameters(): ThemeParameters { - return useParameter(PARAM_KEY, DEFAULT_THEME_PARAMETERS) as ThemeParameters; +export function useThemeParameters(context?: StoryContext): ThemeParameters { + deprecate( + dedent`The useThemeParameters function is deprecated. Please access parameters via the context directly instead e.g. + - const { themeOverride } = context.parameters.themes ?? {}; + ` + ); + + if (!context) { + return useParameter(PARAM_KEY, DEFAULT_THEME_PARAMETERS) as ThemeParameters; + } + + return context.parameters[PARAM_KEY] ?? DEFAULT_THEME_PARAMETERS; } export function initializeThemeState(themeNames: string[], defaultTheme: string) { diff --git a/code/addons/themes/src/decorators/provider.decorator.tsx b/code/addons/themes/src/decorators/provider.decorator.tsx index 8985a9773fa..41964e7afcf 100644 --- a/code/addons/themes/src/decorators/provider.decorator.tsx +++ b/code/addons/themes/src/decorators/provider.decorator.tsx @@ -4,7 +4,8 @@ import React from 'react'; import { useMemo } from 'storybook/internal/preview-api'; import type { DecoratorFunction, Renderer } from 'storybook/internal/types'; -import { initializeThemeState, pluckThemeFromContext, useThemeParameters } from './helpers'; +import { PARAM_KEY } from '../constants'; +import { initializeThemeState, pluckThemeFromContext } from './helpers'; type Theme = Record; type ThemeMap = Record; @@ -32,7 +33,8 @@ export const withThemeFromJSXProvider = ({ // eslint-disable-next-line react/display-name return (storyFn, context) => { - const { themeOverride } = useThemeParameters(); + // eslint-disable-next-line react/destructuring-assignment + const { themeOverride } = context.parameters[PARAM_KEY] ?? {}; const selected = pluckThemeFromContext(context); const theme = useMemo(() => { diff --git a/code/addons/toolbars/package.json b/code/addons/toolbars/package.json index f4917fae862..2799a7a8d7d 100644 --- a/code/addons/toolbars/package.json +++ b/code/addons/toolbars/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-toolbars", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Create your own toolbar items that control story rendering", "keywords": [ "addon", diff --git a/code/addons/viewport/package.json b/code/addons/viewport/package.json index 2cad1d10517..a41b266ffaf 100644 --- a/code/addons/viewport/package.json +++ b/code/addons/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-viewport", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Build responsive components by adjusting Storybook’s viewport size and orientation", "keywords": [ "addon", diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json index 12c37e443cc..5fd9910d7fa 100644 --- a/code/builders/builder-vite/package.json +++ b/code/builders/builder-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-vite", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "A plugin to run and build Storybooks with Vite", "homepage": "https://github.com/storybookjs/storybook/tree/next/code/builders/builder-vite/#readme", "bugs": { diff --git a/code/builders/builder-vite/src/build.ts b/code/builders/builder-vite/src/build.ts index fa7d1ee4f76..9782081c046 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 785db459cec..7051cc11636 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-vite/src/optimizeDeps.ts b/code/builders/builder-vite/src/optimizeDeps.ts index cbade7a268a..74dd0090be4 100644 --- a/code/builders/builder-vite/src/optimizeDeps.ts +++ b/code/builders/builder-vite/src/optimizeDeps.ts @@ -13,16 +13,58 @@ const INCLUDE_CANDIDATES = [ '@emotion/core', '@emotion/is-prop-valid', '@emotion/styled', + '@storybook/addon-a11y/preview', + '@storybook/addon-backgrounds/preview', + '@storybook/addon-designs/blocks', + '@storybook/addon-docs/preview', + '@storybook/addon-essentials/actions/preview', + '@storybook/addon-essentials/actions/preview', + '@storybook/addon-essentials/backgrounds/preview', + '@storybook/addon-essentials/docs/preview', + '@storybook/addon-essentials/highlight/preview', + '@storybook/addon-essentials/measure/preview', + '@storybook/addon-essentials/outline/preview', + '@storybook/addon-essentials/viewport/preview', + '@storybook/addon-highlight/preview', + '@storybook/addon-links/preview', + '@storybook/addon-measure/preview', + '@storybook/addon-outline/preview', + '@storybook/addon-themes', + '@storybook/addon-themes/preview', + '@storybook/addon-viewport', + '@storybook/addon-viewport/preview', + '@storybook/blocks', + '@storybook/components', + '@storybook/experimental-addon-test/preview', + '@storybook/experimental-nextjs-vite/dist/preview.mjs', + '@storybook/html', + '@storybook/html/dist/entry-preview-docs.mjs', + '@storybook/html/dist/entry-preview.mjs', + '@storybook/preact', + '@storybook/preact/dist/entry-preview-docs.mjs', + '@storybook/preact/dist/entry-preview.mjs', '@storybook/react > acorn-jsx', '@storybook/react', + '@storybook/react/dist/entry-preview-docs.mjs', + '@storybook/react/dist/entry-preview-rsc.mjs', + '@storybook/react/dist/entry-preview.mjs', '@storybook/svelte', + '@storybook/svelte/dist/entry-preview-docs.mjs', + '@storybook/svelte/dist/entry-preview.mjs', + '@storybook/theming', '@storybook/vue3', + '@storybook/vue3/dist/entry-preview-docs.mjs', + '@storybook/vue3/dist/entry-preview.mjs', + '@storybook/web-components', + '@storybook/web-components/dist/entry-preview-docs.mjs', + '@storybook/web-components/dist/entry-preview.mjs', 'acorn-jsx', 'acorn-walk', 'acorn', 'airbnb-js-shims', 'ansi-to-html', 'axe-core', + 'chromatic/isChromatic', 'color-convert', 'deep-object-diff', 'doctrine', @@ -73,6 +115,8 @@ const INCLUDE_CANDIDATES = [ 'lodash/upperFirst.js', 'lodash/upperFirst', 'memoizerific', + 'mockdate', + 'msw-storybook-addon', 'overlayscrollbars', 'polished', 'prettier/parser-babel', @@ -100,8 +144,11 @@ const INCLUDE_CANDIDATES = [ 'refractor/lang/typescript.js', 'refractor/lang/yaml.js', 'regenerator-runtime/runtime.js', + 'sb-original/default-loader', + 'sb-original/image-context', 'slash', 'store2', + 'storybook/internal/preview/runtime', 'synchronous-promise', 'telejson', 'ts-dedent', diff --git a/code/builders/builder-webpack5/package.json b/code/builders/builder-webpack5/package.json index 31dc74add9c..3e5c45e92f9 100644 --- a/code/builders/builder-webpack5/package.json +++ b/code/builders/builder-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-webpack5", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/builders/builder-webpack5/src/index.ts b/code/builders/builder-webpack5/src/index.ts index a8af6e699ad..71be9982973 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 763f2bf1564..aa9e34c73af 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/assets/server/addon.tsconfig.json b/code/core/assets/server/addon.tsconfig.json index 9161d5ff825..38452bcdfa2 100644 --- a/code/core/assets/server/addon.tsconfig.json +++ b/code/core/assets/server/addon.tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "jsx": "react-jsx", + "jsx": "react", "jsxImportSource": "react" } } diff --git a/code/core/package.json b/code/core/package.json index ac5c8991b31..8771b8f76c1 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/core/scripts/helpers/sourcefiles.ts b/code/core/scripts/helpers/sourcefiles.ts index 2f437961a91..e7a5394cb9b 100644 --- a/code/core/scripts/helpers/sourcefiles.ts +++ b/code/core/scripts/helpers/sourcefiles.ts @@ -2,6 +2,7 @@ import { readdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { GlobalRegistrator } from '@happy-dom/global-registrator'; +import { isNotNil } from 'es-toolkit'; import { dedent, esbuild, getWorkspace, prettier } from '../../../../scripts/prepare/tools'; import { temporaryFile } from '../../src/common/utils/cli'; @@ -26,7 +27,7 @@ export const generateSourceFiles = async () => { async function generateVersionsFile(prettierConfig: prettier.Options | null): Promise { const location = join(__dirname, '..', '..', 'src', 'common', 'versions.ts'); - const workspace = await getWorkspace(); + const workspace = (await getWorkspace()).filter(isNotNil); const versions = JSON.stringify( workspace @@ -55,7 +56,7 @@ async function generateVersionsFile(prettierConfig: prettier.Options | null): Pr } async function generateFrameworksFile(prettierConfig: prettier.Options | null): Promise { - const thirdPartyFrameworks = ['qwik', 'solid', 'react-rsbuild', 'vue3-rsbuild']; + const thirdPartyFrameworks = ['qwik', 'solid', 'nuxt', 'react-rsbuild', 'vue3-rsbuild']; const location = join(__dirname, '..', '..', 'src', 'types', 'modules', 'frameworks.ts'); const frameworksDirectory = join(__dirname, '..', '..', '..', 'frameworks'); diff --git a/code/core/src/__mocks__/page.ts b/code/core/src/__mocks__/page.ts new file mode 100644 index 00000000000..fb87bbd306b --- /dev/null +++ b/code/core/src/__mocks__/page.ts @@ -0,0 +1 @@ +// empty file only matched on path diff --git a/code/core/src/__mocks__/path/to/Screens/index.jsx b/code/core/src/__mocks__/path/to/Screens/index.jsx new file mode 100644 index 00000000000..fb87bbd306b --- /dev/null +++ b/code/core/src/__mocks__/path/to/Screens/index.jsx @@ -0,0 +1 @@ +// empty file only matched on path diff --git a/code/core/src/cli/detect.test.ts b/code/core/src/cli/detect.test.ts index ea7f8139fda..409cf92effa 100644 --- a/code/core/src/cli/detect.test.ts +++ b/code/core/src/cli/detect.test.ts @@ -43,6 +43,28 @@ const MOCK_FRAMEWORK_FILES: { }, }, }, + { + name: ProjectType.NUXT, + files: { + 'package.json': { + dependencies: { + nuxt: '^3.11.2', + }, + }, + }, + }, + { + name: ProjectType.NUXT, + files: { + 'package.json': { + dependencies: { + // Nuxt projects may have Vue 3 as an explicit dependency + nuxt: '^3.11.2', + vue: '^3.0.0', + }, + }, + }, + }, { name: ProjectType.VUE3, files: { @@ -435,16 +457,6 @@ describe('Detect', () => { expect(result).toBe(ProjectType.UNDETECTED); }); - // TODO(blaine): Remove once Nuxt3 is supported - it(`UNSUPPORTED for Nuxt framework above version 3.0.0`, () => { - const result = detectFrameworkPreset({ - dependencies: { - nuxt: '3.0.0', - }, - }); - expect(result).toBe(ProjectType.UNSUPPORTED); - }); - // TODO: The mocking in this test causes tests after it to fail it('REACT_SCRIPTS for custom react scripts config', () => { const forkedReactScriptsConfig = { diff --git a/code/core/src/cli/detect.ts b/code/core/src/cli/detect.ts index 5b50abee430..a771f6476f8 100644 --- a/code/core/src/cli/detect.ts +++ b/code/core/src/cli/detect.ts @@ -123,7 +123,11 @@ export async function detectBuilder(packageManager: JsPackageManager, projectTyp } // REWORK - if (webpackConfig || (dependencies.webpack && dependencies.vite !== undefined)) { + if ( + webpackConfig || + ((dependencies.webpack || dependencies['@nuxt/webpack-builder']) && + dependencies.vite !== undefined) + ) { commandLog('Detected webpack project. Setting builder to webpack')(); return CoreBuilder.Webpack5; } @@ -138,6 +142,8 @@ export async function detectBuilder(packageManager: JsPackageManager, projectTyp case ProjectType.NEXTJS: case ProjectType.EMBER: return CoreBuilder.Webpack5; + case ProjectType.NUXT: + return CoreBuilder.Vite; default: const { builder } = await prompts( { @@ -207,6 +213,13 @@ export async function detectLanguage(packageManager: JsPackageManager) { } else if (semver.lt(typescriptVersion, '3.8.0')) { logger.warn('Detected TypeScript < 3.8, populating with JavaScript examples'); } + } else { + // No direct dependency on TypeScript, but could be a transitive dependency + // This is eg the case for Nuxt projects, which support a recent version of TypeScript + // Check for tsconfig.json (https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) + if (existsSync('tsconfig.json')) { + language = SupportedLanguage.TYPESCRIPT_4_9; + } } return language; diff --git a/code/core/src/cli/dirs.ts b/code/core/src/cli/dirs.ts index 11ca9cb6744..6e6867e38d5 100644 --- a/code/core/src/cli/dirs.ts +++ b/code/core/src/cli/dirs.ts @@ -40,7 +40,7 @@ export async function getRendererDir( ) { const externalFramework = externalFrameworks.find((framework) => framework.name === renderer); const frameworkPackageName = - externalFramework?.renderer || externalFramework?.packageName || `@storybook/${renderer}`; + externalFramework?.packageName || externalFramework?.renderer || `@storybook/${renderer}`; const packageJsonPath = join(frameworkPackageName, 'package.json'); diff --git a/code/core/src/cli/helpers.ts b/code/core/src/cli/helpers.ts index 27e91f001ac..d5ebb9b65a2 100644 --- a/code/core/src/cli/helpers.ts +++ b/code/core/src/cli/helpers.ts @@ -153,6 +153,7 @@ export const frameworkToDefaultBuilder: Record< 'html-vite': CoreBuilder.Vite, 'html-webpack5': CoreBuilder.Webpack5, nextjs: CoreBuilder.Webpack5, + nuxt: CoreBuilder.Vite, 'experimental-nextjs-vite': CoreBuilder.Vite, 'preact-vite': CoreBuilder.Vite, 'preact-webpack5': CoreBuilder.Webpack5, diff --git a/code/core/src/cli/project_types.ts b/code/core/src/cli/project_types.ts index 25148d2bc08..d9ca64e2534 100644 --- a/code/core/src/cli/project_types.ts +++ b/code/core/src/cli/project_types.ts @@ -1,5 +1,5 @@ import type { - SupportedRenderers as CoreSupportedFrameworks, + SupportedRenderers as CoreSupportedRenderers, SupportedFrameworks, } from '@storybook/core/types'; @@ -24,10 +24,16 @@ export type ExternalFramework = { export const externalFrameworks: ExternalFramework[] = [ { name: 'qwik', packageName: 'storybook-framework-qwik' }, { name: 'solid', frameworks: ['storybook-solidjs-vite'], renderer: 'storybook-solidjs' }, + { + name: 'nuxt', + packageName: '@storybook-vue/nuxt', + frameworks: ['@storybook-vue/nuxt'], + renderer: '@storybook/vue3', + }, ]; -/** @deprecated Please use `SupportedFrameworks` from `@storybook/types` instead */ -export type SupportedRenderers = CoreSupportedFrameworks; +/** @deprecated Please use `SupportedRenderers` from `@storybook/types` instead */ +export type SupportedRenderers = CoreSupportedRenderers; export const SUPPORTED_RENDERERS: SupportedRenderers[] = [ 'react', @@ -52,6 +58,7 @@ export enum ProjectType { WEBPACK_REACT = 'WEBPACK_REACT', NEXTJS = 'NEXTJS', VUE3 = 'VUE3', + NUXT = 'NUXT', ANGULAR = 'ANGULAR', EMBER = 'EMBER', WEB_COMPONENTS = 'WEB_COMPONENTS', @@ -121,6 +128,13 @@ export type TemplateConfiguration = { * specific. */ export const supportedTemplates: TemplateConfiguration[] = [ + { + preset: ProjectType.NUXT, + dependencies: ['nuxt'], + matcherFunction: ({ dependencies }) => { + return dependencies?.every(Boolean) ?? true; + }, + }, { preset: ProjectType.VUE3, dependencies: { @@ -242,10 +256,7 @@ export const supportedTemplates: TemplateConfiguration[] = [ // users an "Unsupported framework" message export const unsupportedTemplate: TemplateConfiguration = { preset: ProjectType.UNSUPPORTED, - dependencies: { - // TODO(blaine): Remove when we support Nuxt 3 - nuxt: (versionRange) => eqMajor(versionRange, 3), - }, + dependencies: {}, matcherFunction: ({ dependencies }) => { return dependencies?.some(Boolean) ?? false; }, diff --git a/code/core/src/common/utils/framework-to-renderer.ts b/code/core/src/common/utils/framework-to-renderer.ts index 7ae4c3b057a..72cb4c1e248 100644 --- a/code/core/src/common/utils/framework-to-renderer.ts +++ b/code/core/src/common/utils/framework-to-renderer.ts @@ -24,6 +24,7 @@ export const frameworkToRenderer: Record< sveltekit: 'svelte', 'vue3-vite': 'vue3', 'vue3-webpack5': 'vue3', + nuxt: 'vue3', 'web-components-vite': 'web-components', 'web-components-webpack5': 'web-components', 'react-rsbuild': 'react', diff --git a/code/core/src/common/versions.ts b/code/core/src/common/versions.ts index 766faaec02c..1b87cbdf744 100644 --- a/code/core/src/common/versions.ts +++ b/code/core/src/common/versions.ts @@ -1,88 +1,88 @@ // auto generated file, do not edit export default { - '@storybook/addon-a11y': '8.5.0-alpha.20', - '@storybook/addon-actions': '8.5.0-alpha.20', - '@storybook/addon-backgrounds': '8.5.0-alpha.20', - '@storybook/addon-controls': '8.5.0-alpha.20', - '@storybook/addon-docs': '8.5.0-alpha.20', - '@storybook/addon-essentials': '8.5.0-alpha.20', - '@storybook/addon-mdx-gfm': '8.5.0-alpha.20', - '@storybook/addon-highlight': '8.5.0-alpha.20', - '@storybook/addon-interactions': '8.5.0-alpha.20', - '@storybook/addon-jest': '8.5.0-alpha.20', - '@storybook/addon-links': '8.5.0-alpha.20', - '@storybook/addon-measure': '8.5.0-alpha.20', - '@storybook/addon-onboarding': '8.5.0-alpha.20', - '@storybook/addon-outline': '8.5.0-alpha.20', - '@storybook/addon-storysource': '8.5.0-alpha.20', - '@storybook/experimental-addon-test': '8.5.0-alpha.20', - '@storybook/addon-themes': '8.5.0-alpha.20', - '@storybook/addon-toolbars': '8.5.0-alpha.20', - '@storybook/addon-viewport': '8.5.0-alpha.20', - '@storybook/builder-vite': '8.5.0-alpha.20', - '@storybook/builder-webpack5': '8.5.0-alpha.20', - '@storybook/core': '8.5.0-alpha.20', - '@storybook/builder-manager': '8.5.0-alpha.20', - '@storybook/channels': '8.5.0-alpha.20', - '@storybook/client-logger': '8.5.0-alpha.20', - '@storybook/components': '8.5.0-alpha.20', - '@storybook/core-common': '8.5.0-alpha.20', - '@storybook/core-events': '8.5.0-alpha.20', - '@storybook/core-server': '8.5.0-alpha.20', - '@storybook/csf-tools': '8.5.0-alpha.20', - '@storybook/docs-tools': '8.5.0-alpha.20', - '@storybook/manager': '8.5.0-alpha.20', - '@storybook/manager-api': '8.5.0-alpha.20', - '@storybook/node-logger': '8.5.0-alpha.20', - '@storybook/preview': '8.5.0-alpha.20', - '@storybook/preview-api': '8.5.0-alpha.20', - '@storybook/router': '8.5.0-alpha.20', - '@storybook/telemetry': '8.5.0-alpha.20', - '@storybook/theming': '8.5.0-alpha.20', - '@storybook/types': '8.5.0-alpha.20', - '@storybook/angular': '8.5.0-alpha.20', - '@storybook/ember': '8.5.0-alpha.20', - '@storybook/experimental-nextjs-vite': '8.5.0-alpha.20', - '@storybook/html-vite': '8.5.0-alpha.20', - '@storybook/html-webpack5': '8.5.0-alpha.20', - '@storybook/nextjs': '8.5.0-alpha.20', - '@storybook/preact-vite': '8.5.0-alpha.20', - '@storybook/preact-webpack5': '8.5.0-alpha.20', - '@storybook/react-native-web-vite': '8.5.0-alpha.20', - '@storybook/react-vite': '8.5.0-alpha.20', - '@storybook/react-webpack5': '8.5.0-alpha.20', - '@storybook/server-webpack5': '8.5.0-alpha.20', - '@storybook/svelte-vite': '8.5.0-alpha.20', - '@storybook/svelte-webpack5': '8.5.0-alpha.20', - '@storybook/sveltekit': '8.5.0-alpha.20', - '@storybook/vue3-vite': '8.5.0-alpha.20', - '@storybook/vue3-webpack5': '8.5.0-alpha.20', - '@storybook/web-components-vite': '8.5.0-alpha.20', - '@storybook/web-components-webpack5': '8.5.0-alpha.20', - '@storybook/blocks': '8.5.0-alpha.20', - storybook: '8.5.0-alpha.20', - sb: '8.5.0-alpha.20', - '@storybook/cli': '8.5.0-alpha.20', - '@storybook/codemod': '8.5.0-alpha.20', - '@storybook/core-webpack': '8.5.0-alpha.20', - 'create-storybook': '8.5.0-alpha.20', - '@storybook/csf-plugin': '8.5.0-alpha.20', - '@storybook/instrumenter': '8.5.0-alpha.20', - '@storybook/react-dom-shim': '8.5.0-alpha.20', - '@storybook/source-loader': '8.5.0-alpha.20', - '@storybook/test': '8.5.0-alpha.20', - '@storybook/preset-create-react-app': '8.5.0-alpha.20', - '@storybook/preset-html-webpack': '8.5.0-alpha.20', - '@storybook/preset-preact-webpack': '8.5.0-alpha.20', - '@storybook/preset-react-webpack': '8.5.0-alpha.20', - '@storybook/preset-server-webpack': '8.5.0-alpha.20', - '@storybook/preset-svelte-webpack': '8.5.0-alpha.20', - '@storybook/preset-vue3-webpack': '8.5.0-alpha.20', - '@storybook/html': '8.5.0-alpha.20', - '@storybook/preact': '8.5.0-alpha.20', - '@storybook/react': '8.5.0-alpha.20', - '@storybook/server': '8.5.0-alpha.20', - '@storybook/svelte': '8.5.0-alpha.20', - '@storybook/vue3': '8.5.0-alpha.20', - '@storybook/web-components': '8.5.0-alpha.20', + '@storybook/addon-a11y': '8.5.0-beta.5', + '@storybook/addon-actions': '8.5.0-beta.5', + '@storybook/addon-backgrounds': '8.5.0-beta.5', + '@storybook/addon-controls': '8.5.0-beta.5', + '@storybook/addon-docs': '8.5.0-beta.5', + '@storybook/addon-essentials': '8.5.0-beta.5', + '@storybook/addon-mdx-gfm': '8.5.0-beta.5', + '@storybook/addon-highlight': '8.5.0-beta.5', + '@storybook/addon-interactions': '8.5.0-beta.5', + '@storybook/addon-jest': '8.5.0-beta.5', + '@storybook/addon-links': '8.5.0-beta.5', + '@storybook/addon-measure': '8.5.0-beta.5', + '@storybook/addon-onboarding': '8.5.0-beta.5', + '@storybook/addon-outline': '8.5.0-beta.5', + '@storybook/addon-storysource': '8.5.0-beta.5', + '@storybook/experimental-addon-test': '8.5.0-beta.5', + '@storybook/addon-themes': '8.5.0-beta.5', + '@storybook/addon-toolbars': '8.5.0-beta.5', + '@storybook/addon-viewport': '8.5.0-beta.5', + '@storybook/builder-vite': '8.5.0-beta.5', + '@storybook/builder-webpack5': '8.5.0-beta.5', + '@storybook/core': '8.5.0-beta.5', + '@storybook/builder-manager': '8.5.0-beta.5', + '@storybook/channels': '8.5.0-beta.5', + '@storybook/client-logger': '8.5.0-beta.5', + '@storybook/components': '8.5.0-beta.5', + '@storybook/core-common': '8.5.0-beta.5', + '@storybook/core-events': '8.5.0-beta.5', + '@storybook/core-server': '8.5.0-beta.5', + '@storybook/csf-tools': '8.5.0-beta.5', + '@storybook/docs-tools': '8.5.0-beta.5', + '@storybook/manager': '8.5.0-beta.5', + '@storybook/manager-api': '8.5.0-beta.5', + '@storybook/node-logger': '8.5.0-beta.5', + '@storybook/preview': '8.5.0-beta.5', + '@storybook/preview-api': '8.5.0-beta.5', + '@storybook/router': '8.5.0-beta.5', + '@storybook/telemetry': '8.5.0-beta.5', + '@storybook/theming': '8.5.0-beta.5', + '@storybook/types': '8.5.0-beta.5', + '@storybook/angular': '8.5.0-beta.5', + '@storybook/ember': '8.5.0-beta.5', + '@storybook/experimental-nextjs-vite': '8.5.0-beta.5', + '@storybook/html-vite': '8.5.0-beta.5', + '@storybook/html-webpack5': '8.5.0-beta.5', + '@storybook/nextjs': '8.5.0-beta.5', + '@storybook/preact-vite': '8.5.0-beta.5', + '@storybook/preact-webpack5': '8.5.0-beta.5', + '@storybook/react-native-web-vite': '8.5.0-beta.5', + '@storybook/react-vite': '8.5.0-beta.5', + '@storybook/react-webpack5': '8.5.0-beta.5', + '@storybook/server-webpack5': '8.5.0-beta.5', + '@storybook/svelte-vite': '8.5.0-beta.5', + '@storybook/svelte-webpack5': '8.5.0-beta.5', + '@storybook/sveltekit': '8.5.0-beta.5', + '@storybook/vue3-vite': '8.5.0-beta.5', + '@storybook/vue3-webpack5': '8.5.0-beta.5', + '@storybook/web-components-vite': '8.5.0-beta.5', + '@storybook/web-components-webpack5': '8.5.0-beta.5', + '@storybook/blocks': '8.5.0-beta.5', + storybook: '8.5.0-beta.5', + sb: '8.5.0-beta.5', + '@storybook/cli': '8.5.0-beta.5', + '@storybook/codemod': '8.5.0-beta.5', + '@storybook/core-webpack': '8.5.0-beta.5', + 'create-storybook': '8.5.0-beta.5', + '@storybook/csf-plugin': '8.5.0-beta.5', + '@storybook/instrumenter': '8.5.0-beta.5', + '@storybook/react-dom-shim': '8.5.0-beta.5', + '@storybook/source-loader': '8.5.0-beta.5', + '@storybook/test': '8.5.0-beta.5', + '@storybook/preset-create-react-app': '8.5.0-beta.5', + '@storybook/preset-html-webpack': '8.5.0-beta.5', + '@storybook/preset-preact-webpack': '8.5.0-beta.5', + '@storybook/preset-react-webpack': '8.5.0-beta.5', + '@storybook/preset-server-webpack': '8.5.0-beta.5', + '@storybook/preset-svelte-webpack': '8.5.0-beta.5', + '@storybook/preset-vue3-webpack': '8.5.0-beta.5', + '@storybook/html': '8.5.0-beta.5', + '@storybook/preact': '8.5.0-beta.5', + '@storybook/react': '8.5.0-beta.5', + '@storybook/server': '8.5.0-beta.5', + '@storybook/svelte': '8.5.0-beta.5', + '@storybook/vue3': '8.5.0-beta.5', + '@storybook/web-components': '8.5.0-beta.5', }; diff --git a/code/core/src/components/components/Loader/Loader.tsx b/code/core/src/components/components/Loader/Loader.tsx index 26d01ad3f05..dace42a9b35 100644 --- a/code/core/src/components/components/Loader/Loader.tsx +++ b/code/core/src/components/components/Loader/Loader.tsx @@ -63,7 +63,7 @@ const ProgressBar = styled.div(({ theme }) => ({ const ProgressMessage = styled.div(({ theme }) => ({ minHeight: '2em', fontSize: `${theme.typography.size.s1}px`, - color: theme.barTextColor, + color: theme.textMutedColor, })); const ErrorIcon = styled(LightningOffIcon)(({ theme }) => ({ diff --git a/code/core/src/components/components/tooltip/ListItem.tsx b/code/core/src/components/components/tooltip/ListItem.tsx index 2a93287af50..10e513cc1b6 100644 --- a/code/core/src/components/components/tooltip/ListItem.tsx +++ b/code/core/src/components/components/tooltip/ListItem.tsx @@ -40,7 +40,7 @@ const Title = styled(({ active, loading, disabled, ...rest }: TitleProps) => disabled ? { - color: transparentize(0.7, theme.color.defaultText), + color: theme.textMutedColor, } : {} ); diff --git a/code/core/src/manager-api/lib/stories.test.ts b/code/core/src/manager-api/lib/stories.test.ts index 76aaadd7f99..3f0cbe63b5b 100644 --- a/code/core/src/manager-api/lib/stories.test.ts +++ b/code/core/src/manager-api/lib/stories.test.ts @@ -2,8 +2,10 @@ import { describe, expect, it } from 'vitest'; import type { API_PreparedStoryIndex, StoryIndexV2, StoryIndexV3 } from '@storybook/core/types'; +import type { State } from '../root'; import { mockEntries } from '../tests/mockStoriesEntries'; import { + transformStoryIndexToStoriesHash, transformStoryIndexV2toV3, transformStoryIndexV3toV4, transformStoryIndexV4toV5, @@ -216,3 +218,60 @@ describe('transformStoryIndexV4toV5', () => { `); }); }); + +describe('transformStoryIndexToStoriesHash', () => { + it('does not apply filters to failing stories', () => { + // Arrange - set up an index with two stories, one of which has a failing status + const indexV5: API_PreparedStoryIndex = { + v: 5, + entries: { + '1': { + id: '1', + type: 'story', + title: 'Story 1', + name: 'Story 1', + importPath: './path/to/story-1.ts', + parameters: {}, + tags: [], + }, + '2': { + id: '2', + type: 'story', + title: 'Story 2', + name: 'Story 2', + importPath: './path/to/story-2.ts', + parameters: {}, + tags: [], + }, + }, + }; + + const filters: State['filters'] = { + someFilter: () => false, + }; + + const status: State['status'] = { + '1': { someStatus: { status: 'error', title: 'broken', description: 'very bad' } }, + '2': { someStatus: { status: 'success', title: 'perfect', description: 'nice' } }, + }; + + const options = { + provider: { + getConfig: () => ({ sidebar: {} }), + } as any, + docsOptions: { docsMode: false }, + filters, + status, + }; + + // Act - transform the index to hashes + const result = transformStoryIndexToStoriesHash(indexV5, options); + + // Assert - the failing story is still present in the result, even though the filters remove all stories + expect(Object.keys(result)).toHaveLength(2); + expect(result['story-1']).toBeTruthy(); + expect(result['1']).toBeTruthy(); + expect(result['story-2']).toBeUndefined(); + expect(result['2']).toBeUndefined(); + }); +}); diff --git a/code/core/src/manager-api/lib/stories.ts b/code/core/src/manager-api/lib/stories.ts index 59b59f070a1..b38d3000053 100644 --- a/code/core/src/manager-api/lib/stories.ts +++ b/code/core/src/manager-api/lib/stories.ts @@ -192,11 +192,17 @@ export const transformStoryIndexToStoriesHash = ( const entryValues = Object.values(index.entries).filter((entry: any) => { let result = true; + // All stories with a failing status should always show up, regardless of the applied filters + const storyStatus = status[entry.id]; + if (Object.values(storyStatus ?? {}).some(({ status: s }) => s === 'error')) { + return result; + } + Object.values(filters).forEach((filter: any) => { if (result === false) { return; } - result = filter({ ...entry, status: status[entry.id] }); + result = filter({ ...entry, status: storyStatus }); }); return result; @@ -342,6 +348,7 @@ export const transformStoryIndexToStoriesHash = ( .reduce(addItem, orphanHash); }; +/** Now we need to patch in the existing prepared stories */ export const addPreparedStories = (newHash: API_IndexHash, oldHash?: API_IndexHash) => { if (!oldHash) { return newHash; diff --git a/code/core/src/manager-api/modules/layout.ts b/code/core/src/manager-api/modules/layout.ts index 468d51af1b8..6687c36b113 100644 --- a/code/core/src/manager-api/modules/layout.ts +++ b/code/core/src/manager-api/modules/layout.ts @@ -100,7 +100,7 @@ export const defaultLayoutState: SubState = { panelPosition: 'bottom', showTabs: true, }, - selectedPanel: undefined, + selectedPanel: 'chromaui/addon-visual-tests/panel', theme: create(), }; diff --git a/code/core/src/manager-api/modules/refs.ts b/code/core/src/manager-api/modules/refs.ts index 166c04786ad..cb9b8916ce2 100644 --- a/code/core/src/manager-api/modules/refs.ts +++ b/code/core/src/manager-api/modules/refs.ts @@ -179,7 +179,15 @@ export const init: ModuleFn = ( }, changeRefVersion: async (id, url) => { const { versions, title } = api.getRefs()[id]; - const ref: API_SetRefData = { id, url, versions, title, index: {}, expanded: true }; + const ref: API_SetRefData = { + id, + url, + versions, + title, + index: {}, + filteredIndex: {}, + expanded: true, + }; await api.setRef(id, { ...ref, type: 'unknown' }, false); await api.checkRef(ref); @@ -292,6 +300,7 @@ export const init: ModuleFn = ( // eslint-disable-next-line @typescript-eslint/naming-convention let internal_index: StoryIndex | undefined; let index: API_IndexHash | undefined; + let filteredIndex: API_IndexHash | undefined; const { filters } = store.getState(); const { storyMapper = defaultStoryMapper } = provider.getConfig(); const ref = api.getRefs()[id]; @@ -304,19 +313,28 @@ export const init: ModuleFn = ( : storyIndex; // @ts-expect-error (could be undefined) - index = transformStoryIndexToStoriesHash(storyIndex, { + filteredIndex = transformStoryIndexToStoriesHash(storyIndex, { provider, docsOptions, filters, status: {}, }); + // @ts-expect-error (could be undefined) + index = transformStoryIndexToStoriesHash(storyIndex, { + provider, + docsOptions, + filters: {}, + status: {}, + }); } if (index) { index = addRefIds(index, ref); } - - await api.updateRef(id, { ...ref, ...rest, index, internal_index }); + if (filteredIndex) { + filteredIndex = addRefIds(filteredIndex, ref); + } + await api.updateRef(id, { ...ref, ...rest, index, filteredIndex, internal_index }); }, updateRef: async (id, data) => { diff --git a/code/core/src/manager-api/modules/stories.ts b/code/core/src/manager-api/modules/stories.ts index 3c5aac769c6..fa295bc2056 100644 --- a/code/core/src/manager-api/modules/stories.ts +++ b/code/core/src/manager-api/modules/stories.ts @@ -568,41 +568,61 @@ export const init: ModuleFn = ({ // The story index we receive on fetchStoryIndex is not, but all the prepared fields are optional // so we can cast one to the other easily enough setIndex: async (input) => { - const { index: oldHash, status, filters } = store.getState(); - const newHash = transformStoryIndexToStoriesHash(input, { + const { filteredIndex: oldFilteredHash, index: oldHash, status, filters } = store.getState(); + const newFilteredHash = transformStoryIndexToStoriesHash(input, { provider, docsOptions, status, filters, }); + const newHash = transformStoryIndexToStoriesHash(input, { + provider, + docsOptions, + status, + filters: {}, + }); - // Now we need to patch in the existing prepared stories - const output = addPreparedStories(newHash, oldHash); - - await store.setState({ internal_index: input, index: output, indexError: undefined }); + await store.setState({ + internal_index: input, + filteredIndex: addPreparedStories(newFilteredHash, oldFilteredHash), + index: addPreparedStories(newHash, oldHash), + indexError: undefined, + }); }, + // FIXME: is there a bug where filtered stories get added back in on updateStory??? updateStory: async ( storyId: StoryId, update: StoryUpdate, ref?: API_ComposedRef ): Promise => { if (!ref) { - const { index } = store.getState(); - if (!index) { - return; + const { index, filteredIndex } = store.getState(); + if (index) { + index[storyId] = { + ...index[storyId], + ...update, + } as API_StoryEntry; + } + if (filteredIndex) { + filteredIndex[storyId] = { + ...filteredIndex[storyId], + ...update, + } as API_StoryEntry; + } + if (index || filteredIndex) { + await store.setState({ index, filteredIndex }); } - index[storyId] = { - ...index[storyId], - ...update, - } as API_StoryEntry; - await store.setState({ index }); } else { - const { id: refId, index }: any = ref; + const { id: refId, index, filteredIndex }: any = ref; index[storyId] = { ...index[storyId], ...update, } as API_StoryEntry; - await fullAPI.updateRef(refId, { index }); + filteredIndex[storyId] = { + ...filteredIndex[storyId], + ...update, + } as API_StoryEntry; + await fullAPI.updateRef(refId, { index, filteredIndex }); } }, updateDocs: async ( @@ -611,22 +631,33 @@ export const init: ModuleFn = ({ ref?: API_ComposedRef ): Promise => { if (!ref) { - const { index } = store.getState(); - if (!index) { - return; + const { index, filteredIndex } = store.getState(); + if (index) { + index[docsId] = { + ...index[docsId], + ...update, + } as API_DocsEntry; + } + if (filteredIndex) { + filteredIndex[docsId] = { + ...filteredIndex[docsId], + ...update, + } as API_DocsEntry; + } + if (index || filteredIndex) { + await store.setState({ index, filteredIndex }); } - index[docsId] = { - ...index[docsId], - ...update, - } as API_DocsEntry; - await store.setState({ index }); } else { - const { id: refId, index }: any = ref; + const { id: refId, index, filteredIndex }: any = ref; index[docsId] = { ...index[docsId], ...update, } as API_DocsEntry; - await fullAPI.updateRef(refId, { index }); + filteredIndex[docsId] = { + ...filteredIndex[docsId], + ...update, + } as API_DocsEntry; + await fullAPI.updateRef(refId, { index, filteredIndex }); } }, setPreviewInitialized: async (ref) => { diff --git a/code/core/src/manager-api/root.tsx b/code/core/src/manager-api/root.tsx index c2784329ab8..0889a560c8a 100644 --- a/code/core/src/manager-api/root.tsx +++ b/code/core/src/manager-api/root.tsx @@ -41,7 +41,7 @@ import { STORY_CHANGED, } from '@storybook/core/core-events'; -import { mergeWith } from 'es-toolkit'; +import { isEqual } from 'es-toolkit'; import { createContext } from './context'; import getInitialState from './initial-state'; @@ -219,16 +219,9 @@ class ManagerProvider extends Component { } shouldComponentUpdate(nextProps: ManagerProviderProps, nextState: State): boolean { - const prevState = this.state; const prevProps = this.props; - - if (prevState !== nextState) { - return true; - } - if (prevProps.path !== nextProps.path) { - return true; - } - return false; + const prevState = this.state; + return prevProps.path !== nextProps.path || !isEqual(prevState, nextState); } initModules = () => { diff --git a/code/core/src/manager-api/tests/refs.test.ts b/code/core/src/manager-api/tests/refs.test.ts index c8d65baafc5..b9d39ee6a88 100644 --- a/code/core/src/manager-api/tests/refs.test.ts +++ b/code/core/src/manager-api/tests/refs.test.ts @@ -291,6 +291,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "indexError": { @@ -360,6 +361,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "indexError": { @@ -504,6 +506,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -522,6 +525,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -601,6 +605,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -682,6 +687,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -763,6 +769,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "internal_index": undefined, @@ -905,6 +912,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "internal_index": undefined, @@ -987,6 +995,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -1068,6 +1077,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -1227,18 +1237,20 @@ describe('Refs API', () => { }, }; + const transformOptions = { + provider: provider as any, + docsOptions: {}, + filters: {}, + status: {}, + }; const initialState: Partial = { refs: { fake: { id: 'fake', url: 'https://example.com', previewInitialized: true, - index: transformStoryIndexToStoriesHash(index, { - provider: provider as any, - docsOptions: {}, - filters: {}, - status: {}, - }), + index: transformStoryIndexToStoriesHash(index, transformOptions), + filteredIndex: transformStoryIndexToStoriesHash(index, transformOptions), internal_index: index, }, }, @@ -1261,10 +1273,10 @@ describe('Refs API', () => { await api.setRef('fake', { storyIndex: index }); - await expect(api.getRefs().fake.index).toEqual( + await expect(api.getRefs().fake.filteredIndex).toEqual( expect.objectContaining({ 'a--1': expect.anything() }) ); - await expect(api.getRefs().fake.index).not.toEqual( + await expect(api.getRefs().fake.filteredIndex).not.toEqual( expect.objectContaining({ 'a--2': expect.anything() }) ); }); diff --git a/code/core/src/manager-api/tests/stories.test.ts b/code/core/src/manager-api/tests/stories.test.ts index b652c87cd7a..6f480061d9e 100644 --- a/code/core/src/manager-api/tests/stories.test.ts +++ b/code/core/src/manager-api/tests/stories.test.ts @@ -765,10 +765,15 @@ describe('stories API', () => { source: '', sourceLocation: '', type: '', - ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } } as any, + ref: { + id: 'refId', + index: { 'a--1': { args: { a: 'b' } } }, + filteredIndex: { 'a--1': { args: { a: 'b' } } }, + } as any, }); provider.channel.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } }); expect(fullAPI.updateRef).toHaveBeenCalledWith('refId', { + filteredIndex: { 'a--1': { args: { foo: 'bar' } } }, index: { 'a--1': { args: { foo: 'bar' } } }, }); }); @@ -1539,6 +1544,7 @@ describe('stories API', () => { }) ); }); + it('updates state', async () => { const moduleArgs = createMockModuleArgs({}); const { api } = initStories(moduleArgs as unknown as ModuleArgs); @@ -1565,9 +1571,9 @@ describe('stories API', () => { await api.setIndex({ v: 5, entries: navigationEntries }); await api.experimental_setFilter('myCustomFilter', (item: any) => item.id.startsWith('a')); - const { index } = store.getState(); + const { filteredIndex } = store.getState(); - expect(index).toMatchInlineSnapshot(` + expect(filteredIndex).toMatchInlineSnapshot(` { "a": { "children": [ @@ -1624,7 +1630,7 @@ describe('stories API', () => { ); // empty, because there are no stories with status - expect(store.getState().index).toMatchInlineSnapshot('{}'); + expect(store.getState().filteredIndex).toMatchInlineSnapshot('{}'); // setting status should update the index await api.experimental_updateStatus('a-addon-id', { @@ -1636,7 +1642,7 @@ describe('stories API', () => { 'a--2': { status: 'success', title: 'a addon title', description: '' }, }); - expect(store.getState().index).toMatchInlineSnapshot(` + expect(store.getState().filteredIndex).toMatchInlineSnapshot(` { "a": { "children": [ @@ -1676,9 +1682,9 @@ describe('stories API', () => { await api.setIndex({ v: 5, entries: navigationEntries }); - const { index } = store.getState(); + const { filteredIndex } = store.getState(); - expect(index).toMatchInlineSnapshot(` + expect(filteredIndex).toMatchInlineSnapshot(` { "a": { "children": [ diff --git a/code/core/src/manager-api/version.ts b/code/core/src/manager-api/version.ts index 0e6208fa11f..a769bd79992 100644 --- a/code/core/src/manager-api/version.ts +++ b/code/core/src/manager-api/version.ts @@ -1 +1 @@ -export const version = '8.5.0-alpha.20'; +export const version = '8.5.0-beta.5'; diff --git a/code/core/src/manager/components/panel/Panel.tsx b/code/core/src/manager/components/panel/Panel.tsx index 701cd94925e..dc80c20949d 100644 --- a/code/core/src/manager/components/panel/Panel.tsx +++ b/code/core/src/manager/components/panel/Panel.tsx @@ -60,7 +60,7 @@ export const AddonPanel = React.memo<{ return ( ({ const isActive = rp.path.includes(`tab=${tab.id}`); return ( { rp.applyQueryParams({ tab: tabIdToApply }); @@ -146,7 +146,7 @@ export const ToolbarComp = React.memo(function ToolbarComp({ {tabs.map((tab, index) => { return ( { api.applyQueryParams({ tab: tab.id === 'canvas' ? undefined : tab.id }); diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index a2d98eab6a8..ac4aeb671d5 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -2,6 +2,7 @@ import type { ComponentProps, FC, SyntheticEvent } from 'react'; import React, { useMemo, useState } from 'react'; import { TooltipLinkList, WithTooltip } from '@storybook/core/components'; +import { styled } from '@storybook/core/theming'; import { type API_HashEntry, Addon_TypesEnum } from '@storybook/core/types'; import { EllipsisIcon } from '@storybook/icons'; @@ -18,6 +19,16 @@ const empty = { node: null, }; +const PositionedWithTooltip = styled(WithTooltip)({ + position: 'absolute', + right: 0, +}); + +const FloatingStatusButton = styled(StatusButton)({ + background: 'var(--tree-node-background-hover)', + boxShadow: '0 0 5px 5px var(--tree-node-background-hover)', +}); + export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) => { const [hoverCount, setHoverCount] = useState(0); const [isOpen, setIsOpen] = useState(false); @@ -63,7 +74,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) return { onMouseEnter: handlers.onMouseEnter, node: isRendered ? ( - } > - + - - + + ) : null, }; }, [context, handlers, isOpen, isRendered, links]); diff --git a/code/core/src/manager/components/sidebar/Explorer.stories.tsx b/code/core/src/manager/components/sidebar/Explorer.stories.tsx index 908ead2be9f..34adad0a32a 100644 --- a/code/core/src/manager/components/sidebar/Explorer.stories.tsx +++ b/code/core/src/manager/components/sidebar/Explorer.stories.tsx @@ -34,7 +34,7 @@ const simple: Record = { url: 'iframe.html', previewInitialized: true, // @ts-expect-error (invalid input) - index: mockDataset.withRoot, + filteredIndex: mockDataset.withRoot, }, }; @@ -47,7 +47,7 @@ const withRefs: Record = { previewInitialized: true, type: 'auto-inject', // @ts-expect-error (invalid input) - index: mockDataset.noRoot, + filteredIndex: mockDataset.noRoot, }, injected: { id: 'injected', @@ -56,7 +56,7 @@ const withRefs: Record = { previewInitialized: false, type: 'auto-inject', // @ts-expect-error (invalid input) - index: mockDataset.noRoot, + filteredIndex: mockDataset.noRoot, }, unknown: { id: 'unknown', @@ -65,7 +65,7 @@ const withRefs: Record = { previewInitialized: true, type: 'unknown', // @ts-expect-error (invalid input) - index: mockDataset.noRoot, + filteredIndex: mockDataset.noRoot, }, lazy: { id: 'lazy', @@ -74,7 +74,7 @@ const withRefs: Record = { previewInitialized: false, type: 'lazy', // @ts-expect-error (invalid input) - index: mockDataset.withRoot, + filteredIndex: mockDataset.withRoot, }, }; diff --git a/code/core/src/manager/components/sidebar/LegacyRender.tsx b/code/core/src/manager/components/sidebar/LegacyRender.tsx index 82bc83bd1b3..f8afa4317f7 100644 --- a/code/core/src/manager/components/sidebar/LegacyRender.tsx +++ b/code/core/src/manager/components/sidebar/LegacyRender.tsx @@ -32,7 +32,7 @@ const TitleWrapper = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ( const DescriptionWrapper = styled.div(({ theme }) => ({ fontSize: theme.typography.size.s1, - color: theme.barTextColor, + color: theme.textMutedColor, })); const Progress = styled(ProgressSpinner)({ diff --git a/code/core/src/manager/components/sidebar/Refs.stories.tsx b/code/core/src/manager/components/sidebar/Refs.stories.tsx index a042970bead..3b6e37efce5 100644 --- a/code/core/src/manager/components/sidebar/Refs.stories.tsx +++ b/code/core/src/manager/components/sidebar/Refs.stories.tsx @@ -37,11 +37,11 @@ export default { }; const { menu } = standardHeaderData; -const index = mockDataset.withRoot; +const filteredIndex = mockDataset.withRoot; const storyId = '1-12-121'; -export const simpleData = { menu, index, storyId }; -export const loadingData = { menu, index: {} }; +export const simpleData = { menu, filteredIndex, storyId }; +export const loadingData = { menu, filteredIndex: {} }; // @ts-expect-error (non strict) const indexError: Error = (() => { @@ -60,14 +60,14 @@ const refs: Record = { previewInitialized: false, type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, }, empty: { id: 'empty', title: 'It is empty because no stories were loaded', url: 'https://example.com', type: 'lazy', - index: {}, + filteredIndex: {}, previewInitialized: false, }, startInjected_unknown: { @@ -77,7 +77,7 @@ const refs: Record = { type: 'unknown', previewInitialized: false, // @ts-expect-error (invalid input) - index, + filteredIndex, }, startInjected_loading: { id: 'startInjected_loading', @@ -86,7 +86,7 @@ const refs: Record = { type: 'auto-inject', previewInitialized: false, // @ts-expect-error (invalid input) - index, + filteredIndex, }, startInjected_ready: { id: 'startInjected_ready', @@ -95,7 +95,7 @@ const refs: Record = { type: 'auto-inject', previewInitialized: true, // @ts-expect-error (invalid input) - index, + filteredIndex, }, versions: { id: 'versions', @@ -103,7 +103,7 @@ const refs: Record = { url: 'https://example.com', type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, versions: { '1.0.0': 'https://example.com/v1', '2.0.0': 'https://example.com' }, previewInitialized: true, }, @@ -113,7 +113,7 @@ const refs: Record = { url: 'https://example.com', type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, versions: { '1.0.0': 'https://example.com/v1', '2.0.0': 'https://example.com/v2' }, previewInitialized: true, }, @@ -138,7 +138,7 @@ const refs: Record = { title: 'This storybook has a very very long name for some reason', url: 'https://example.com', // @ts-expect-error (invalid input) - index, + filteredIndex, type: 'lazy', versions: { '111.111.888-new': 'https://example.com/new', @@ -154,7 +154,7 @@ const refs: Record = { previewInitialized: false, type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, }, }; diff --git a/code/core/src/manager/components/sidebar/Refs.tsx b/code/core/src/manager/components/sidebar/Refs.tsx index 7412a627e88..b4d2a79ca35 100644 --- a/code/core/src/manager/components/sidebar/Refs.tsx +++ b/code/core/src/manager/components/sidebar/Refs.tsx @@ -81,7 +81,7 @@ export const Ref: FC = React. const { docsOptions } = useStorybookState(); const api = useStorybookApi(); const { - index, + filteredIndex: index, id: refId, title = refId, isLoading: isLoadingMain, diff --git a/code/core/src/manager/components/sidebar/SearchResults.tsx b/code/core/src/manager/components/sidebar/SearchResults.tsx index 7573befe34a..b7689b7d64d 100644 --- a/code/core/src/manager/components/sidebar/SearchResults.tsx +++ b/code/core/src/manager/components/sidebar/SearchResults.tsx @@ -70,7 +70,7 @@ const NoResults = styled.div(({ theme }) => ({ lineHeight: `18px`, color: theme.color.defaultText, small: { - color: theme.barTextColor, + color: theme.textMutedColor, fontSize: `${theme.typography.size.s1}px`, }, })); diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx index 53f22957f17..0e2df01f6ae 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx @@ -111,7 +111,7 @@ const refs: Record = { title: 'This is a ref', url: 'https://example.com', type: 'lazy', - index, + filteredIndex: index, previewInitialized: true, }, }; @@ -123,7 +123,7 @@ const refsError = { optimized: { ...refs.optimized, // @ts-expect-error (non strict) - index: undefined as IndexHash, + filteredIndex: undefined as IndexHash, indexError, }, }; @@ -132,7 +132,7 @@ const refsEmpty = { optimized: { ...refs.optimized, // type: 'auto-inject', - index: {} as IndexHash, + filteredIndex: {} as IndexHash, }, }; diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index d4784cc8c56..dfec7fba782 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -93,6 +93,7 @@ const useCombination = ( () => ({ [DEFAULT_REF_ID]: { index, + filteredIndex: index, indexError, previewInitialized, status, diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index 37a872810a6..38efdae4f68 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -1,7 +1,7 @@ -import React, { Fragment, useEffect, useRef, useState } from 'react'; +import React, { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { styled } from '@storybook/core/theming'; -import { type API_FilterFunction, type API_StatusValue } from '@storybook/core/types'; +import { type API_FilterFunction } from '@storybook/core/types'; import { TESTING_MODULE_CRASH_REPORT, @@ -119,7 +119,8 @@ export const SidebarBottomBase = ({ api.experimental_setFilter('sidebar-bottom-filter', filter); }, [api, hasWarnings, hasErrors, warningsActive, errorsActive]); - useEffect(() => { + // Register listeners before the first render + useLayoutEffect(() => { const onCrashReport = ({ providerId, ...details }: TestingModuleCrashReportPayload) => { api.updateTestProviderState(providerId, { error: { name: 'Crashed!', message: details.error.message }, diff --git a/code/core/src/manager/components/sidebar/StatusButton.tsx b/code/core/src/manager/components/sidebar/StatusButton.tsx index 9d1b49998df..8002cc87ba3 100644 --- a/code/core/src/manager/components/sidebar/StatusButton.tsx +++ b/code/core/src/manager/components/sidebar/StatusButton.tsx @@ -3,7 +3,7 @@ import { styled } from '@storybook/core/theming'; import type { API_StatusValue } from '@storybook/types'; import type { Theme } from '@emotion/react'; -import { transparentize } from 'polished'; +import { darken, lighten, transparentize } from 'polished'; const withStatusColor = ({ theme, status }: { theme: Theme; status: API_StatusValue }) => { const defaultColor = @@ -43,6 +43,19 @@ export const StatusButton = styled(IconButton)<{ '&:hover': { color: theme.color.secondary, + background: + theme.base === 'dark' + ? darken(0.3, theme.color.secondary) + : lighten(0.4, theme.color.secondary), + }, + + '[data-selected="true"] &': { + background: theme.color.secondary, + boxShadow: `0 0 5px 5px ${theme.color.secondary}`, + + '&:hover': { + background: lighten(0.1, theme.color.secondary), + }, }, '&:focus': { diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index d08ab8ffc4d..cbfc1e330be 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -24,9 +24,10 @@ import type { StoryEntry, } from '@storybook/core/manager-api'; -import { transparentize } from 'polished'; +import { darken, lighten } from 'polished'; import type { Link } from '../../../components/components/tooltip/TooltipLinkList'; +import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants'; import { getGroupStatus, getHighestStatus, statusMapping } from '../../utils/status'; import { createId, @@ -66,7 +67,7 @@ const CollapseButton = styled.button(({ theme }) => ({ '&:hover, &:focus': { outline: 'none', - background: transparentize(0.93, theme.color.secondary), + background: 'var(--tree-node-background-hover)', }, })); @@ -79,9 +80,19 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({ background: 'transparent', minHeight: 28, borderRadius: 4, + overflow: 'hidden', + '--tree-node-background-hover': theme.background.content, + + [MEDIA_DESKTOP_BREAKPOINT]: { + '--tree-node-background-hover': theme.background.app, + }, '&:hover, &:focus': { - background: transparentize(0.93, theme.color.secondary), + '--tree-node-background-hover': + theme.base === 'dark' + ? darken(0.35, theme.color.secondary) + : lighten(0.45, theme.color.secondary), + background: 'var(--tree-node-background-hover)', outline: 'none', }, @@ -94,11 +105,11 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({ }, '& [data-displayed="on"] + *': { - display: 'none', + visibility: 'hidden', }, '&:hover [data-displayed="off"] + *': { - display: 'none', + visibility: 'hidden', }, '&[data-selected="true"]': { @@ -107,7 +118,8 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({ fontWeight: theme.typography.weight.bold, '&&:hover, &&:focus': { - background: theme.color.secondary, + '--tree-node-background-hover': theme.color.secondary, + background: 'var(--tree-node-background-hover)', }, svg: { color: theme.color.lightest }, }, diff --git a/code/core/src/manager/components/sidebar/useHighlighted.ts b/code/core/src/manager/components/sidebar/useHighlighted.ts index d0f06fc87aa..37701c7b1ee 100644 --- a/code/core/src/manager/components/sidebar/useHighlighted.ts +++ b/code/core/src/manager/components/sidebar/useHighlighted.ts @@ -22,6 +22,25 @@ export interface HighlightedProps { const fromSelection = (selection: Selection): Highlight => selection ? { itemId: selection.storyId, refId: selection.refId } : null; +const scrollToSelector = ( + selector: string, + options: { + containerRef?: RefObject; + center?: boolean; + attempts?: number; + delay?: number; + } = {}, + _attempt = 1 +) => { + const { containerRef, center = false, attempts = 3, delay = 500 } = options; + const element = (containerRef ? containerRef.current : document)?.querySelector(selector); + if (element) { + scrollIntoView(element, center); + } else if (_attempt <= attempts) { + setTimeout(scrollToSelector, delay, selector, options, _attempt + 1); + } +}; + export const useHighlighted = ({ containerRef, isLoading, @@ -65,14 +84,10 @@ export const useHighlighted = ({ const highlight = fromSelection(selected); updateHighlighted(highlight); if (highlight) { - const { itemId, refId } = highlight; - setTimeout(() => { - scrollIntoView( - // @ts-expect-error (non strict) - containerRef.current?.querySelector(`[data-item-id="${itemId}"][data-ref-id="${refId}"]`), - true // make sure it's clearly visible by centering it - ); - }, 0); + scrollToSelector(`[data-item-id="${highlight.itemId}"][data-ref-id="${highlight.refId}"]`, { + containerRef, + center: true, + }); } }, [containerRef, selected, updateHighlighted]); diff --git a/code/core/src/manager/container/Panel.tsx b/code/core/src/manager/container/Panel.tsx index c81e489d8f6..f8cc2877cef 100644 --- a/code/core/src/manager/container/Panel.tsx +++ b/code/core/src/manager/container/Panel.tsx @@ -32,6 +32,12 @@ const getPanels = (api: API) => { if (paramKey && parameters && parameters[paramKey] && parameters[paramKey].disable) { return; } + if ( + panel.disabled === true || + (typeof panel.disabled === 'function' && panel.disabled(parameters)) + ) { + return; + } filteredPanels[id] = panel; }); diff --git a/code/core/src/manager/container/Sidebar.tsx b/code/core/src/manager/container/Sidebar.tsx index bc05d1713b5..723d9989ac1 100755 --- a/code/core/src/manager/container/Sidebar.tsx +++ b/code/core/src/manager/container/Sidebar.tsx @@ -27,7 +27,7 @@ const Sidebar = React.memo(function Sideber({ onMenuClick }: SidebarProps) { // is actually the stories hash. We should fix this up and make it consistent. // eslint-disable-next-line @typescript-eslint/naming-convention internal_index, - index, + filteredIndex: index, status, indexError, previewInitialized, diff --git a/code/core/src/manager/utils/tree.ts b/code/core/src/manager/utils/tree.ts index 3002eb97a77..dfe6dfb4cf2 100644 --- a/code/core/src/manager/utils/tree.ts +++ b/code/core/src/manager/utils/tree.ts @@ -85,10 +85,14 @@ export const scrollIntoView = (element: Element, center = false) => { return; } const { top, bottom } = element.getBoundingClientRect(); - const isInView = - top >= 0 && bottom <= (globalWindow.innerHeight || document.documentElement.clientHeight); - - if (!isInView) { + if (!top || !bottom) { + return; + } + const bottomOffset = + document?.querySelector('#sidebar-bottom-wrapper')?.getBoundingClientRect().top || + globalWindow.innerHeight || + document.documentElement.clientHeight; + if (bottom > bottomOffset) { element.scrollIntoView({ block: center ? 'center' : 'nearest' }); } }; diff --git a/code/core/src/telemetry/exec-command-count-lines.test.ts b/code/core/src/telemetry/exec-command-count-lines.test.ts new file mode 100644 index 00000000000..eacfe9f7295 --- /dev/null +++ b/code/core/src/telemetry/exec-command-count-lines.test.ts @@ -0,0 +1,71 @@ +import type { Transform } from 'node:stream'; +import { PassThrough } from 'node:stream'; + +import { beforeEach, describe, expect, it, vitest } from 'vitest'; + +// eslint-disable-next-line depend/ban-dependencies +import { execaCommand as rawExecaCommand } from 'execa'; + +import { execCommandCountLines } from './exec-command-count-lines'; + +vitest.mock('execa'); + +const execaCommand = vitest.mocked(rawExecaCommand); +beforeEach(() => { + execaCommand.mockReset(); +}); + +type ExecaStreamer = typeof Promise & { + stdout: Transform; + kill: () => void; +}; + +function createExecaStreamer() { + let resolver: () => void; + const promiseLike: ExecaStreamer = new Promise((aResolver, aRejecter) => { + resolver = aResolver; + }) as any; + + promiseLike.stdout = new PassThrough(); + // @ts-expect-error technically it is invalid to use resolver "before" it is assigned (but not really) + promiseLike.kill = resolver; + return promiseLike; +} + +describe('execCommandCountLines', () => { + it('counts lines, many', async () => { + const streamer = createExecaStreamer(); + execaCommand.mockReturnValue(streamer as any); + + const promise = execCommandCountLines('some command'); + + streamer.stdout.write('First line\n'); + streamer.stdout.write('Second line\n'); + streamer.kill(); + + expect(await promise).toEqual(2); + }); + + it('counts lines, one', async () => { + const streamer = createExecaStreamer(); + execaCommand.mockReturnValue(streamer as any); + + const promise = execCommandCountLines('some command'); + + streamer.stdout.write('First line\n'); + streamer.kill(); + + expect(await promise).toEqual(1); + }); + + it('counts lines, none', async () => { + const streamer = createExecaStreamer(); + execaCommand.mockReturnValue(streamer as any); + + const promise = execCommandCountLines('some command'); + + streamer.kill(); + + expect(await promise).toEqual(0); + }); +}); diff --git a/code/core/src/telemetry/exec-command-count-lines.ts b/code/core/src/telemetry/exec-command-count-lines.ts new file mode 100644 index 00000000000..fdc4547ce46 --- /dev/null +++ b/code/core/src/telemetry/exec-command-count-lines.ts @@ -0,0 +1,35 @@ +import { createInterface } from 'node:readline'; + +// eslint-disable-next-line depend/ban-dependencies +import { execaCommand } from 'execa'; + +/** + * Execute a command in the local terminal and count the lines in the result + * + * @param command The command to execute. + * @param options Execa options + * @returns The number of lines the command returned + */ +export async function execCommandCountLines( + command: string, + options?: Parameters[1] +) { + const process = execaCommand(command, { shell: true, buffer: false, ...options }); + if (!process.stdout) { + // eslint-disable-next-line local-rules/no-uncategorized-errors + throw new Error('Unexpected missing stdout'); + } + + let lineCount = 0; + const rl = createInterface(process.stdout); + rl.on('line', () => { + lineCount += 1; + }); + + // If the process errors, this will throw + await process; + + rl.close(); + + return lineCount; +} diff --git a/code/core/src/telemetry/get-application-file-count.test.ts b/code/core/src/telemetry/get-application-file-count.test.ts new file mode 100644 index 00000000000..7fc57068914 --- /dev/null +++ b/code/core/src/telemetry/get-application-file-count.test.ts @@ -0,0 +1,14 @@ +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { getApplicationFilesCountUncached } from './get-application-file-count'; + +const mocksDir = join(__dirname, '..', '__mocks__'); + +describe('getApplicationFilesCount', () => { + it('should find files with correct names', async () => { + const files = await getApplicationFilesCountUncached(mocksDir); + expect(files).toMatchInlineSnapshot(`2`); + }); +}); diff --git a/code/core/src/telemetry/get-application-file-count.ts b/code/core/src/telemetry/get-application-file-count.ts new file mode 100644 index 00000000000..4f4807ddff0 --- /dev/null +++ b/code/core/src/telemetry/get-application-file-count.ts @@ -0,0 +1,32 @@ +import { sep } from 'node:path'; + +import { execCommandCountLines } from './exec-command-count-lines'; +import { runTelemetryOperation } from './run-telemetry-operation'; + +// We are looking for files with the word "page" or "screen" somewhere in them with these exts +const nameMatches = ['page', 'screen']; +const extensions = ['js', 'jsx', 'ts', 'tsx']; + +export const getApplicationFilesCountUncached = async (basePath: string) => { + const bothCasesNameMatches = nameMatches.flatMap((match) => [ + match, + [match[0].toUpperCase(), ...match.slice(1)].join(''), + ]); + + const globs = bothCasesNameMatches.flatMap((match) => + extensions.map((extension) => `"${basePath}${sep}*${match}*.${extension}"`) + ); + + try { + const command = `git ls-files -- ${globs.join(' ')}`; + return await execCommandCountLines(command); + } catch { + return undefined; + } +}; + +export const getApplicationFileCount = async (path: string) => { + return runTelemetryOperation('applicationFiles', async () => + getApplicationFilesCountUncached(path) + ); +}; diff --git a/code/core/src/telemetry/get-has-router-package.test.ts b/code/core/src/telemetry/get-has-router-package.test.ts new file mode 100644 index 00000000000..8504a5bc4d8 --- /dev/null +++ b/code/core/src/telemetry/get-has-router-package.test.ts @@ -0,0 +1,29 @@ +import { expect, it } from 'vitest'; + +import { getHasRouterPackage } from './get-has-router-package'; + +it('returns true if there is a routing package in package.json', () => { + expect( + getHasRouterPackage({ + dependencies: { + react: '^18', + 'react-dom': '^18', + 'react-router': '^6', + }, + }) + ).toBe(true); +}); + +it('returns false if there is a routing package in package.json dependencies', () => { + expect( + getHasRouterPackage({ + dependencies: { + react: '^18', + 'react-dom': '^18', + }, + devDependencies: { + 'react-router': '^6', + }, + }) + ).toBe(false); +}); diff --git a/code/core/src/telemetry/get-has-router-package.ts b/code/core/src/telemetry/get-has-router-package.ts new file mode 100644 index 00000000000..5873c3832d2 --- /dev/null +++ b/code/core/src/telemetry/get-has-router-package.ts @@ -0,0 +1,37 @@ +import type { PackageJson } from '../types'; + +const routerPackages = new Set([ + 'react-router', + 'react-router-dom', + 'remix', + '@tanstack/react-router', + 'expo-router', + '@reach/router', + 'react-easy-router', + '@remix-run/router', + 'wouter', + 'wouter-preact', + 'preact-router', + 'vue-router', + 'unplugin-vue-router', + '@angular/router', + '@solidjs/router', + + // metaframeworks that imply routing + 'next', + 'react-scripts', + 'gatsby', + 'nuxt', + '@sveltejs/kit', +]); + +/** + * @param packageJson The package JSON of the project + * @returns Boolean Does this project use a routing package? + */ +export function getHasRouterPackage(packageJson: PackageJson) { + // NOTE: we just check real dependencies; if it is in dev dependencies, it may just be an example + return Object.keys(packageJson?.dependencies ?? {}).some((depName) => + routerPackages.has(depName) + ); +} diff --git a/code/core/src/telemetry/get-portable-stories-usage.ts b/code/core/src/telemetry/get-portable-stories-usage.ts index cd9da7f4f58..0831b484ab6 100644 --- a/code/core/src/telemetry/get-portable-stories-usage.ts +++ b/code/core/src/telemetry/get-portable-stories-usage.ts @@ -1,37 +1,18 @@ -// eslint-disable-next-line depend/ban-dependencies -import { execaCommand } from 'execa'; - -import { createFileSystemCache, resolvePathInStorybookCache } from '../common'; - -const cache = createFileSystemCache({ - basePath: resolvePathInStorybookCache('portable-stories'), - ns: 'storybook', - ttl: 24 * 60 * 60 * 1000, // 24h -}); +import { execCommandCountLines } from './exec-command-count-lines'; +import { runTelemetryOperation } from './run-telemetry-operation'; export const getPortableStoriesFileCountUncached = async (path?: string) => { - const command = `git grep -l composeStor` + (path ? ` -- ${path}` : ''); - const { stdout } = await execaCommand(command, { - cwd: process.cwd(), - shell: true, - }); - - return stdout.split('\n').filter(Boolean).length; -}; - -const CACHE_KEY = 'portableStories'; -export const getPortableStoriesFileCount = async (path?: string) => { - let cached = await cache.get(CACHE_KEY); - if (!cached) { - try { - const count = await getPortableStoriesFileCountUncached(); - cached = { count }; - await cache.set(CACHE_KEY, cached); - } catch (err: any) { - // exit code 1 if no matches are found - const count = err.exitCode === 1 ? 0 : null; - cached = { count }; - } + try { + const command = `git grep -l composeStor` + (path ? ` -- ${path}` : ''); + return await execCommandCountLines(command); + } catch (err: any) { + // exit code 1 if no matches are found + return err.exitCode === 1 ? 0 : undefined; } - return cached.count; +}; + +export const getPortableStoriesFileCount = async (path?: string) => { + return runTelemetryOperation('portableStories', async () => + getPortableStoriesFileCountUncached(path) + ); }; diff --git a/code/core/src/telemetry/run-telemetry-operation.ts b/code/core/src/telemetry/run-telemetry-operation.ts new file mode 100644 index 00000000000..29d2aee6c72 --- /dev/null +++ b/code/core/src/telemetry/run-telemetry-operation.ts @@ -0,0 +1,25 @@ +import { createFileSystemCache, resolvePathInStorybookCache } from '../common'; + +const cache = createFileSystemCache({ + basePath: resolvePathInStorybookCache('telemetry'), + ns: 'storybook', + ttl: 24 * 60 * 60 * 1000, // 24h +}); + +/** + * Run an (expensive) operation, caching the result in a FS cache for 24 hours. + * + * NOTE: if the operation returns `undefined` the value will not be cached. Use this to indicate + * that the operation failed. + */ +export const runTelemetryOperation = async (cacheKey: string, operation: () => Promise) => { + let cached = await cache.get(cacheKey); + if (cached === undefined) { + cached = await operation(); + // Undefined indicates an error, setting isn't really valuable. + if (cached !== undefined) { + await cache.set(cacheKey, cached); + } + } + return cached; +}; diff --git a/code/core/src/telemetry/storybook-metadata.test.ts b/code/core/src/telemetry/storybook-metadata.test.ts index 06bf355c068..8d73ff7b437 100644 --- a/code/core/src/telemetry/storybook-metadata.test.ts +++ b/code/core/src/telemetry/storybook-metadata.test.ts @@ -12,6 +12,8 @@ const packageJsonMock: PackageJson = { version: 'x.x.x', }; +const packageJsonPath = process.cwd(); + const mainJsMock: StorybookConfig = { stories: [], }; @@ -126,6 +128,7 @@ describe('storybook-metadata', () => { it('should parse pnp paths for known frameworks', async () => { const unixResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -144,6 +147,7 @@ describe('storybook-metadata', () => { const windowsResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -164,6 +168,7 @@ describe('storybook-metadata', () => { it('should parse pnp paths for unknown frameworks', async () => { const unixResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -178,6 +183,7 @@ describe('storybook-metadata', () => { const windowsResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -198,6 +204,7 @@ describe('storybook-metadata', () => { const unixResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -215,6 +222,7 @@ describe('storybook-metadata', () => { cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue('C:\\Users\\foo\\my-project'); const windowsResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -232,6 +240,7 @@ describe('storybook-metadata', () => { it('should return frameworkOptions from mainjs', async () => { const reactResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -250,6 +259,7 @@ describe('storybook-metadata', () => { const angularResult = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: { @@ -279,6 +289,7 @@ describe('storybook-metadata', () => { 'storybook-addon-deprecated': 'x.x.z', }, } as PackageJson, + packageJsonPath, mainConfig: { ...mainJsMock, addons: [ @@ -319,6 +330,7 @@ describe('storybook-metadata', () => { const result = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, features, @@ -332,6 +344,7 @@ describe('storybook-metadata', () => { expect( await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, framework: '@storybook/react-vite', @@ -347,6 +360,7 @@ describe('storybook-metadata', () => { it('should return the number of refs', async () => { const res = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, refs: { @@ -361,6 +375,7 @@ describe('storybook-metadata', () => { it('only reports addon options for addon-essentials', async () => { const res = await computeStorybookMetadata({ packageJson: packageJsonMock, + packageJsonPath, mainConfig: { ...mainJsMock, addons: [ @@ -395,6 +410,7 @@ describe('storybook-metadata', () => { [metaFramework]: 'x.x.x', }, } as PackageJson, + packageJsonPath, mainConfig: mainJsMock, }); expect(res.metaFramework).toEqual({ diff --git a/code/core/src/telemetry/storybook-metadata.ts b/code/core/src/telemetry/storybook-metadata.ts index 75804b813fc..5258eef0ffb 100644 --- a/code/core/src/telemetry/storybook-metadata.ts +++ b/code/core/src/telemetry/storybook-metadata.ts @@ -1,3 +1,5 @@ +import { dirname } from 'node:path'; + import { getProjectRoot, getStorybookConfiguration, @@ -9,10 +11,12 @@ import type { PackageJson, StorybookConfig } from '@storybook/core/types'; import { readConfig } from '@storybook/core/csf-tools'; import { detect, getNpmVersion } from 'detect-package-manager'; -import { findPackage } from 'fd-package-json'; +import { findPackage, findPackagePath } from 'fd-package-json'; +import { getApplicationFileCount } from './get-application-file-count'; import { getChromaticVersionSpecifier } from './get-chromatic-version'; import { getFrameworkInfo } from './get-framework-info'; +import { getHasRouterPackage } from './get-has-router-package'; import { getMonorepoType } from './get-monorepo-type'; import { getPortableStoriesFileCount } from './get-portable-stories-usage'; import { getActualPackageVersion, getActualPackageVersions } from './package-json'; @@ -41,9 +45,11 @@ export const sanitizeAddonName = (name: string) => { // Analyze a combination of information from main.js and package.json // to provide telemetry over a Storybook project export const computeStorybookMetadata = async ({ + packageJsonPath, packageJson, mainConfig, }: { + packageJsonPath: string; packageJson: PackageJson; mainConfig: StorybookConfig & Record; }): Promise => { @@ -100,6 +106,8 @@ export const computeStorybookMetadata = async ({ ) ); + metadata.hasRouterPackage = getHasRouterPackage(packageJson); + const monorepoType = getMonorepoType(); if (monorepoType) { metadata.monorepo = monorepoType; @@ -209,11 +217,13 @@ export const computeStorybookMetadata = async ({ const storybookVersion = storybookPackages[storybookInfo.frameworkPackage]?.version; const portableStoriesFileCount = await getPortableStoriesFileCount(); + const applicationFileCount = await getApplicationFileCount(dirname(packageJsonPath)); return { ...metadata, ...frameworkInfo, portableStoriesFileCount, + applicationFileCount, storybookVersion, storybookVersionSpecifier: storybookInfo.version, language, @@ -223,13 +233,29 @@ export const computeStorybookMetadata = async ({ }; }; +async function getPackageJsonDetails() { + const packageJsonPath = await findPackagePath(process.cwd()); + if (packageJsonPath) { + return { + packageJsonPath, + packageJson: (await findPackage(packageJsonPath)) || {}, + }; + } + + // If we don't find a `package.json`, we assume it "would have" been in the current working directory + return { + packageJsonPath: process.cwd(), + packageJson: {}, + }; +} + let cachedMetadata: StorybookMetadata; export const getStorybookMetadata = async (_configDir?: string) => { if (cachedMetadata) { return cachedMetadata; } - const packageJson = (await findPackage(process.cwd())) || {}; + const { packageJson, packageJsonPath } = await getPackageJsonDetails(); // TODO: improve the way configDir is extracted, as a "storybook" script might not be present // Scenarios: // 1. user changed it to something else e.g. "storybook:dev" @@ -243,6 +269,6 @@ export const getStorybookMetadata = async (_configDir?: string) => { ) as string)) ?? '.storybook'; const mainConfig = await loadMainConfig({ configDir }); - cachedMetadata = await computeStorybookMetadata({ mainConfig, packageJson }); + cachedMetadata = await computeStorybookMetadata({ mainConfig, packageJson, packageJsonPath }); return cachedMetadata; }; diff --git a/code/core/src/telemetry/types.ts b/code/core/src/telemetry/types.ts index 757f5afc197..e43373e5e61 100644 --- a/code/core/src/telemetry/types.ts +++ b/code/core/src/telemetry/types.ts @@ -59,6 +59,7 @@ export type StorybookMetadata = { version: string; }; testPackages?: Record; + hasRouterPackage?: boolean; hasStorybookEslint?: boolean; hasStaticDirs?: boolean; hasCustomWebpack?: boolean; @@ -69,6 +70,7 @@ export type StorybookMetadata = { usesGlobals?: boolean; }; portableStoriesFileCount?: number; + applicationFileCount?: number; }; export interface Payload { diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index 47f5aec6412..b2c7b4ac2f3 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -5,7 +5,7 @@ import type { TestProviderConfig, TestingModuleProgressReportProgress } from '.. import type { RenderData as RouterData } from '../../router/types'; import type { ThemeVars } from '../../theming/types'; import type { API_SidebarOptions } from './api'; -import type { API_HashEntry, API_StatusState, API_StatusUpdate } from './api-stories'; +import type { API_HashEntry, API_StoryEntry } from './api-stories'; import type { Args, ArgsStoryFn as ArgsStoryFnForFramework, @@ -392,7 +392,7 @@ export interface Addon_BaseType { /** @unstable */ paramKey?: string; /** @unstable */ - disabled?: boolean; + disabled?: boolean | ((parameters: API_StoryEntry['parameters']) => boolean); /** @unstable */ hidden?: boolean; } diff --git a/code/core/src/types/modules/api.ts b/code/core/src/types/modules/api.ts index 30edbe36ad2..f5e2d6f3210 100644 --- a/code/core/src/types/modules/api.ts +++ b/code/core/src/types/modules/api.ts @@ -155,6 +155,7 @@ export type API_StoryMapper = (ref: API_ComposedRef, story: SetStoriesStory) => export interface API_LoadedRefData { index?: API_IndexHash; + filteredIndex?: API_IndexHash; indexError?: Error; previewInitialized: boolean; } @@ -180,6 +181,7 @@ export type API_ComposedRefUpdate = Partial< | 'type' | 'expanded' | 'index' + | 'filteredIndex' | 'versions' | 'loginUrl' | 'version' diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index 3254723fc4e..2b4624a0564 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/src/types/modules/frameworks.ts b/code/core/src/types/modules/frameworks.ts index e3e1b6383a7..8e0ec1a7eea 100644 --- a/code/core/src/types/modules/frameworks.ts +++ b/code/core/src/types/modules/frameworks.ts @@ -21,5 +21,6 @@ export type SupportedFrameworks = | 'web-components-webpack5' | 'qwik' | 'solid' + | 'nuxt' | 'react-rsbuild' | 'vue3-rsbuild'; diff --git a/code/core/src/types/modules/renderers.ts b/code/core/src/types/modules/renderers.ts index 4fcf0be99d8..e6fd0f650bf 100644 --- a/code/core/src/types/modules/renderers.ts +++ b/code/core/src/types/modules/renderers.ts @@ -11,4 +11,5 @@ export type SupportedRenderers = | 'html' | 'web-components' | 'server' - | 'solid'; + | 'solid' + | 'nuxt'; diff --git a/code/core/template/stories/rendering.stories.ts b/code/core/template/stories/rendering.stories.ts index b0d0a7b06a1..77e9432ef51 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/builder-manager/package.json b/code/deprecated/builder-manager/package.json index 62cda75bafd..e5118a8c957 100644 --- a/code/deprecated/builder-manager/package.json +++ b/code/deprecated/builder-manager/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-manager", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook manager builder", "keywords": [ "storybook" diff --git a/code/deprecated/channels/package.json b/code/deprecated/channels/package.json index f05e0f4b745..4ce43a5a47e 100644 --- a/code/deprecated/channels/package.json +++ b/code/deprecated/channels/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/channels", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/deprecated/client-logger/package.json b/code/deprecated/client-logger/package.json index d58ca7e6471..e92abdd99cf 100644 --- a/code/deprecated/client-logger/package.json +++ b/code/deprecated/client-logger/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/client-logger", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/deprecated/components/package.json b/code/deprecated/components/package.json index 94f77c38389..505500302c1 100644 --- a/code/deprecated/components/package.json +++ b/code/deprecated/components/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/components", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook Components", "keywords": [ "storybook" diff --git a/code/deprecated/core-common/package.json b/code/deprecated/core-common/package.json index 66994e0d8bd..c9b99bcbd50 100644 --- a/code/deprecated/core-common/package.json +++ b/code/deprecated/core-common/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-common", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/deprecated/core-events/package.json b/code/deprecated/core-events/package.json index 3bb7a8fb597..ca0f2220c5f 100644 --- a/code/deprecated/core-events/package.json +++ b/code/deprecated/core-events/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-events", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Event names used in storybook core", "keywords": [ "storybook" diff --git a/code/deprecated/core-server/package.json b/code/deprecated/core-server/package.json index e2a34f4e8af..f2583597b81 100644 --- a/code/deprecated/core-server/package.json +++ b/code/deprecated/core-server/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-server", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/deprecated/csf-tools/package.json b/code/deprecated/csf-tools/package.json index 5c83ffcbb11..d7e81cfebe0 100644 --- a/code/deprecated/csf-tools/package.json +++ b/code/deprecated/csf-tools/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/csf-tools", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Parse and manipulate CSF and Storybook config files", "keywords": [ "storybook" diff --git a/code/deprecated/docs-tools/package.json b/code/deprecated/docs-tools/package.json index 1c98b0f0d3e..e41e131d138 100644 --- a/code/deprecated/docs-tools/package.json +++ b/code/deprecated/docs-tools/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/docs-tools", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Shared utility functions for frameworks to implement docs", "keywords": [ "storybook" diff --git a/code/deprecated/manager-api/package.json b/code/deprecated/manager-api/package.json index ba3117ca023..b474990df91 100644 --- a/code/deprecated/manager-api/package.json +++ b/code/deprecated/manager-api/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/manager-api", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook Manager API & Context", "keywords": [ "storybook" diff --git a/code/deprecated/manager/package.json b/code/deprecated/manager/package.json index bb7eeb7188b..361f7902619 100644 --- a/code/deprecated/manager/package.json +++ b/code/deprecated/manager/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/manager", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook UI", "keywords": [ "storybook" diff --git a/code/deprecated/node-logger/package.json b/code/deprecated/node-logger/package.json index b1700e6e65c..cb464d5a7bc 100644 --- a/code/deprecated/node-logger/package.json +++ b/code/deprecated/node-logger/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/node-logger", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" @@ -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/deprecated/preview-api/package.json b/code/deprecated/preview-api/package.json index 6c23642f785..c6279096606 100644 --- a/code/deprecated/preview-api/package.json +++ b/code/deprecated/preview-api/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preview-api", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/deprecated/preview/package.json b/code/deprecated/preview/package.json index 37550e11099..9a8e640f2f5 100644 --- a/code/deprecated/preview/package.json +++ b/code/deprecated/preview/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preview", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/deprecated/router/package.json b/code/deprecated/router/package.json index 501562f1e6e..e73f2154fa5 100644 --- a/code/deprecated/router/package.json +++ b/code/deprecated/router/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/router", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook Router", "keywords": [ "storybook" diff --git a/code/deprecated/telemetry/package.json b/code/deprecated/telemetry/package.json index 6fe8cd1b0ac..cf98e902a38 100644 --- a/code/deprecated/telemetry/package.json +++ b/code/deprecated/telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/telemetry", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Telemetry logging for crash reports and usage statistics", "keywords": [ "storybook" diff --git a/code/deprecated/theming/package.json b/code/deprecated/theming/package.json index e91486a6beb..dfad0d3ac42 100644 --- a/code/deprecated/theming/package.json +++ b/code/deprecated/theming/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/theming", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook Components", "keywords": [ "storybook" diff --git a/code/deprecated/types/package.json b/code/deprecated/types/package.json index 214340eddaa..a10b2cf6770 100644 --- a/code/deprecated/types/package.json +++ b/code/deprecated/types/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/types", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Core Storybook TS Types", "keywords": [ "storybook" diff --git a/code/e2e-tests/addon-docs.spec.ts b/code/e2e-tests/addon-docs.spec.ts index 54c046a8aec..6d0542bf4b8 100644 --- a/code/e2e-tests/addon-docs.spec.ts +++ b/code/e2e-tests/addon-docs.spec.ts @@ -191,7 +191,7 @@ test.describe('addon-docs', () => { test('should resolve react to the correct version', async ({ page }) => { test.skip( - templateName?.includes('nextjs'), + templateName?.includes('nextjs') || templateName?.includes('nuxt'), 'TODO: remove this once sandboxes are synced (SOON!!)' ); // Arrange - Navigate to MDX docs diff --git a/code/e2e-tests/addon-onboarding.spec.ts b/code/e2e-tests/addon-onboarding.spec.ts new file mode 100644 index 00000000000..85181d8abf2 --- /dev/null +++ b/code/e2e-tests/addon-onboarding.spec.ts @@ -0,0 +1,53 @@ +import { expect, test } from '@playwright/test'; +import process from 'process'; + +import { SbPage } from './util'; + +const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001'; +const templateName = process.env.STORYBOOK_TEMPLATE_NAME || ''; +const type = process.env.STORYBOOK_TYPE || 'dev'; + +const supportsOnboarding = + templateName.includes('react') || + templateName.includes('vue3') || + templateName.includes('angular') || + templateName.includes('next'); + +test.describe('addon-onboarding', () => { + test.skip(type === 'build', `Skipping addon tests for production Storybooks`); + test.skip( + !supportsOnboarding, + `Skipping ${templateName}, which does not have addon-onboarding set up.` + ); + test('the onboarding flow', async ({ page }) => { + await page.goto(`${storybookUrl}/?path=/onboarding`); + const sbPage = new SbPage(page, expect); + await sbPage.waitUntilLoaded(); + + await expect(page.getByRole('heading', { name: 'Meet your new frontend' })).toBeVisible(); + await page.locator('#storybook-addon-onboarding').getByRole('button').click(); + + await expect(page.getByText('Interactive story playground')).toBeVisible(); + await page.getByLabel('Next').click(); + + await expect(page.getByText('Save your changes as a new')).toBeVisible(); + await page.getByLabel('Next').click(); + + await expect(page.getByRole('heading', { name: 'Create new story' })).toBeVisible(); + await page.getByPlaceholder('Story export name').click(); + + // this is needed because the e2e test will generate a new file in the system + // which we don't know of its location (it runs in different sandboxes) + // so we just create a random id to make it easier to run tests + const id = Math.random().toString(36).substring(7); + await page.getByPlaceholder('Story export name').fill('Test-' + id); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByText('You just added your first')).toBeVisible(); + await page.getByLabel('Last').click(); + + await expect( + sbPage.previewIframe().getByRole('heading', { name: 'Configure your project' }) + ).toBeVisible(); + }); +}); diff --git a/code/e2e-tests/composition.spec.ts b/code/e2e-tests/composition.spec.ts index 15bc6c4c4ed..ec5a96e5898 100644 --- a/code/e2e-tests/composition.spec.ts +++ b/code/e2e-tests/composition.spec.ts @@ -27,8 +27,12 @@ test.describe('composition', () => { await page.locator('[id="storybook\\@7\\.6\\.18_components-badge"]').click(); await expect( - page.locator('[id="storybook\\@7\\.6\\.18_components-badge--default"]') - ).toBeVisible(); + page + .locator('iframe[title="storybook-ref-storybook\\@7\\.6\\.18"]') + .contentFrame() + .locator('#storybook-root') + .getByText('Default') + ).toBeVisible({ timeout: 15000 }); // Expect composed stories `to be available in the search await page.getByPlaceholder('Find components').fill('Button'); diff --git a/code/e2e-tests/preview-api.spec.ts b/code/e2e-tests/preview-api.spec.ts index cfabaf89674..31b566027fd 100644 --- a/code/e2e-tests/preview-api.spec.ts +++ b/code/e2e-tests/preview-api.spec.ts @@ -65,6 +65,7 @@ test.describe('preview-api', () => { const root = sbPage.previewRoot(); + await sbPage.viewAddonPanel('Controls'); const labelControl = sbPage.page.locator('#control-label'); await expect(root.getByText('Loaded. Click me')).toBeVisible(); diff --git a/code/e2e-tests/util.ts b/code/e2e-tests/util.ts index 057dc91f715..d9faad22939 100644 --- a/code/e2e-tests/util.ts +++ b/code/e2e-tests/util.ts @@ -1,3 +1,4 @@ +/* eslint-disable local-rules/no-uncategorized-errors */ import { toId } from '@storybook/csf'; import type { Expect, Page } from '@playwright/test'; @@ -61,6 +62,7 @@ export class SbPage { await this.expect(selected).toHaveAttribute('data-selected', 'true'); await this.previewRoot(); + await this.waitUntilLoaded(); } async navigateToUnattachedDocs(title: string, name = 'docs') { @@ -80,7 +82,25 @@ export class SbPage { const selected = storyLink; await this.expect(selected).toHaveAttribute('data-selected', 'true'); - await this.previewRoot(); + await this.waitForStoryLoaded(); + } + + async waitForStoryLoaded() { + try { + const root = this.previewRoot(); + // Wait until there is at least one child (a story element) in the preview iframe + await root.locator(':scope > *').first().waitFor({ + state: 'attached', + timeout: 10000, + }); + } catch (error: any) { + if (error.name === 'TimeoutError') { + throw new Error( + 'The Storybook iframe did not have children within the specified timeout. Did the story load correctly?' + ); + } + throw error; + } } async waitUntilLoaded() { @@ -112,6 +132,8 @@ export class SbPage { const storyLoadingPage = root.locator('.sb-preparing-story'); await docsLoadingPage.waitFor({ state: 'hidden' }); await storyLoadingPage.waitFor({ state: 'hidden' }); + + await this.waitForStoryLoaded(); } previewIframe() { @@ -143,6 +165,30 @@ export class SbPage { getCanvasBodyElement() { return this.previewIframe().locator('body'); } + + // utility to try and decrease flake + async retryTimes( + fn: () => Promise, + options?: { + retries?: number; + delay?: number; + } + ): Promise { + let attempts = 0; + const { retries = 3, delay = 0 } = options || {}; + while (attempts < retries) { + try { + await fn(); + return; + } catch (error) { + attempts++; + if (attempts === retries) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } } const templateName: keyof typeof allTemplates = process.env.STORYBOOK_TEMPLATE_NAME || ('' as any); diff --git a/code/frameworks/angular/package.json b/code/frameworks/angular/package.json index 06a9b483458..2105d16d28b 100644 --- a/code/frameworks/angular/package.json +++ b/code/frameworks/angular/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/angular", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Angular: Develop Angular components in isolation with hot reloading.", "keywords": [ "storybook", diff --git a/code/frameworks/angular/src/builders/build-storybook/schema.json b/code/frameworks/angular/src/builders/build-storybook/schema.json index 6d976bf5dd0..77e455cdb1b 100644 --- a/code/frameworks/angular/src/builders/build-storybook/schema.json +++ b/code/frameworks/angular/src/builders/build-storybook/schema.json @@ -77,6 +77,11 @@ "description": "Write Webpack Stats JSON to disk", "default": false }, + "statsJson": { + "type": ["boolean", "string"], + "description": "Write stats JSON to disk", + "default": false + }, "previewUrl": { "type": "string", "description": "Disables the default storybook preview and lets you use your own" diff --git a/code/frameworks/angular/src/builders/start-storybook/schema.json b/code/frameworks/angular/src/builders/start-storybook/schema.json index 729c3ea5b72..64d53bd5481 100644 --- a/code/frameworks/angular/src/builders/start-storybook/schema.json +++ b/code/frameworks/angular/src/builders/start-storybook/schema.json @@ -139,6 +139,11 @@ "description": "Write Webpack Stats JSON to disk", "default": false }, + "statsJson": { + "type": ["boolean", "string"], + "description": "Write stats JSON to disk", + "default": false + }, "previewUrl": { "type": "string", "description": "Disables the default storybook preview and lets you use your own" diff --git a/code/frameworks/ember/package.json b/code/frameworks/ember/package.json index 18ff9618d20..bd1ae51f689 100644 --- a/code/frameworks/ember/package.json +++ b/code/frameworks/ember/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/ember", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Ember: Develop Ember Component in isolation with Hot Reloading.", "homepage": "https://github.com/storybookjs/storybook/tree/next/code/frameworks/ember", "bugs": { diff --git a/code/frameworks/experimental-nextjs-vite/package.json b/code/frameworks/experimental-nextjs-vite/package.json index 2fe79c99238..da9fce69ded 100644 --- a/code/frameworks/experimental-nextjs-vite/package.json +++ b/code/frameworks/experimental-nextjs-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/experimental-nextjs-vite", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Next.js and Vite", "keywords": [ "storybook", @@ -97,6 +97,7 @@ "dependencies": { "@storybook/builder-vite": "workspace:*", "@storybook/react": "workspace:*", + "@storybook/react-vite": "workspace:*", "@storybook/test": "workspace:*", "styled-jsx": "5.1.6", "vite-plugin-storybook-nextjs": "^1.1.0" diff --git a/code/frameworks/experimental-nextjs-vite/src/preset.ts b/code/frameworks/experimental-nextjs-vite/src/preset.ts index 0a725be3580..633f62a5dce 100644 --- a/code/frameworks/experimental-nextjs-vite/src/preset.ts +++ b/code/frameworks/experimental-nextjs-vite/src/preset.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import type { PresetProperty } from 'storybook/internal/types'; import type { StorybookConfigVite } from '@storybook/builder-vite'; +import { viteFinal as reactViteFinal } from '@storybook/react-vite/preset'; import { dirname, join } from 'path'; import vitePluginStorybookNextjs from 'vite-plugin-storybook-nextjs'; @@ -34,11 +35,13 @@ export const previewAnnotations: PresetProperty<'previewAnnotations'> = (entry = }; export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, options) => { - config.plugins = config.plugins || []; + const reactConfig = await reactViteFinal(config, options); + const { plugins = [] } = reactConfig; + const { nextConfigPath } = await options.presets.apply('frameworkOptions'); const nextDir = nextConfigPath ? path.dirname(nextConfigPath) : undefined; - config.plugins.push(vitePluginStorybookNextjs({ dir: nextDir })); + plugins.push(vitePluginStorybookNextjs({ dir: nextDir })); - return config; + return reactConfig; }; diff --git a/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx b/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx index 68e01381948..eec03995d26 100644 --- a/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx +++ b/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx @@ -44,8 +44,6 @@ function getSelectedParams(currentTree: FlightRouterState, params: Params = {}): continue; } - // Ensure catchAll and optional catchall are turned into an array - // Ensure catchAll and optional catchall are turned into an array const isCatchAll = isDynamicParameter && (segment[2] === 'c' || segment[2] === 'oc'); @@ -82,6 +80,16 @@ export const AppRouterProvider: React.FC @@ -106,10 +114,18 @@ export const AppRouterProvider: React.FC = v15.1.1 + parentTree: tree, + parentCacheNode: newLazyCacheNode, + // END url: pathname, + loading: null, }} > {children} diff --git a/code/frameworks/experimental-nextjs-vite/src/types.ts b/code/frameworks/experimental-nextjs-vite/src/types.ts index 8de91a4430d..0221787dccb 100644 --- a/code/frameworks/experimental-nextjs-vite/src/types.ts +++ b/code/frameworks/experimental-nextjs-vite/src/types.ts @@ -1,9 +1,7 @@ -import type { - CompatibleString, - StorybookConfig as StorybookConfigBase, -} from 'storybook/internal/types'; +import type { CompatibleString } from 'storybook/internal/types'; -import type { BuilderOptions, StorybookConfigVite } from '@storybook/builder-vite'; +import type { BuilderOptions } from '@storybook/builder-vite'; +import type { StorybookConfig as StorybookConfigReactVite } from '@storybook/react-vite'; type FrameworkName = CompatibleString<'@storybook/experimental-nextjs-vite'>; type BuilderName = CompatibleString<'@storybook/builder-vite'>; @@ -21,7 +19,7 @@ type StorybookConfigFramework = { name: FrameworkName; options: FrameworkOptions; }; - core?: StorybookConfigBase['core'] & { + core?: StorybookConfigReactVite['core'] & { builder?: | BuilderName | { @@ -32,9 +30,5 @@ type StorybookConfigFramework = { }; /** The interface for Storybook configuration in `main.ts` files. */ -export type StorybookConfig = Omit< - StorybookConfigBase, - keyof StorybookConfigVite | keyof StorybookConfigFramework -> & - StorybookConfigVite & - StorybookConfigFramework & {}; +export type StorybookConfig = Omit & + StorybookConfigFramework; diff --git a/code/frameworks/html-vite/package.json b/code/frameworks/html-vite/package.json index 02a11a32b86..08fb3adfa57 100644 --- a/code/frameworks/html-vite/package.json +++ b/code/frameworks/html-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html-vite", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for HTML and Vite: Develop HTML in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/html-webpack5/package.json b/code/frameworks/html-webpack5/package.json index 356198becd2..82f8536ca24 100644 --- a/code/frameworks/html-webpack5/package.json +++ b/code/frameworks/html-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html-webpack5", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for HTML: View HTML snippets in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index 88ccba6855a..964a6ff1441 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/nextjs", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Next.js", "keywords": [ "storybook", diff --git a/code/frameworks/nextjs/src/config/webpack.ts b/code/frameworks/nextjs/src/config/webpack.ts index 76edac25c81..4b10922c9df 100644 --- a/code/frameworks/nextjs/src/config/webpack.ts +++ b/code/frameworks/nextjs/src/config/webpack.ts @@ -1,8 +1,7 @@ import type { NextConfig } from 'next'; import type { Configuration as WebpackConfig } from 'webpack'; -import { DefinePlugin } from 'webpack'; -import { addScopedAlias, resolveNextConfig, setAlias } from '../utils'; +import { addScopedAlias, resolveNextConfig } from '../utils'; const tryResolve = (path: string) => { try { @@ -48,12 +47,15 @@ export const configureConfig = async ({ addScopedAlias(baseConfig, 'react-dom/server', 'next/dist/compiled/react-dom/server'); } - setupRuntimeConfig(baseConfig, nextConfig); + await setupRuntimeConfig(baseConfig, nextConfig); return nextConfig; }; -const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): void => { +const setupRuntimeConfig = async ( + baseConfig: WebpackConfig, + nextConfig: NextConfig +): Promise => { const definePluginConfig: Record = { // this mimics what nextjs does client side // https://github.com/vercel/next.js/blob/57702cb2a9a9dba4b552e0007c16449cf36cfb44/packages/next/client/index.tsx#L101 @@ -67,5 +69,7 @@ const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): definePluginConfig['process.env.__NEXT_NEW_LINK_BEHAVIOR'] = newNextLinkBehavior; - baseConfig.plugins?.push(new DefinePlugin(definePluginConfig)); + // Load DefinePlugin with a dynamic import to ensure that Next.js can first + // replace webpack with its own internal instance, and we get that here. + baseConfig.plugins?.push(new (await import('webpack')).default.DefinePlugin(definePluginConfig)); }; diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 3463910175e..6b90ae543a2 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -10,31 +10,29 @@ import type { ConfigItem, PluginItem, TransformOptions } from '@babel/core'; import { loadPartialConfig } from '@babel/core'; import semver from 'semver'; -import { configureAliases } from './aliases/webpack'; -import { configureBabelLoader } from './babel/loader'; import nextBabelPreset from './babel/preset'; -import { configureCompatibilityAliases } from './compatibility/compatibility-map'; import { configureConfig } from './config/webpack'; -import { configureCss } from './css/webpack'; -import { configureNextExportMocks } from './export-mocks/webpack'; -import { configureFastRefresh } from './fastRefresh/webpack'; import TransformFontImports from './font/babel'; -import { configureNextFont } from './font/webpack/configureNextFont'; -import { configureImages } from './images/webpack'; -import { configureImports } from './imports/webpack'; -import { configureNodePolyfills } from './nodePolyfills/webpack'; -import { configureRSC } from './rsc/webpack'; -import { configureStyledJsx } from './styledJsx/webpack'; -import { configureSWCLoader } from './swc/loader'; import type { FrameworkOptions, StorybookConfig } from './types'; -import { configureRuntimeNextjsVersionResolution, getNextjsVersion } from './utils'; export const addons: PresetProperty<'addons'> = [ dirname(require.resolve(join('@storybook/preset-react-webpack', 'package.json'))), ]; export const core: PresetProperty<'core'> = async (config, options) => { - const framework = await options.presets.apply('framework'); + const framework = await options.presets.apply('framework'); + + // Load the Next.js configuration before we need it in webpackFinal (below). + // This gives Next.js an opportunity to override some of webpack's internals + // (see next/dist/server/config-utils.js) before @storybook/builder-webpack5 + // starts to use it. Without this, webpack's file system cache (fsCache: true) + // does not work. + await configureConfig({ + // Pass in a dummy webpack config object for now, since we don't want to + // modify the real one yet. We pass in the real one in webpackFinal. + baseConfig: {}, + nextConfigPath: typeof framework === 'string' ? undefined : framework.options.nextConfigPath, + }); return { ...config, @@ -144,6 +142,22 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, nextConfigPath, }); + // Use dynamic imports to ensure these modules that use webpack load after + // Next.js has been configured (above), and has replaced webpack with its precompiled + // version. + const { configureNextFont } = await import('./font/webpack/configureNextFont'); + const { configureRuntimeNextjsVersionResolution, getNextjsVersion } = await import('./utils'); + const { configureImports } = await import('./imports/webpack'); + const { configureCss } = await import('./css/webpack'); + const { configureImages } = await import('./images/webpack'); + const { configureStyledJsx } = await import('./styledJsx/webpack'); + const { configureNodePolyfills } = await import('./nodePolyfills/webpack'); + const { configureAliases } = await import('./aliases/webpack'); + const { configureFastRefresh } = await import('./fastRefresh/webpack'); + const { configureRSC } = await import('./rsc/webpack'); + const { configureSWCLoader } = await import('./swc/loader'); + const { configureBabelLoader } = await import('./babel/loader'); + const babelRCPath = join(getProjectRoot(), '.babelrc'); const babelConfigPath = join(getProjectRoot(), 'babel.config.js'); const hasBabelConfig = existsSync(babelRCPath) || existsSync(babelConfigPath); diff --git a/code/frameworks/nextjs/src/routing/app-router-provider.tsx b/code/frameworks/nextjs/src/routing/app-router-provider.tsx index 36e980b1a5f..3f06a666864 100644 --- a/code/frameworks/nextjs/src/routing/app-router-provider.tsx +++ b/code/frameworks/nextjs/src/routing/app-router-provider.tsx @@ -80,6 +80,16 @@ export const AppRouterProvider: React.FC @@ -104,8 +114,16 @@ export const AppRouterProvider: React.FC = v15.1.1 + parentTree: tree, + parentCacheNode: newLazyCacheNode, + // END url: pathname, loading: null, }} diff --git a/code/frameworks/preact-vite/package.json b/code/frameworks/preact-vite/package.json index fb80969ea2a..ec95f3fcf6f 100644 --- a/code/frameworks/preact-vite/package.json +++ b/code/frameworks/preact-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact-vite", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Preact and Vite: Develop Preact components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/preact-webpack5/package.json b/code/frameworks/preact-webpack5/package.json index 158b8645be8..50c51eee007 100644 --- a/code/frameworks/preact-webpack5/package.json +++ b/code/frameworks/preact-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact-webpack5", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Preact: Develop Preact Component in isolation.", "keywords": [ "storybook" diff --git a/code/frameworks/react-native-web-vite/package.json b/code/frameworks/react-native-web-vite/package.json index 4b6c1143687..b7392768f46 100644 --- a/code/frameworks/react-native-web-vite/package.json +++ b/code/frameworks/react-native-web-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-native-web-vite", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Develop react-native components an isolated web environment with hot reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/react-native-web-vite/src/preset.ts b/code/frameworks/react-native-web-vite/src/preset.ts index 3ccd4240bf6..44e847171dd 100644 --- a/code/frameworks/react-native-web-vite/src/preset.ts +++ b/code/frameworks/react-native-web-vite/src/preset.ts @@ -1,4 +1,3 @@ -// @ts-expect-error FIXME import { viteFinal as reactViteFinal } from '@storybook/react-vite/preset'; import { esbuildFlowPlugin, flowPlugin } from '@bunchtogether/vite-plugin-flow'; diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json index 178ff614e54..2db52d15165 100644 --- a/code/frameworks/react-vite/package.json +++ b/code/frameworks/react-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-vite", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for React and Vite: Develop React components in isolation with Hot Reloading.", "keywords": [ "storybook" @@ -35,6 +35,16 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preset": [ + "dist/preset.d.ts" + ] + } + }, "files": [ "dist/**/*", "README.md", diff --git a/code/frameworks/react-vite/src/preset.ts b/code/frameworks/react-vite/src/preset.ts index cef1a270f33..a01721dadac 100644 --- a/code/frameworks/react-vite/src/preset.ts +++ b/code/frameworks/react-vite/src/preset.ts @@ -12,7 +12,7 @@ export const core: PresetProperty<'core'> = { renderer: getAbsolutePath('@storybook/react'), }; -export const viteFinal: StorybookConfig['viteFinal'] = async (config, { presets }) => { +export const viteFinal: NonNullable = async (config, { presets }) => { const { plugins = [] } = config; // Add docgen plugin diff --git a/code/frameworks/react-webpack5/package.json b/code/frameworks/react-webpack5/package.json index ff54c5fbe72..868da383102 100644 --- a/code/frameworks/react-webpack5/package.json +++ b/code/frameworks/react-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-webpack5", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for React: Develop React Component in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/react-webpack5/src/preset.ts b/code/frameworks/react-webpack5/src/preset.ts index 9e233459c10..08f870a2305 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/frameworks/server-webpack5/package.json b/code/frameworks/server-webpack5/package.json index 728774d9db9..24e17e44452 100644 --- a/code/frameworks/server-webpack5/package.json +++ b/code/frameworks/server-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/server-webpack5", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Server: View HTML snippets from a server in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/svelte-vite/package.json b/code/frameworks/svelte-vite/package.json index 168c9fdfd5c..b17aea75239 100644 --- a/code/frameworks/svelte-vite/package.json +++ b/code/frameworks/svelte-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/svelte-vite", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Svelte and Vite: Develop Svelte components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/svelte-webpack5/package.json b/code/frameworks/svelte-webpack5/package.json index ed6fbe94b33..12bc1d7d84e 100644 --- a/code/frameworks/svelte-webpack5/package.json +++ b/code/frameworks/svelte-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/svelte-webpack5", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Svelte: Develop Svelte Component in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index fba6bdb225f..6e90749fedf 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/sveltekit", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for SvelteKit", "keywords": [ "storybook", diff --git a/code/frameworks/vue3-vite/package.json b/code/frameworks/vue3-vite/package.json index 2f3ae021c76..c863dc5bbfe 100644 --- a/code/frameworks/vue3-vite/package.json +++ b/code/frameworks/vue3-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue3-vite", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Vue3 and Vite: Develop Vue3 components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/vue3-webpack5/package.json b/code/frameworks/vue3-webpack5/package.json index 3100bb0b628..bcb49a1fb6e 100644 --- a/code/frameworks/vue3-webpack5/package.json +++ b/code/frameworks/vue3-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue3-webpack5", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Vue 3: Develop Vue 3 Components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/web-components-vite/package.json b/code/frameworks/web-components-vite/package.json index 836c140ca82..1000ee22686 100644 --- a/code/frameworks/web-components-vite/package.json +++ b/code/frameworks/web-components-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/web-components-vite", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for web-components and Vite: Develop Web Components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/frameworks/web-components-webpack5/package.json b/code/frameworks/web-components-webpack5/package.json index 4d52e1e849e..1922e8bb0c2 100644 --- a/code/frameworks/web-components-webpack5/package.json +++ b/code/frameworks/web-components-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/web-components-webpack5", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for web-components: View web components snippets in isolation with Hot Reloading.", "keywords": [ "lit", diff --git a/code/lib/blocks/package.json b/code/lib/blocks/package.json index c38aa59f379..d815acc1a45 100644 --- a/code/lib/blocks/package.json +++ b/code/lib/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/blocks", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook Doc Blocks", "keywords": [ "storybook" diff --git a/code/lib/cli-sb/package.json b/code/lib/cli-sb/package.json index 9fb9cbcc612..5210403c97b 100644 --- a/code/lib/cli-sb/package.json +++ b/code/lib/cli-sb/package.json @@ -1,6 +1,6 @@ { "name": "sb", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook CLI", "keywords": [ "storybook" diff --git a/code/lib/cli-storybook/package.json b/code/lib/cli-storybook/package.json index fd8411206ce..f08e2354ada 100644 --- a/code/lib/cli-storybook/package.json +++ b/code/lib/cli-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/cli", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook CLI", "keywords": [ "storybook" diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts index 71110733d89..87da579183c 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts @@ -7,7 +7,13 @@ import dedent from 'ts-dedent'; import { getAddonNames } from '../helpers/mainConfigFile'; import { addonA11yAddonTest, transformSetupFile } from './addon-a11y-addon-test'; -vi.mock('../helpers/mainConfigFile'); +vi.mock('../helpers/mainConfigFile', async (importOriginal) => { + const mod = (await importOriginal()) as any; + return { + ...mod, + getAddonNames: vi.fn(), + }; +}); // mock fs.existsSync vi.mock('fs', async (importOriginal) => { @@ -46,6 +52,20 @@ describe('addonA11yAddonTest', () => { expect(result).toBeNull(); }); + it('should return null if provided framework is not supported', async () => { + vi.mocked(getAddonNames).mockReturnValue([ + '@storybook/addon-a11y', + '@storybook/experimental-addon-test', + ]); + const result = await addonA11yAddonTest.check({ + mainConfig: { + framework: '@storybook/angular', + }, + configDir: '', + } as any); + expect(result).toBeNull(); + }); + it('should return setupFile and transformedSetupCode if vitest.setup file exists', async () => { vi.mocked(getAddonNames).mockReturnValue([ '@storybook/addon-a11y', @@ -54,7 +74,12 @@ describe('addonA11yAddonTest', () => { vi.mocked(existsSync).mockReturnValue(true); vi.mocked(readFileSync).mockReturnValue('const annotations = setProjectAnnotations([]);'); - const result = await addonA11yAddonTest.check({ mainConfig, configDir } as any); + const result = await addonA11yAddonTest.check({ + mainConfig: { + framework: '@storybook/react-vite', + }, + configDir, + } as any); expect(result).toEqual({ setupFile: path.join(configDir, 'vitest.setup.js'), transformedSetupCode: expect.any(String), @@ -71,7 +96,12 @@ describe('addonA11yAddonTest', () => { throw new Error('Test error'); }); - const result = await addonA11yAddonTest.check({ mainConfig, configDir } as any); + const result = await addonA11yAddonTest.check({ + mainConfig: { + framework: '@storybook/sveltekit', + }, + configDir, + } as any); expect(result).toEqual({ setupFile: path.join(configDir, 'vitest.setup.js'), transformedSetupCode: null, @@ -141,7 +171,8 @@ describe('addonA11yAddonTest', () => { `; vi.mocked(readFileSync).mockReturnValue(source); - const transformedCode = transformSetupFile(setupFile); + const s = readFileSync(setupFile, 'utf8'); + const transformedCode = transformSetupFile(s); expect(transformedCode).toMatchInlineSnapshot(` "import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; import { beforeAll } from 'vitest'; @@ -169,7 +200,35 @@ describe('addonA11yAddonTest', () => { `; vi.mocked(readFileSync).mockReturnValue(source); - const transformedCode = transformSetupFile(setupFile); + const s = readFileSync(setupFile, 'utf8'); + const transformedCode = transformSetupFile(s); + expect(transformedCode).toMatchInlineSnapshot(` + "import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; + import { beforeAll } from 'vitest'; + import { setProjectAnnotations } from 'storybook'; + import * as projectAnnotations from './preview'; + + const project = setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); + + beforeAll(project.beforeAll);" + `); + }); + + it('should transform setup file correctly - project annotation is not an array', () => { + const setupFile = '/path/to/vitest.setup.ts'; + const source = dedent` + import { beforeAll } from 'vitest'; + import { setProjectAnnotations } from 'storybook'; + import * as projectAnnotations from './preview'; + + const project = setProjectAnnotations(projectAnnotations); + + beforeAll(project.beforeAll); + `; + vi.mocked(readFileSync).mockReturnValue(source); + + const s = readFileSync(setupFile, 'utf8'); + const transformedCode = transformSetupFile(s); expect(transformedCode).toMatchInlineSnapshot(` "import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; import { beforeAll } from 'vitest'; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts index ae848cf5768..a8cd25cdf37 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts @@ -1,10 +1,17 @@ +import { rendererPackages } from 'storybook/internal/common'; + import { existsSync, readFileSync, writeFileSync } from 'fs'; import * as jscodeshift from 'jscodeshift'; import path from 'path'; import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; -import { getAddonNames } from '../helpers/mainConfigFile'; +// Relative path import to avoid dependency to @storybook/test +import { + SUPPORTED_FRAMEWORKS, + SUPPORTED_RENDERERS, +} from '../../../../../addons/test/src/constants'; +import { getAddonNames, getFrameworkPackageName, getRendererName } from '../helpers/mainConfigFile'; import type { Fix } from '../types'; export const vitestFileExtensions = ['.js', '.ts', '.cts', '.mts', '.cjs', '.mjs'] as const; @@ -22,8 +29,7 @@ interface AddonA11yAddonTestOptions { */ export const addonA11yAddonTest: Fix = { id: 'addonA11yAddonTest', - // TODO: Change to the correct version after testing - versionRange: ['<8.5.0', '*'], + versionRange: ['<8.5.0', '>=8.5.0'], promptType(result) { if (result.setupFile === null) { @@ -36,11 +42,23 @@ export const addonA11yAddonTest: Fix = { async check({ mainConfig, configDir }) { const addons = getAddonNames(mainConfig); + const frameworkPackageName = getFrameworkPackageName(mainConfig); + const rendererPackageName = getRendererName(mainConfig); + const hasA11yAddon = !!addons.find((addon) => addon.includes('@storybook/addon-a11y')); const hasTestAddon = !!addons.find((addon) => addon.includes('@storybook/experimental-addon-test') ); + if ( + !SUPPORTED_FRAMEWORKS.find((framework) => frameworkPackageName?.includes(framework)) && + !SUPPORTED_RENDERERS.find((renderer) => + rendererPackageName?.includes(rendererPackages[renderer]) + ) + ) { + return null; + } + if (!hasA11yAddon || !hasTestAddon || !configDir) { return null; } @@ -53,7 +71,11 @@ export const addonA11yAddonTest: Fix = { try { if (vitestSetupFile) { - const transformedSetupCode = transformSetupFile(vitestSetupFile); + const source = readFileSync(vitestSetupFile, 'utf8'); + if (source.includes('@storybook/addon-a11y')) { + return null; + } + const transformedSetupCode = transformSetupFile(source); return { setupFile: vitestSetupFile, transformedSetupCode, @@ -124,8 +146,7 @@ export const addonA11yAddonTest: Fix = { }, }; -export function transformSetupFile(setupFile: string) { - const source = readFileSync(setupFile, 'utf8'); +export function transformSetupFile(source: string) { const j = jscodeshift.withParser('ts'); const root = j(source); @@ -148,9 +169,14 @@ export function transformSetupFile(setupFile: string) { throw new Error('Could not find setProjectAnnotations call in vitest.setup file'); } - // Add a11yAddonAnnotations to the annotations array - setProjectAnnotationsCall.find(j.ArrayExpression).forEach((p) => { - p.value.elements.unshift(j.identifier('a11yAddonAnnotations')); + // Add a11yAddonAnnotations to the annotations array or create a new array if argument is a string + setProjectAnnotationsCall.forEach((p) => { + if (p.value.arguments.length === 1 && p.value.arguments[0].type === 'ArrayExpression') { + p.value.arguments[0].elements.unshift(j.identifier('a11yAddonAnnotations')); + } else if (p.value.arguments.length === 1 && p.value.arguments[0].type === 'Identifier') { + const arg = p.value.arguments[0]; + p.value.arguments[0] = j.arrayExpression([j.identifier('a11yAddonAnnotations'), arg]); + } }); // Add the import declaration at the top diff --git a/code/lib/cli-storybook/src/sandbox-templates.ts b/code/lib/cli-storybook/src/sandbox-templates.ts index f905d284180..0324635b1ff 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'], }, @@ -400,6 +433,16 @@ const baseTemplates = { }, skipTasks: ['e2e-tests-dev', 'bench'], }, + 'nuxt-vite/default-ts': { + name: 'Nuxt v3 (Vite | TypeScript)', + script: 'npx nuxi init --packageManager yarn --gitInit false {{beforeDir}}', + expected: { + framework: '@storybook-vue/nuxt', + renderer: '@storybook/vue3', + builder: '@storybook/builder-vite', + }, + skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'], + }, 'html-webpack/default': { name: 'HTML Latest (Webpack | JavaScript)', script: 'yarn create webpack5-html {{beforeDir}}', @@ -809,6 +852,7 @@ export const normal: TemplateKey[] = [ 'react-vite/default-ts', 'angular-cli/default-ts', 'vue3-vite/default-ts', + 'nuxt-vite/default-ts', 'lit-vite/default-ts', 'svelte-vite/default-ts', 'svelte-kit/skeleton-ts', diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json index 8e715035754..cdc83ea4bbb 100644 --- a/code/lib/cli/package.json +++ b/code/lib/cli/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook's CLI - install, dev, build, upgrade, and more", "keywords": [ "cli", @@ -311,7 +311,8 @@ ], "scripts": { "check": "jiti ../../../scripts/prepare/check.ts", - "prep": "jiti ../../../scripts/prepare/bundle.ts" + "prep": "jiti ../../../scripts/prepare/bundle.ts", + "sb": "node ./bin/index.js" }, "dependencies": { "@storybook/core": "workspace:*" diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index 4826ca8fc29..324c7bc2632 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/codemod", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "A collection of codemod scripts written with JSCodeshift", "keywords": [ "storybook" diff --git a/code/lib/core-webpack/package.json b/code/lib/core-webpack/package.json index af5e0cb57a9..798463e8345 100644 --- a/code/lib/core-webpack/package.json +++ b/code/lib/core-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-webpack", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/lib/create-storybook/package.json b/code/lib/create-storybook/package.json index 9a05e6de9ad..05255add8f4 100644 --- a/code/lib/create-storybook/package.json +++ b/code/lib/create-storybook/package.json @@ -1,6 +1,6 @@ { "name": "create-storybook", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Initialize Storybook into your project", "homepage": "https://github.com/storybookjs/storybook/tree/next/code/lib/create-storybook", "bugs": { diff --git a/code/lib/create-storybook/src/generators/NUXT/index.ts b/code/lib/create-storybook/src/generators/NUXT/index.ts new file mode 100644 index 00000000000..a7d7be640eb --- /dev/null +++ b/code/lib/create-storybook/src/generators/NUXT/index.ts @@ -0,0 +1,31 @@ +import { baseGenerator } from '../baseGenerator'; +import type { Generator } from '../types'; + +const generator: Generator = async (packageManager, npmOptions, options) => { + await baseGenerator( + packageManager, + npmOptions, + options, + 'vue3', + { + extraPackages: async () => { + return ['@nuxtjs/storybook']; + }, + installFrameworkPackages: false, + componentsDestinationPath: './components', + extraMain: { + stories: ['../components/**/*.mdx', '../components/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + }, + }, + 'nuxt' + ); + // Add nuxtjs/storybook to nuxt.config.js + await packageManager.runPackageCommand('nuxi', [ + 'module', + 'add', + '@nuxtjs/storybook', + '--skipInstall', + ]); +}; + +export default generator; diff --git a/code/lib/create-storybook/src/generators/baseGenerator.ts b/code/lib/create-storybook/src/generators/baseGenerator.ts index 84daecba1b4..1b0c917edd5 100644 --- a/code/lib/create-storybook/src/generators/baseGenerator.ts +++ b/code/lib/create-storybook/src/generators/baseGenerator.ts @@ -1,14 +1,14 @@ import { mkdir } from 'node:fs/promises'; import { dirname, join } from 'node:path'; -import type { NpmOptions } from 'storybook/internal/cli'; -import type { Builder, SupportedRenderers } from 'storybook/internal/cli'; +import type { Builder, NpmOptions } from 'storybook/internal/cli'; import { SupportedLanguage, externalFrameworks } from 'storybook/internal/cli'; import { copyTemplateFiles } from 'storybook/internal/cli'; import { configureEslintPlugin, extractEslintInfo } from 'storybook/internal/cli'; import { detectBuilder } from 'storybook/internal/cli'; import type { JsPackageManager } from 'storybook/internal/common'; import { getPackageDetails, versions as packageVersions } from 'storybook/internal/common'; +import type { SupportedRenderers } from 'storybook/internal/types'; import type { SupportedFrameworks } from 'storybook/internal/types'; // eslint-disable-next-line depend/ban-dependencies @@ -27,6 +27,7 @@ const defaultOptions: FrameworkOptions = { staticDir: undefined, addScripts: true, addMainFile: true, + addPreviewFile: true, addComponents: true, webpackCompiler: () => undefined, extraMain: undefined, @@ -34,6 +35,7 @@ const defaultOptions: FrameworkOptions = { extensions: undefined, componentsDestinationPath: undefined, storybookConfigFolder: '.storybook', + installFrameworkPackages: true, }; const getBuilderDetails = (builder: string) => { @@ -82,13 +84,10 @@ const getFrameworkPackage = (framework: string | undefined, renderer: string, bu ); } - if (externalFramework.frameworks !== undefined) { - return externalFramework.frameworks.find((item) => - item.match(new RegExp(`-${storybookBuilder}`)) - ); - } - - return externalFramework.packageName; + return ( + externalFramework.frameworks?.find((item) => item.match(new RegExp(`-${storybookBuilder}`))) ?? + externalFramework.packageName + ); }; const getRendererPackage = (framework: string | undefined, renderer: string) => { @@ -117,6 +116,7 @@ const getFrameworkDetails = ( framework?: string; renderer?: string; rendererId: SupportedRenderers; + frameworkPackage?: string; } => { const frameworkPackage = getFrameworkPackage(framework, renderer, builder); invariant(frameworkPackage, 'Missing framework package.'); @@ -144,6 +144,7 @@ const getFrameworkDetails = ( return { packages: [rendererPackage, frameworkPackage], framework: frameworkPackagePath, + frameworkPackage, rendererId: renderer, type: 'framework', }; @@ -169,8 +170,18 @@ const stripVersions = (addons: string[]) => addons.map((addon) => getPackageDeta const hasInteractiveStories = (rendererId: SupportedRenderers) => ['react', 'angular', 'preact', 'svelte', 'vue3', 'html', 'solid', 'qwik'].includes(rendererId); -const hasFrameworkTemplates = (framework?: SupportedFrameworks) => - framework ? ['angular', 'nextjs', 'react-native-web-vite'].includes(framework) : false; +const hasFrameworkTemplates = (framework?: SupportedFrameworks) => { + if (!framework) { + return false; + } + // Nuxt has framework templates, but for sandboxes we create them from the Vue3 renderer + // As the Nuxt framework templates are not compatible with the stories we need for CI. + // See: https://github.com/storybookjs/storybook/pull/28607#issuecomment-2467903327 + if (framework === 'nuxt') { + return process.env.IN_STORYBOOK_SANDBOX !== 'true'; + } + return ['angular', 'nextjs', 'react-native-web-vite'].includes(framework); +}; export async function baseGenerator( packageManager: JsPackageManager, @@ -193,6 +204,7 @@ export async function baseGenerator( rendererId, framework: frameworkInclude, builder: builderInclude, + frameworkPackage, } = getFrameworkDetails( renderer, builder, @@ -208,12 +220,14 @@ export async function baseGenerator( staticDir, addScripts, addMainFile, + addPreviewFile, addComponents, extraMain, extensions, storybookConfigFolder, componentsDestinationPath, webpackCompiler, + installFrameworkPackages, } = { ...defaultOptions, ...options, @@ -278,7 +292,7 @@ export async function baseGenerator( const allPackages = [ 'storybook', getExternalFramework(rendererId) ? undefined : `@storybook/${rendererId}`, - ...frameworkPackages, + ...(installFrameworkPackages ? frameworkPackages : []), ...addonPackages, ...(extraPackagesToInstall || []), ].filter(Boolean); @@ -320,9 +334,9 @@ export async function baseGenerator( addDependenciesSpinner.succeed(); } - // Passing `recursive: true` ensures that the method doesn't throw when - // the directory already exists. - await mkdir(`./${storybookConfigFolder}`, { recursive: true }); + if (addMainFile || addPreviewFile) { + await mkdir(`./${storybookConfigFolder}`, { recursive: true }); + } if (addMainFile) { const prefixes = shouldApplyRequireWrapperOnPackageNames @@ -351,6 +365,7 @@ export async function baseGenerator( name: frameworkInclude, options: options.framework || {}, }, + frameworkPackage, prefixes, storybookConfigFolder, addons: shouldApplyRequireWrapperOnPackageNames @@ -370,12 +385,14 @@ export async function baseGenerator( }); } - await configurePreview({ - frameworkPreviewParts, - storybookConfigFolder: storybookConfigFolder as string, - language, - rendererId, - }); + if (addPreviewFile) { + await configurePreview({ + frameworkPreviewParts, + storybookConfigFolder: storybookConfigFolder as string, + language, + rendererId, + }); + } if (addScripts) { await packageManager.addStorybookCommandInScripts({ diff --git a/code/lib/create-storybook/src/generators/configure.test.ts b/code/lib/create-storybook/src/generators/configure.test.ts index 853e1102a05..e593810dd97 100644 --- a/code/lib/create-storybook/src/generators/configure.test.ts +++ b/code/lib/create-storybook/src/generators/configure.test.ts @@ -26,6 +26,7 @@ describe('configureMain', () => { framework: { name: '@storybook/react-vite', }, + frameworkPackage: '@storybook/react-vite', }); const { calls } = vi.mocked(fsp.writeFile).mock; @@ -55,6 +56,7 @@ describe('configureMain', () => { framework: { name: '@storybook/react-vite', }, + frameworkPackage: '@storybook/react-vite', }); const { calls } = vi.mocked(fsp.writeFile).mock; @@ -89,6 +91,7 @@ describe('configureMain', () => { framework: { name: "%%path.dirname(require.resolve(path.join('@storybook/react-webpack5', 'package.json')))%%", }, + frameworkPackage: '@storybook/react-webpack5', }); const { calls } = vi.mocked(fsp.writeFile).mock; diff --git a/code/lib/create-storybook/src/generators/configure.ts b/code/lib/create-storybook/src/generators/configure.ts index c7002c58c04..401509dc239 100644 --- a/code/lib/create-storybook/src/generators/configure.ts +++ b/code/lib/create-storybook/src/generators/configure.ts @@ -13,6 +13,7 @@ interface ConfigureMainOptions { storybookConfigFolder: string; language: SupportedLanguage; prefixes: string[]; + frameworkPackage: string; /** * Extra values for main.js * @@ -61,6 +62,7 @@ export async function configureMain({ extensions = ['js', 'jsx', 'mjs', 'ts', 'tsx'], storybookConfigFolder, language, + frameworkPackage, prefixes = [], ...custom }: ConfigureMainOptions) { @@ -78,8 +80,6 @@ export async function configureMain({ let mainConfigTemplate = dedent`<><>const config<> = <>; export default config;`; - const frameworkPackage = sanitizeFramework(custom.framework?.name); - if (!frameworkPackage) { mainConfigTemplate = mainConfigTemplate.replace('<>', '').replace('<>', ''); logger.warn('Could not find framework package name'); diff --git a/code/lib/create-storybook/src/generators/types.ts b/code/lib/create-storybook/src/generators/types.ts index e9192a47c3c..3c07d97e27c 100644 --- a/code/lib/create-storybook/src/generators/types.ts +++ b/code/lib/create-storybook/src/generators/types.ts @@ -23,6 +23,7 @@ export interface FrameworkOptions { staticDir?: string; addScripts?: boolean; addMainFile?: boolean; + addPreviewFile?: boolean; addComponents?: boolean; webpackCompiler?: ({ builder }: { builder: Builder }) => 'babel' | 'swc' | undefined; extraMain?: any; @@ -30,6 +31,7 @@ export interface FrameworkOptions { framework?: Record; storybookConfigFolder?: string; componentsDestinationPath?: string; + installFrameworkPackages?: boolean; } export type Generator = ( diff --git a/code/lib/create-storybook/src/initiate.ts b/code/lib/create-storybook/src/initiate.ts index 6c1c6559d26..970cef3d137 100644 --- a/code/lib/create-storybook/src/initiate.ts +++ b/code/lib/create-storybook/src/initiate.ts @@ -27,6 +27,7 @@ import angularGenerator from './generators/ANGULAR'; import emberGenerator from './generators/EMBER'; import htmlGenerator from './generators/HTML'; import nextjsGenerator from './generators/NEXTJS'; +import nuxtGenerator from './generators/NUXT'; import preactGenerator from './generators/PREACT'; import qwikGenerator from './generators/QWIK'; import reactGenerator from './generators/REACT'; @@ -117,6 +118,11 @@ const installStorybook = async ( commandLog('Adding Storybook support to your "Vue 3" app') ); + case ProjectType.NUXT: + return nuxtGenerator(packageManager, npmOptions, generatorOptions).then( + commandLog('Adding Storybook support to your "Nuxt" app') + ); + case ProjectType.ANGULAR: commandLog('Adding Storybook support to your "Angular" app'); return angularGenerator(packageManager, npmOptions, generatorOptions, options); diff --git a/code/lib/csf-plugin/package.json b/code/lib/csf-plugin/package.json index 363ee09250f..3b57203d481 100644 --- a/code/lib/csf-plugin/package.json +++ b/code/lib/csf-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/csf-plugin", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Enrich CSF files via static analysis", "keywords": [ "storybook" diff --git a/code/lib/instrumenter/package.json b/code/lib/instrumenter/package.json index 559bcbad40c..84b42100656 100644 --- a/code/lib/instrumenter/package.json +++ b/code/lib/instrumenter/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/instrumenter", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/lib/react-dom-shim/package.json b/code/lib/react-dom-shim/package.json index a25cbdb8c8e..13eacc85b7e 100644 --- a/code/lib/react-dom-shim/package.json +++ b/code/lib/react-dom-shim/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-dom-shim", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/lib/source-loader/package.json b/code/lib/source-loader/package.json index 2de7c8411c9..409cdf2a3a0 100644 --- a/code/lib/source-loader/package.json +++ b/code/lib/source-loader/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/source-loader", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Source loader", "keywords": [ "lib", diff --git a/code/lib/test/package.json b/code/lib/test/package.json index 792d88e03dd..da8c9beb922 100644 --- a/code/lib/test/package.json +++ b/code/lib/test/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/test", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "", "keywords": [ "storybook" diff --git a/code/nx.json b/code/nx.json index 688fc38b5dd..eaaabd089ea 100644 --- a/code/nx.json +++ b/code/nx.json @@ -3,6 +3,7 @@ "extends": "nx/presets/npm.json", "nxCloudAccessToken": "NGVmYTkxMmItYzY3OS00MjkxLTk1ZDktZDFmYTFmNmVlNGY4fHJlYWQ=", "defaultBase": "next", + "useLegacyCache": true, "parallel": 8, "plugins": [ { diff --git a/code/package.json b/code/package.json index 35bc57478fb..b8531c49d34 100644 --- a/code/package.json +++ b/code/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/root", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "private": true, "description": "Storybook root", "homepage": "https://storybook.js.org/", @@ -90,11 +90,11 @@ "type-fest": "~2.19" }, "dependencies": { - "@chromatic-com/storybook": "^3.2.0", + "@chromatic-com/storybook": "^3.2.2", "@happy-dom/global-registrator": "^14.12.0", - "@nx/eslint": "20.1.4", - "@nx/vite": "20.1.4", - "@nx/workspace": "20.1.4", + "@nx/eslint": "20.2.2", + "@nx/vite": "20.2.2", + "@nx/workspace": "20.2.2", "@playwright/test": "1.48.1", "@storybook/addon-a11y": "workspace:*", "@storybook/addon-actions": "workspace:*", @@ -202,7 +202,7 @@ "husky": "^4.3.7", "lint-staged": "^13.2.2", "mock-require": "^3.0.3", - "nx": "20.1.4", + "nx": "20.2.2", "prettier": "^3.1.1", "prettier-plugin-brace-style": "^0.6.2", "prettier-plugin-css-order": "^2.1.2", diff --git a/code/presets/create-react-app/package.json b/code/presets/create-react-app/package.json index 6e6a149e760..034243e84eb 100644 --- a/code/presets/create-react-app/package.json +++ b/code/presets/create-react-app/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-create-react-app", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Create React App preset", "keywords": [ "storybook" diff --git a/code/presets/html-webpack/package.json b/code/presets/html-webpack/package.json index 9ca2cb19ba3..b0d64281756 100644 --- a/code/presets/html-webpack/package.json +++ b/code/presets/html-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-html-webpack", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for HTML: View HTML snippets in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/presets/preact-webpack/package.json b/code/presets/preact-webpack/package.json index 6442f22d7c2..b9afcadcc54 100644 --- a/code/presets/preact-webpack/package.json +++ b/code/presets/preact-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-preact-webpack", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Preact: Develop Preact Component in isolation.", "keywords": [ "storybook" diff --git a/code/presets/react-webpack/package.json b/code/presets/react-webpack/package.json index ecdae18620f..d6dbc6b2988 100644 --- a/code/presets/react-webpack/package.json +++ b/code/presets/react-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-react-webpack", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for React: Develop React Component in isolation with Hot Reloading", "keywords": [ "storybook" diff --git a/code/presets/server-webpack/package.json b/code/presets/server-webpack/package.json index 275e323e0c9..11dd58d5e50 100644 --- a/code/presets/server-webpack/package.json +++ b/code/presets/server-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-server-webpack", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Server: View HTML snippets from a server in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/presets/svelte-webpack/package.json b/code/presets/svelte-webpack/package.json index 668564406d7..31132600ffc 100644 --- a/code/presets/svelte-webpack/package.json +++ b/code/presets/svelte-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-svelte-webpack", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Svelte: Develop Svelte Component in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/presets/vue3-webpack/package.json b/code/presets/vue3-webpack/package.json index a2b686257fa..997b4807d36 100644 --- a/code/presets/vue3-webpack/package.json +++ b/code/presets/vue3-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preset-vue3-webpack", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook for Vue 3: Develop Vue 3 Components in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/renderers/html/package.json b/code/renderers/html/package.json index c127ef64950..78f5d26b080 100644 --- a/code/renderers/html/package.json +++ b/code/renderers/html/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook HTML renderer", "keywords": [ "storybook" diff --git a/code/renderers/preact/package.json b/code/renderers/preact/package.json index 3005d5e0e57..a740e9671db 100644 --- a/code/renderers/preact/package.json +++ b/code/renderers/preact/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook Preact renderer", "keywords": [ "storybook" diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json index 6e2c8d94047..3df61fc8f8c 100644 --- a/code/renderers/react/package.json +++ b/code/renderers/react/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react", - "version": "8.5.0-alpha.20", + "version": "8.5.0-beta.5", "description": "Storybook React renderer", "keywords": [ "storybook" diff --git a/code/renderers/react/src/act-compat.ts b/code/renderers/react/src/act-compat.ts index 36e56712e02..7d64e0f7c3b 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 08d625b5729..b2e6c1de5b1 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' }; @@ -10,9 +11,9 @@ export { renderToCanvas } from './renderToCanvas'; export { mount } from './mount'; export const decorators: Decorator[] = [ - (Story, context) => { + (story, context) => { if (!context.parameters?.react?.rsc) { - return ; + return story(); } const major = semver.major(React.version); @@ -21,10 +22,73 @@ export const decorators: Decorator[] = [ throw new Error('React Server Components require React >= 18.3'); } - return ( - - - - ); + 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 7b906c9f4bd..ca29c8c7de7 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 3ae6136f958..4ae1acbb7fe 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 5b36a6347d0..dabe38e0e82 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 ( -export default { - title: 'Addons/Test', +const meta = { + title: 'Addons/Group/Test', component: Component, } as Meta; +export default meta; + +type Story = StoryObj; + const { pass } = instrument({ pass: async () => {}, }, { intercept: true }) -export const ExpectedFailure = { +export const ExpectedFailure: Story = { args: { forceFailure: false, }, @@ -28,34 +32,61 @@ export const ExpectedFailure = { throw new Error('Expected failure'); } } -} satisfies StoryAnnotations; +}; -export const ExpectedSuccess = { +export const ExpectedSuccess: Story = { play: async () => { await pass(); } -} satisfies StoryAnnotations; +}; -export const LongRunning = { +export const LongRunning: Story = { loaders: [async () => new Promise((resolve) => setTimeout(resolve, 800))], -} satisfies StoryAnnotations; +}; // Tests will pass in browser, but fail in CLI -export const MismatchFailure = { +export const MismatchFailure: Story = { play: async () => { await pass(); if(!globalThis.__vitest_browser__) { throw new Error('Expected failure'); } } -} satisfies StoryAnnotations; +}; // Tests will fail in browser, but pass in CLI -export const MismatchSuccess = { +export const MismatchSuccess: Story = { play: async () => { await pass(); if(globalThis.__vitest_browser__) { throw new Error('Unexpected success'); } + }, + tags: ['fail-on-purpose'], +}; + +export const PreviewHeadTest: Story = { + play: async () => { + const styles = window.getComputedStyle(document.body); + // set in preview-head.html + expect(styles.backgroundColor).toBe('rgb(250, 250, 210)'); + // set in main.js#previewHead + expect(styles.borderColor).toBe('rgb(255, 0, 0)'); } -} satisfies StoryAnnotations; \ No newline at end of file +}; + +export const StaticDirTest: Story = { + play: async () => { + const path = '/test-static-dirs/static.js'; + const { staticFunction } = await import(/* @vite-ignore */path); + expect(staticFunction()).toBe(true); + } +} + +export const ViteFinalTest: Story = { + play: async () => { + // @ts-expect-error TS doesn't know about the alias + const { aliasedFunction } = await import('test-alias'); + expect(aliasedFunction()).toBe(true); + } +} diff --git a/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.playwright.tsx b/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.playwright.tsx index db4590c82fa..ee201e58715 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.playwright.tsx +++ b/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.playwright.tsx @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ + import { createTest } from '@storybook/react/experimental-playwright'; import { test as base, expect } from '@playwright/experimental-ct-react'; import stories, { SingleComposedStory, WithSpanishGlobal } from './Button.stories.playwright'; diff --git a/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.stories.tsx b/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.stories.tsx index 9d2cf823096..2c976b43cc4 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.stories.tsx +++ b/test-storybooks/portable-stories-kitchen-sink/react/stories/Button.stories.tsx @@ -67,6 +67,8 @@ export const CSF3Primary: CSF3Story = { size: 'large', primary: true, }, + // Accessibility is failing for the Button + tags: ['fail-on-purpose'], }; export const CSF3Button: CSF3Story = { diff --git a/test-storybooks/portable-stories-kitchen-sink/react/stories/OtherComponent.stories.tsx b/test-storybooks/portable-stories-kitchen-sink/react/stories/OtherComponent.stories.tsx new file mode 100644 index 00000000000..dd410fe9c74 --- /dev/null +++ b/test-storybooks/portable-stories-kitchen-sink/react/stories/OtherComponent.stories.tsx @@ -0,0 +1,22 @@ +import { Meta, type StoryObj } from '@storybook/react' + +const Component = () => + +const meta = { + title: 'Addons/Group/Other', + component: Component, +} as Meta; + +export default meta; + +type Story = StoryObj; + +export const Passes: Story = { +}; + +export const Fails: Story = { + play: async () => { + throw new Error('Expected failure'); + }, + tags: ['fail-on-purpose'], +}; diff --git a/test-storybooks/portable-stories-kitchen-sink/react/vitest.workspace.ts b/test-storybooks/portable-stories-kitchen-sink/react/vitest.workspace.ts index f1657f3acdd..effd954bd78 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/vitest.workspace.ts +++ b/test-storybooks/portable-stories-kitchen-sink/react/vitest.workspace.ts @@ -5,14 +5,15 @@ export default defineWorkspace([ { extends: "vite.config.ts", plugins: [ - storybookTest(), + storybookTest(process.env.SKIP_FAIL_ON_PURPOSE ? { + tags: { + exclude: ["fail-on-purpose"], + } + } : undefined), ], test: { name: "storybook", pool: "threads", - include: [ - "stories/AddonTest.stories.?(c|m)[jt]s?(x)", - ], deps: { optimizer: { web: { @@ -30,4 +31,4 @@ export default defineWorkspace([ environment: "happy-dom", }, }, -]); \ No newline at end of file +]); diff --git a/test-storybooks/portable-stories-kitchen-sink/react/yarn.lock b/test-storybooks/portable-stories-kitchen-sink/react/yarn.lock index e3484dc0172..b8242119352 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/yarn.lock +++ b/test-storybooks/portable-stories-kitchen-sink/react/yarn.lock @@ -12,7 +12,7 @@ __metadata: languageName: node linkType: hard -"@ampproject/remapping@npm:^2.2.0": +"@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" dependencies: @@ -135,6 +135,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-string-parser@npm:7.25.9" + checksum: 10/c28656c52bd48e8c1d9f3e8e68ecafd09d949c57755b0d353739eb4eae7ba4f7e67e92e4036f1cd43378cc1397a2c943ed7bcaf5949b04ab48607def0258b775 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.25.7": version: 7.25.7 resolution: "@babel/helper-validator-identifier@npm:7.25.7" @@ -142,6 +149,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-identifier@npm:7.25.9" + checksum: 10/3f9b649be0c2fd457fa1957b694b4e69532a668866b8a0d81eabfa34ba16dbf3107b39e0e7144c55c3c652bf773ec816af8df4a61273a2bb4eb3145ca9cf478e + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.25.7": version: 7.25.7 resolution: "@babel/helper-validator-option@npm:7.25.7" @@ -182,6 +196,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.4": + version: 7.26.2 + resolution: "@babel/parser@npm:7.26.2" + dependencies: + "@babel/types": "npm:^7.26.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10/8baee43752a3678ad9f9e360ec845065eeee806f1fdc8e0f348a8a0e13eef0959dabed4a197c978896c493ea205c804d0a1187cc52e4a1ba017c7935bab4983d + languageName: node + linkType: hard + "@babel/plugin-syntax-async-generators@npm:^7.8.4": version: 7.8.4 resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" @@ -437,6 +462,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.25.4, @babel/types@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10/40780741ecec886ed9edae234b5eb4976968cc70d72b4e5a40d55f83ff2cc457de20f9b0f4fe9d858350e43dab0ea496e7ef62e2b2f08df699481a76df02cd6e + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -444,12 +479,12 @@ __metadata: languageName: node linkType: hard -"@bundled-es-modules/cookie@npm:^2.0.0": - version: 2.0.0 - resolution: "@bundled-es-modules/cookie@npm:2.0.0" +"@bundled-es-modules/cookie@npm:^2.0.1": + version: 2.0.1 + resolution: "@bundled-es-modules/cookie@npm:2.0.1" dependencies: - cookie: "npm:^0.5.0" - checksum: 10/c8ef02aa5d3f6c786cfa407e1c93b4af29c600eb09990973f47a7a49e4771c1bec37c8f8e567638bb9cbc41f4e38d065ff1d8eaf9bf91f0c3613a6d60bc82c8c + cookie: "npm:^0.7.2" + checksum: 10/0038a5e82c41bfcd722afedabeb6961a5f15747b3681d7f4b61e35eb1e33130039e10ee9250dc9c9e4d3915ce1aeee717c0fb92225111574f0a030411abc0987 languageName: node linkType: hard @@ -911,58 +946,48 @@ __metadata: languageName: node linkType: hard -"@inquirer/confirm@npm:^3.0.0": - version: 3.2.0 - resolution: "@inquirer/confirm@npm:3.2.0" +"@inquirer/confirm@npm:^5.0.0": + version: 5.0.2 + resolution: "@inquirer/confirm@npm:5.0.2" dependencies: - "@inquirer/core": "npm:^9.1.0" - "@inquirer/type": "npm:^1.5.3" - checksum: 10/6b032a26c64075dc14769558720b17f09bc6784a223bbf2c85ec42e491be6ce4c4b83518433c47e05d7e8836ba680ab1b2f6b9c553410d4326582308a1fd2259 + "@inquirer/core": "npm:^10.1.0" + "@inquirer/type": "npm:^3.0.1" + peerDependencies: + "@types/node": ">=18" + checksum: 10/4e775b80b689adeb0b2852ed79b368ef23a82fe3d5f580a562f4af7cdf002a19e0ec1b3b95acc6d49427a72c0fcb5b6548e0cdcafe2f0d3f3d6a923e04aabd0c languageName: node linkType: hard -"@inquirer/core@npm:^9.1.0": - version: 9.2.1 - resolution: "@inquirer/core@npm:9.2.1" +"@inquirer/core@npm:^10.1.0": + version: 10.1.0 + resolution: "@inquirer/core@npm:10.1.0" dependencies: - "@inquirer/figures": "npm:^1.0.6" - "@inquirer/type": "npm:^2.0.0" - "@types/mute-stream": "npm:^0.0.4" - "@types/node": "npm:^22.5.5" - "@types/wrap-ansi": "npm:^3.0.0" + "@inquirer/figures": "npm:^1.0.8" + "@inquirer/type": "npm:^3.0.1" ansi-escapes: "npm:^4.3.2" cli-width: "npm:^4.1.0" - mute-stream: "npm:^1.0.0" + mute-stream: "npm:^2.0.0" signal-exit: "npm:^4.1.0" strip-ansi: "npm:^6.0.1" wrap-ansi: "npm:^6.2.0" yoctocolors-cjs: "npm:^2.1.2" - checksum: 10/bf35e46e70add8ffa9e9d4ae6b528ac660484afca082bca31af95ce8a145a2f8c8d0d07cc7a8627771452e68ade9849c9c9c450a004133ed10ac2d6730900452 + checksum: 10/5d097d0484c1b758f788b792d29395199bdc84af3e8cd4d9273e31de2c5202839b6edf299056956044ba7fb097c4cee7b5c0288e094a380c045082b044f9946e languageName: node linkType: hard -"@inquirer/figures@npm:^1.0.6": - version: 1.0.7 - resolution: "@inquirer/figures@npm:1.0.7" - checksum: 10/ce896860de9d822a7c2a212667bcfd0f04cf2ce86d9a2411cc9c077bb59cd61732cb5f72ac66e88d52912466eec433f005bf8a25efa658f41e1a32f3977080bd +"@inquirer/figures@npm:^1.0.8": + version: 1.0.8 + resolution: "@inquirer/figures@npm:1.0.8" + checksum: 10/0e5e4fbb15e799e818c598fcc3558ef076daf78662149711b046723fd6316381e95f7d5573d6ef0062095ad22c6ac98833033f0948df5c722932107a567fd9c3 languageName: node linkType: hard -"@inquirer/type@npm:^1.5.3": - version: 1.5.5 - resolution: "@inquirer/type@npm:1.5.5" - dependencies: - mute-stream: "npm:^1.0.0" - checksum: 10/bd3f3d7510785af4ad599e042e99e4be6380f52f79f3db140fe6fed0a605acf27b1a0a20fb5cc688eaf7b8aa0c36dacb1d89c7bba4586f38cbf58ba9f159e7b5 - languageName: node - linkType: hard - -"@inquirer/type@npm:^2.0.0": - version: 2.0.0 - resolution: "@inquirer/type@npm:2.0.0" - dependencies: - mute-stream: "npm:^1.0.0" - checksum: 10/e85f359866c28cce06272d2d51cc17788a5c9de9fda7f181c27775dd26821de0dacbc947b521cfe2009cd2965ec54696799035ef3a25a9a5794e47d8e8bdf794 +"@inquirer/type@npm:^3.0.1": + version: 3.0.1 + resolution: "@inquirer/type@npm:3.0.1" + peerDependencies: + "@types/node": ">=18" + checksum: 10/af412f1e7541d43554b02199ae71a2039a1bff5dc51ceefd87de9ece55b199682733b28810fb4b6cb3ed4a159af4cc4a26d4bb29c58dd127e7d9dbda0797d8e7 languageName: node linkType: hard @@ -1287,7 +1312,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: @@ -1309,9 +1334,9 @@ __metadata: languageName: node linkType: hard -"@mswjs/interceptors@npm:^0.35.8": - version: 0.35.9 - resolution: "@mswjs/interceptors@npm:0.35.9" +"@mswjs/interceptors@npm:^0.37.0": + version: 0.37.3 + resolution: "@mswjs/interceptors@npm:0.37.3" dependencies: "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/logger": "npm:^0.3.0" @@ -1319,7 +1344,7 @@ __metadata: is-node-process: "npm:^1.2.0" outvariant: "npm:^1.4.3" strict-event-emitter: "npm:^0.5.1" - checksum: 10/9eaf8d7876c9a38c2c9a1259873f8ad27ab41c68a49f7e14a55cd9f596458d9232adb85a5084b044d4eead3be1e7ef5bf54ed6d774d16b02d96caf1e7faa2ab3 + checksum: 10/3d3e2e073feead8702c18dc97e5201785865292b32bd882c4d80461adc3380483b33517c55d7c6c1e53723f5e2ecf50cca0412e6ecd2eb771f4eaabfa2138932 languageName: node linkType: hard @@ -1780,11 +1805,11 @@ __metadata: linkType: soft "@storybook/components@file:../../../code/deprecated/components::locator=portable-stories-react%40workspace%3A.": - version: 8.5.0-alpha.18 - resolution: "@storybook/components@file:../../../code/deprecated/components#../../../code/deprecated/components::hash=aad2fe&locator=portable-stories-react%40workspace%3A." + version: 8.5.0-alpha.20 + resolution: "@storybook/components@file:../../../code/deprecated/components#../../../code/deprecated/components::hash=40954d&locator=portable-stories-react%40workspace%3A." peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/44d4a8a54fd32d94b03dc9f29e2d68ca3c37156b56055b94493f5f626786a02349c195859c48b4f8fa25e209ce12f385305d601135f7e63bdc4541fa0645cae5 + checksum: 10/ff9107e9c84c5d522a0067f77e217a9963c4bdde27032fdd38efae906f67df7b65488969a9cee89715d49fa89213fe04a72af825ac118e72e835f4be648409ff languageName: node linkType: hard @@ -1840,15 +1865,14 @@ __metadata: linkType: hard "@storybook/experimental-addon-test@file:../../../code/addons/test::locator=portable-stories-react%40workspace%3A.": - version: 8.5.0-alpha.18 - resolution: "@storybook/experimental-addon-test@file:../../../code/addons/test#../../../code/addons/test::hash=21369f&locator=portable-stories-react%40workspace%3A." + version: 8.5.0-alpha.20 + resolution: "@storybook/experimental-addon-test@file:../../../code/addons/test#../../../code/addons/test::hash=1aa098&locator=portable-stories-react%40workspace%3A." dependencies: "@storybook/csf": "npm:0.1.12" "@storybook/global": "npm:^5.0.0" "@storybook/icons": "npm:^1.2.12" "@storybook/instrumenter": "workspace:*" "@storybook/test": "workspace:*" - "@storybook/theming": "workspace:*" polished: "npm:^4.2.2" prompts: "npm:^2.4.0" ts-dedent: "npm:^2.2.0" @@ -1864,7 +1888,7 @@ __metadata: optional: true vitest: optional: true - checksum: 10/2081814e214dc1dd31144870a6a4ea7637c9c241ab02044488be57e19402c206c0037d449197f77bb4262147703f6d0b27f09c9f6cc2ee358c97fd7d1cdfa908 + checksum: 10/42188bb3814cae7c04df03c75994f8df971cc2ce71bda9e536211b96468bbdf2b8babc78526fdec1b2ddbd4d703118474f23c31f2bddffb6fa8947dc35ef0f19 languageName: node linkType: hard @@ -1897,20 +1921,20 @@ __metadata: linkType: soft "@storybook/manager-api@file:../../../code/deprecated/manager-api::locator=portable-stories-react%40workspace%3A.": - version: 8.5.0-alpha.18 - resolution: "@storybook/manager-api@file:../../../code/deprecated/manager-api#../../../code/deprecated/manager-api::hash=c1892e&locator=portable-stories-react%40workspace%3A." + version: 8.5.0-alpha.20 + resolution: "@storybook/manager-api@file:../../../code/deprecated/manager-api#../../../code/deprecated/manager-api::hash=92b213&locator=portable-stories-react%40workspace%3A." peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/405745f48728bfa4d8340fe2403bca0d60f803ed346c12d20a63ab9472ba1d8d1021aa12ee0fd36a6b41aec42d8c2a657acffeb9c8663aa32edcda490882e7f5 + checksum: 10/c63e624ab2d90f2992ecb2a650d9e54ace0f7f699d425bc68f616f870651a2b5f3c6c7d66be84aaab9200a9f01686376a7532f905f0dbd4dddb5542afe855584 languageName: node linkType: hard "@storybook/preview-api@file:../../../code/deprecated/preview-api::locator=portable-stories-react%40workspace%3A.": - version: 8.5.0-alpha.18 - resolution: "@storybook/preview-api@file:../../../code/deprecated/preview-api#../../../code/deprecated/preview-api::hash=0085a8&locator=portable-stories-react%40workspace%3A." + version: 8.5.0-alpha.20 + resolution: "@storybook/preview-api@file:../../../code/deprecated/preview-api#../../../code/deprecated/preview-api::hash=b70449&locator=portable-stories-react%40workspace%3A." peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/29c5f7134b4300bac03c42f57c3138e24315516b23515893cfb4a144786327fb6e0a90d5470d951eb22e055466769fc9ad4577658afc7da43b60870c0d06e767 + checksum: 10/3251de34aca72eb6c82b7aa8fde6e877e0974294d97eb60f63819b890b399cd7703a4dfde151180cf81918b36b3523bdbafe0731d232bda7d77c9f3b8fc5b75b languageName: node linkType: hard @@ -1991,11 +2015,11 @@ __metadata: linkType: soft "@storybook/theming@file:../../../code/deprecated/theming::locator=portable-stories-react%40workspace%3A.": - version: 8.5.0-alpha.18 - resolution: "@storybook/theming@file:../../../code/deprecated/theming#../../../code/deprecated/theming::hash=dd5360&locator=portable-stories-react%40workspace%3A." + version: 8.5.0-alpha.20 + resolution: "@storybook/theming@file:../../../code/deprecated/theming#../../../code/deprecated/theming::hash=db0fbd&locator=portable-stories-react%40workspace%3A." peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/8f8ecbd709ff4a8e0bafa19642497aad0d6ee40f2e613793a4248cb3825765783dd40eb57d180cb6cc4ad1f8acbd4e4a1a1b3448245b89e28581e25289c88c86 + checksum: 10/de77ed744ce172b1007cf94b4b5ff1cf9cf8492d4ddb9ac1ca61d76b18ed73f76e911c1518c1b66ba37c8d2d4e0f63f2eb3bddc64cd0beb7d64f952a45f9b8b7 languageName: node linkType: hard @@ -2356,16 +2380,7 @@ __metadata: languageName: node linkType: hard -"@types/mute-stream@npm:^0.0.4": - version: 0.0.4 - resolution: "@types/mute-stream@npm:0.0.4" - dependencies: - "@types/node": "npm:*" - checksum: 10/af8d83ad7b68ea05d9357985daf81b6c9b73af4feacb2f5c2693c7fd3e13e5135ef1bd083ce8d5bdc8e97acd28563b61bb32dec4e4508a8067fcd31b8a098632 - languageName: node - linkType: hard - -"@types/node@npm:*, @types/node@npm:^22.5.5": +"@types/node@npm:*": version: 22.7.7 resolution: "@types/node@npm:22.7.7" dependencies: @@ -2456,13 +2471,6 @@ __metadata: languageName: node linkType: hard -"@types/wrap-ansi@npm:^3.0.0": - version: 3.0.0 - resolution: "@types/wrap-ansi@npm:3.0.0" - checksum: 10/8aa644946ca4e859668c36b8e2bcf2ac4bdee59dac760414730ea57be8a93ae9166ebd40a088f2ab714843aaea2a2a67f0e6e6ec11cfc9c8701b2466ca1c4089 - languageName: node - linkType: hard - "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -2697,21 +2705,21 @@ __metadata: linkType: hard "@vitest/browser@npm:^2.1.3": - version: 2.1.3 - resolution: "@vitest/browser@npm:2.1.3" + version: 2.1.8 + resolution: "@vitest/browser@npm:2.1.8" dependencies: "@testing-library/dom": "npm:^10.4.0" "@testing-library/user-event": "npm:^14.5.2" - "@vitest/mocker": "npm:2.1.3" - "@vitest/utils": "npm:2.1.3" - magic-string: "npm:^0.30.11" - msw: "npm:^2.3.5" - sirv: "npm:^2.0.4" + "@vitest/mocker": "npm:2.1.8" + "@vitest/utils": "npm:2.1.8" + magic-string: "npm:^0.30.12" + msw: "npm:^2.6.4" + sirv: "npm:^3.0.0" tinyrainbow: "npm:^1.2.0" ws: "npm:^8.18.0" peerDependencies: playwright: "*" - vitest: 2.1.3 + vitest: 2.1.8 webdriverio: "*" peerDependenciesMeta: playwright: @@ -2720,7 +2728,33 @@ __metadata: optional: true webdriverio: optional: true - checksum: 10/e639496fa529140fb9e7dce97890c5b75fffbfb41881bee5ef25b194832d3cadcb77490d9b54777bfa968b993f6878649fe4961d6ef312ca1222b9a2fc8d4f12 + checksum: 10/6063e02222440347bbc23b2c54e259078aa83a29869337b9ffd642be5a4321ac3ddf3c0bbe4eac5237eb0bb8b9fa17d21d2c31299376de407716e3c7dd3b704c + languageName: node + linkType: hard + +"@vitest/coverage-v8@npm:^2.1.3": + version: 2.1.8 + resolution: "@vitest/coverage-v8@npm:2.1.8" + dependencies: + "@ampproject/remapping": "npm:^2.3.0" + "@bcoe/v8-coverage": "npm:^0.2.3" + debug: "npm:^4.3.7" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-lib-source-maps: "npm:^5.0.6" + istanbul-reports: "npm:^3.1.7" + magic-string: "npm:^0.30.12" + magicast: "npm:^0.3.5" + std-env: "npm:^3.8.0" + test-exclude: "npm:^7.0.1" + tinyrainbow: "npm:^1.2.0" + peerDependencies: + "@vitest/browser": 2.1.8 + vitest: 2.1.8 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10/2e1e7fe2a20c1eec738f6d84d890bed4aa5138094943dd1229962c2c42428a1a517c8a4ad4fb52637d7494f044440e061e9bc5982a83df95223db185d5a28f4d languageName: node linkType: hard @@ -2736,35 +2770,34 @@ __metadata: languageName: node linkType: hard -"@vitest/expect@npm:2.1.3": - version: 2.1.3 - resolution: "@vitest/expect@npm:2.1.3" +"@vitest/expect@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/expect@npm:2.1.8" dependencies: - "@vitest/spy": "npm:2.1.3" - "@vitest/utils": "npm:2.1.3" - chai: "npm:^5.1.1" + "@vitest/spy": "npm:2.1.8" + "@vitest/utils": "npm:2.1.8" + chai: "npm:^5.1.2" tinyrainbow: "npm:^1.2.0" - checksum: 10/94e61e01f14cfcd9ced0e7ac1bbdeee55ff4bf68f09d8f244fd7d73f97b106f35d10cba3fe7a0132464c312206f2eee9e83b16a8d761101b61da053890062858 + checksum: 10/3594149dd67dfac884a90f8b6a35687cdddd2f5f764562819bf7b66ae2eacfd4aa5e8914155deb4082fbe5a3792dced2fd7e59a948ffafe67acba4d2229dfe5f languageName: node linkType: hard -"@vitest/mocker@npm:2.1.3": - version: 2.1.3 - resolution: "@vitest/mocker@npm:2.1.3" +"@vitest/mocker@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/mocker@npm:2.1.8" dependencies: - "@vitest/spy": "npm:2.1.3" + "@vitest/spy": "npm:2.1.8" estree-walker: "npm:^3.0.3" - magic-string: "npm:^0.30.11" + magic-string: "npm:^0.30.12" peerDependencies: - "@vitest/spy": 2.1.3 - msw: ^2.3.5 + msw: ^2.4.9 vite: ^5.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - checksum: 10/84be8830d6e965109730257d7a84b3d7594db0998ae55decdbfc304857c1c7d29b49f1f5b23f2addcbce1bd7e8bb33832407737a9bb3f95cb3bf7bb312db4d9d + checksum: 10/f04060f42102caa4cca72059e63c1ecae8b8e091aaa61a2d4a914b129fc711ada4ad117eb0184e49e363757784ed1117fdbf9f4a81a45fe575fd92769740a970 languageName: node linkType: hard @@ -2777,33 +2810,33 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:2.1.3, @vitest/pretty-format@npm:^2.1.3": - version: 2.1.3 - resolution: "@vitest/pretty-format@npm:2.1.3" +"@vitest/pretty-format@npm:2.1.8, @vitest/pretty-format@npm:^2.1.8": + version: 2.1.8 + resolution: "@vitest/pretty-format@npm:2.1.8" dependencies: tinyrainbow: "npm:^1.2.0" - checksum: 10/d9382ee93f0f32e2ef8fe03bda818e5277f052a50ddb05b6a6cf0864b2ccb228484f12f130c05faf62dc2140292ffafc213f2941b0fa24058b3ee2943daa286c + checksum: 10/f0f60c007424194887ad398d202867d58d850154de327993925041e2972357544eea95a22e0bb3a62a470b006ff8de5f691d2078708dcd7f625e24f8a06b26e7 languageName: node linkType: hard -"@vitest/runner@npm:2.1.3": - version: 2.1.3 - resolution: "@vitest/runner@npm:2.1.3" +"@vitest/runner@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/runner@npm:2.1.8" dependencies: - "@vitest/utils": "npm:2.1.3" + "@vitest/utils": "npm:2.1.8" pathe: "npm:^1.1.2" - checksum: 10/cdf9b82d388c1cc148753f4a8632dfcadf9c4a1c0e065fdcd485d5af824af62507fd7eab9efb21244009775c05773ccb59547043af522a5ab6d216433321066e + checksum: 10/27f265a3ab1e20297b948b06232bfa4dc9fda44d1f9bb6206baa9e6fa643b71143ebfd2d1771570296b7ee74a12d684e529a830f545ad61235cefb454e94a8e9 languageName: node linkType: hard -"@vitest/snapshot@npm:2.1.3": - version: 2.1.3 - resolution: "@vitest/snapshot@npm:2.1.3" +"@vitest/snapshot@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/snapshot@npm:2.1.8" dependencies: - "@vitest/pretty-format": "npm:2.1.3" - magic-string: "npm:^0.30.11" + "@vitest/pretty-format": "npm:2.1.8" + magic-string: "npm:^0.30.12" pathe: "npm:^1.1.2" - checksum: 10/2c0c4ad8abb758f2f76d1d6094f8928360437e09d0a59e0c6a85a544c892cc41a5324ebbc5657a66c8a3793e51cbf58e357c7f71e899f4e5c5eb76e8c9745abf + checksum: 10/71edf4f574d317579c605ed0a7ecab7ee96fddcebc777bd130774a770ddc692c538f9f5b3dfde89af83ecb36f7338fe880943c83cede58f55e3556768a1a0749 languageName: node linkType: hard @@ -2816,12 +2849,29 @@ __metadata: languageName: node linkType: hard -"@vitest/spy@npm:2.1.3": - version: 2.1.3 - resolution: "@vitest/spy@npm:2.1.3" +"@vitest/spy@npm:2.1.8": + version: 2.1.8 + resolution: "@vitest/spy@npm:2.1.8" dependencies: - tinyspy: "npm:^3.0.0" - checksum: 10/94d6f1bc34da5d0c973d9382c133b938e555fcf2d238edf0aaad3de1a98dd57ebf7c104ba229c6beec48122d2e6f55386d8d2cf96a5804dc95ac683a54754cc7 + tinyspy: "npm:^3.0.2" + checksum: 10/9a1cb9cf6b23c122681469b5890d91ca26fc8d74953b3d46d293a5d2a4944490106891f6a178cd732ab7a8abbda339f43681c81d1594565ecc3bf3e7f9b7735f + languageName: node + linkType: hard + +"@vitest/ui@npm:^2.1.3": + version: 2.1.8 + resolution: "@vitest/ui@npm:2.1.8" + dependencies: + "@vitest/utils": "npm:2.1.8" + fflate: "npm:^0.8.2" + flatted: "npm:^3.3.1" + pathe: "npm:^1.1.2" + sirv: "npm:^3.0.0" + tinyglobby: "npm:^0.2.10" + tinyrainbow: "npm:^1.2.0" + peerDependencies: + vitest: 2.1.8 + checksum: 10/7ff0532b3b0e3f93c037cad3528b8fde8a93188f3222b92faae42e0fdd996e9284b362c4e5e9d725ce0a019870d3b0b7ad80c1874f74b43ffc5a1d703803fdd8 languageName: node linkType: hard @@ -2837,14 +2887,14 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:2.1.3, @vitest/utils@npm:^2.1.1": - version: 2.1.3 - resolution: "@vitest/utils@npm:2.1.3" +"@vitest/utils@npm:2.1.8, @vitest/utils@npm:^2.1.1": + version: 2.1.8 + resolution: "@vitest/utils@npm:2.1.8" dependencies: - "@vitest/pretty-format": "npm:2.1.3" - loupe: "npm:^3.1.1" + "@vitest/pretty-format": "npm:2.1.8" + loupe: "npm:^3.1.2" tinyrainbow: "npm:^1.2.0" - checksum: 10/f064e6634cb84c925a17d8937df7441d150c3e24fa5bbd6304151d11dab6cdeb0cb3d5a95a9aacb8b416c87fb0d9aa8c6b9cc5e174191784231e8345948d6d18 + checksum: 10/be1f4254347199fb5c1d9de8e4537dad4af3f434c033e7cd023165bd4b7e9de16fa0f86664256ab331120585df95ed6be8eea58b209b510651b49f6482051733 languageName: node linkType: hard @@ -3455,16 +3505,16 @@ __metadata: languageName: node linkType: hard -"chai@npm:^5.1.1": - version: 5.1.1 - resolution: "chai@npm:5.1.1" +"chai@npm:^5.1.1, chai@npm:^5.1.2": + version: 5.1.2 + resolution: "chai@npm:5.1.2" dependencies: assertion-error: "npm:^2.0.1" check-error: "npm:^2.1.1" deep-eql: "npm:^5.0.1" loupe: "npm:^3.1.0" pathval: "npm:^2.0.0" - checksum: 10/ee67279a5613bd36dc1dc13660042429ae2f1dc5a9030a6abcf381345866dfb5bce7bc10b9d74c8de86b6f656489f654bbbef3f3361e06925591e6a00c72afff + checksum: 10/e8c2bbc83cb5a2f87130d93056d4cfbbe04106e12aa798b504816dbe3fa538a9f68541b472e56cbf0f54558b501d7e31867d74b8218abcd5a8cc8ba536fba46c languageName: node linkType: hard @@ -3695,10 +3745,10 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.5.0": - version: 0.5.0 - resolution: "cookie@npm:0.5.0" - checksum: 10/aae7911ddc5f444a9025fbd979ad1b5d60191011339bce48e555cb83343d0f98b865ff5c4d71fecdfb8555a5cafdc65632f6fce172f32aaf6936830a883a0380 +"cookie@npm:^0.7.2": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 10/24b286c556420d4ba4e9bc09120c9d3db7d28ace2bd0f8ccee82422ce42322f73c8312441271e5eefafbead725980e5996cc02766dbb89a90ac7f5636ede608f languageName: node linkType: hard @@ -3853,7 +3903,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.7": version: 4.3.7 resolution: "debug@npm:4.3.7" dependencies: @@ -4174,6 +4224,13 @@ __metadata: languageName: node linkType: hard +"es-module-lexer@npm:^1.5.4": + version: 1.5.4 + resolution: "es-module-lexer@npm:1.5.4" + checksum: 10/f29c7c97a58eb17640dcbd71bd6ef754ad4f58f95c3073894573d29dae2cad43ecd2060d97ed5b866dfb7804d5590fb7de1d2c5339a5fceae8bd60b580387fc5 + languageName: node + linkType: hard + "esbuild-register@npm:^3.5.0": version: 3.6.0 resolution: "esbuild-register@npm:3.6.0" @@ -4634,6 +4691,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.1.0": + version: 1.1.0 + resolution: "expect-type@npm:1.1.0" + checksum: 10/05fca80ddc7d493a89361f783c6b000750fa04a8226bc24701f3b90adb0efc2fb467f2a0baaed4015a02d8b9034ef5bb87521df9dba980f50b1105bd596ef833 + languageName: node + linkType: hard + "expect@npm:^29.7.0": version: 29.7.0 resolution: "expect@npm:29.7.0" @@ -4753,6 +4817,25 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.4.2": + version: 6.4.2 + resolution: "fdir@npm:6.4.2" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10/5ff80d1d2034e75cc68be175401c9f64c4938a6b2c1e9a0c27f2d211ffbe491fd86d29e4576825d9da8aff9bd465f0283427c2dddc11653457906c46d3bbc448 + languageName: node + linkType: hard + +"fflate@npm:^0.8.2": + version: 0.8.2 + resolution: "fflate@npm:0.8.2" + checksum: 10/2bd26ba6d235d428de793c6a0cd1aaa96a06269ebd4e21b46c8fd1bd136abc631acf27e188d47c3936db090bf3e1ede11d15ce9eae9bffdc4bfe1b9dc66ca9cb + languageName: node + linkType: hard + "figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -4818,6 +4901,13 @@ __metadata: languageName: node linkType: hard +"flatted@npm:^3.3.1": + version: 3.3.2 + resolution: "flatted@npm:3.3.2" + checksum: 10/ac3c159742e01d0e860a861164bcfd35bb567ccbebb8a0dd041e61cf3c64a435b917dd1e7ed1c380c2ebca85735fb16644485ec33665bc6aafc3b316aa1eed44 + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -5030,7 +5120,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10": +"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.1": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -5726,7 +5816,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0, istanbul-lib-coverage@npm:^3.2.2": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" checksum: 10/40bbdd1e937dfd8c830fa286d0f665e81b7a78bdabcd4565f6d5667c99828bda3db7fb7ac6b96a3e2e8a2461ddbc5452d9f8bc7d00cb00075fa6a3e99f5b6a81 @@ -5759,7 +5849,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-report@npm:^3.0.0": +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": version: 3.0.1 resolution: "istanbul-lib-report@npm:3.0.1" dependencies: @@ -5781,7 +5871,18 @@ __metadata: languageName: node linkType: hard -"istanbul-reports@npm:^3.1.3": +"istanbul-lib-source-maps@npm:^5.0.6": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.23" + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + checksum: 10/569dd0a392ee3464b1fe1accbaef5cc26de3479eacb5b91d8c67ebb7b425d39fd02247d85649c3a0e9c29b600809fa60b5af5a281a75a89c01f385b1e24823a2 + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.3, istanbul-reports@npm:^3.1.7": version: 3.1.7 resolution: "istanbul-reports@npm:3.1.7" dependencies: @@ -6593,7 +6694,7 @@ __metadata: languageName: node linkType: hard -"loupe@npm:^3.1.0, loupe@npm:^3.1.1": +"loupe@npm:^3.1.0, loupe@npm:^3.1.1, loupe@npm:^3.1.2": version: 3.1.2 resolution: "loupe@npm:3.1.2" checksum: 10/8f5734e53fb64cd914aa7d986e01b6d4c2e3c6c56dcbd5428d71c2703f0ab46b5ab9f9eeaaf2b485e8a1c43f865bdd16ec08ae1a661c8f55acdbd9f4d59c607a @@ -6634,7 +6735,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.0, magic-string@npm:^0.30.11": +"magic-string@npm:^0.30.0": version: 0.30.12 resolution: "magic-string@npm:0.30.12" dependencies: @@ -6643,6 +6744,26 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.12": + version: 0.30.14 + resolution: "magic-string@npm:0.30.14" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.0" + checksum: 10/8ca0f8937c2824e48ebc70e7e065a193c467713639cc6e5972aaba0fa5417b375a6f62c383410a19a66e618c386bb7253fbd3ccbfb0144bb310f0ba772121f12 + languageName: node + linkType: hard + +"magicast@npm:^0.3.5": + version: 0.3.5 + resolution: "magicast@npm:0.3.5" + dependencies: + "@babel/parser": "npm:^7.25.4" + "@babel/types": "npm:^7.25.4" + source-map-js: "npm:^1.2.0" + checksum: 10/3a2dba6b0bdde957797361d09c7931ebdc1b30231705360eeb40ed458d28e1c3112841c3ed4e1b87ceb28f741e333c7673cd961193aa9fdb4f4946b202e6205a + languageName: node + linkType: hard + "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -6892,15 +7013,16 @@ __metadata: languageName: node linkType: hard -"msw@npm:^2.3.5": - version: 2.4.11 - resolution: "msw@npm:2.4.11" +"msw@npm:^2.6.4": + version: 2.6.6 + resolution: "msw@npm:2.6.6" dependencies: - "@bundled-es-modules/cookie": "npm:^2.0.0" + "@bundled-es-modules/cookie": "npm:^2.0.1" "@bundled-es-modules/statuses": "npm:^1.0.1" "@bundled-es-modules/tough-cookie": "npm:^0.1.6" - "@inquirer/confirm": "npm:^3.0.0" - "@mswjs/interceptors": "npm:^0.35.8" + "@inquirer/confirm": "npm:^5.0.0" + "@mswjs/interceptors": "npm:^0.37.0" + "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/until": "npm:^2.1.0" "@types/cookie": "npm:^0.6.0" "@types/statuses": "npm:^2.0.4" @@ -6920,14 +7042,14 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10/d073ede4bfc7f1f41f7a0cb05b3d20d9befc1658e53faacd3f217a7cb78e3e748a3ee8e937e2a4d93fd09f16b35cba00d71df767736dd567ac15fd8e01aa7d6e + checksum: 10/7762ba5f1570789328af27167e03c2b8eb4981faa476ae47d74c125c90ddc1792bc28b9ce1100bbc4e105b55e3e7d65e7cae8d27fa7677b6516e42a63c38b7a3 languageName: node linkType: hard -"mute-stream@npm:^1.0.0": - version: 1.0.0 - resolution: "mute-stream@npm:1.0.0" - checksum: 10/36fc968b0e9c9c63029d4f9dc63911950a3bdf55c9a87f58d3a266289b67180201cade911e7699f8b2fa596b34c9db43dad37649e3f7fdd13c3bb9edb0017ee7 +"mute-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "mute-stream@npm:2.0.0" + checksum: 10/d2e4fd2f5aa342b89b98134a8d899d8ef9b0a6d69274c4af9df46faa2d97aeb1f2ce83d867880d6de63643c52386579b99139801e24e7526c3b9b0a6d1e18d6c languageName: node linkType: hard @@ -7298,6 +7420,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.2": + version: 4.0.2 + resolution: "picomatch@npm:4.0.2" + checksum: 10/ce617b8da36797d09c0baacb96ca8a44460452c89362d7cb8f70ca46b4158ba8bc3606912de7c818eb4a939f7f9015cef3c766ec8a0c6bfc725fdc078e39c717 + languageName: node + linkType: hard + "pify@npm:^2.2.0": version: 2.3.0 resolution: "pify@npm:2.3.0" @@ -7380,6 +7509,8 @@ __metadata: "@typescript-eslint/parser": "npm:^6.21.0" "@vitejs/plugin-react": "npm:^4.2.1" "@vitest/browser": "npm:^2.1.3" + "@vitest/coverage-v8": "npm:^2.1.3" + "@vitest/ui": "npm:^2.1.3" cypress: "npm:^13.6.4" eslint: "npm:^8.56.0" eslint-plugin-react-hooks: "npm:^4.6.0" @@ -7991,14 +8122,14 @@ __metadata: languageName: node linkType: hard -"sirv@npm:^2.0.4": - version: 2.0.4 - resolution: "sirv@npm:2.0.4" +"sirv@npm:^3.0.0": + version: 3.0.0 + resolution: "sirv@npm:3.0.0" dependencies: "@polka/url": "npm:^1.0.0-next.24" mrmime: "npm:^2.0.0" totalist: "npm:^3.0.0" - checksum: 10/24f42cf06895017e589c9d16fc3f1c6c07fe8b0dbafce8a8b46322cfba67b7f2498610183954cb0e9d089c8cb60002a7ee7e8bca6a91a0d7042bfbc3473c95c3 + checksum: 10/94dbd5df7cf4965f7c5941767117cbf9709e1d25de1d619a114c3f77fc63c124b5a5255717af2a0de637bb83d0b0defd0822d01420764b56432b53281b1d675d languageName: node linkType: hard @@ -8066,7 +8197,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.2.1": +"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3 @@ -8157,10 +8288,10 @@ __metadata: languageName: node linkType: hard -"std-env@npm:^3.7.0": - version: 3.7.0 - resolution: "std-env@npm:3.7.0" - checksum: 10/6ee0cca1add3fd84656b0002cfbc5bfa20340389d9ba4720569840f1caa34bce74322aef4c93f046391583e50649d0cf81a5f8fe1d411e50b659571690a45f12 +"std-env@npm:^3.8.0": + version: 3.8.0 + resolution: "std-env@npm:3.8.0" + checksum: 10/034176196cfcaaab16dbdd96fc9e925a9544799fb6dc5a3e36fe43270f3a287c7f779d785b89edaf22cef2b5f1dcada2aae67430b8602e785ee74bdb3f671768 languageName: node linkType: hard @@ -8359,6 +8490,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^7.0.1": + version: 7.0.1 + resolution: "test-exclude@npm:7.0.1" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^10.4.1" + minimatch: "npm:^9.0.4" + checksum: 10/e6f6f4e1df2e7810e082e8d7dfc53be51a931e6e87925f5e1c2ef92cc1165246ba3bf2dae6b5d86251c16925683dba906bd41e40169ebc77120a2d1b5a0dbbe0 + languageName: node + linkType: hard + "text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" @@ -8394,17 +8536,27 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^0.3.0": +"tinyexec@npm:^0.3.1": version: 0.3.1 resolution: "tinyexec@npm:0.3.1" checksum: 10/0537c70590d52d354f40c0255ff0f654a3d18ddb3812b440ddf9d436edf516c8057838ad5a38744c0c59670ec03e3cf23fbe04ae3d49f031d948274e99002569 languageName: node linkType: hard -"tinypool@npm:^1.0.0": - version: 1.0.1 - resolution: "tinypool@npm:1.0.1" - checksum: 10/eaceb93784b8e27e60c0e3e2c7d11c29e1e79b2a025b2c232215db73b90fe22bd4753ad53fc8e801c2b5a63b94a823af549555d8361272bc98271de7dd4a9925 +"tinyglobby@npm:^0.2.10": + version: 0.2.10 + resolution: "tinyglobby@npm:0.2.10" + dependencies: + fdir: "npm:^6.4.2" + picomatch: "npm:^4.0.2" + checksum: 10/10c976866d849702edc47fc3fef27d63f074c40f75ef17171ecc1452967900699fa1e62373681dd58e673ddff2e3f6094bcd0a2101e3e4b30f4c2b9da41397f2 + languageName: node + linkType: hard + +"tinypool@npm:^1.0.1": + version: 1.0.2 + resolution: "tinypool@npm:1.0.2" + checksum: 10/6109322f14b3763f65c8fa49fddab72cd3edd96b82dd50e05e63de74867329ff5353bff4377281ec963213d9314f37f4a353e9ee34bbac85fd4c1e4a568d6076 languageName: node linkType: hard @@ -8415,7 +8567,7 @@ __metadata: languageName: node linkType: hard -"tinyspy@npm:^3.0.0": +"tinyspy@npm:^3.0.0, tinyspy@npm:^3.0.2": version: 3.0.2 resolution: "tinyspy@npm:3.0.2" checksum: 10/5db671b2ff5cd309de650c8c4761ca945459d7204afb1776db9a04fb4efa28a75f08517a8620c01ee32a577748802231ad92f7d5b194dc003ee7f987a2a06337 @@ -8759,17 +8911,18 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:2.1.3": - version: 2.1.3 - resolution: "vite-node@npm:2.1.3" +"vite-node@npm:2.1.8": + version: 2.1.8 + resolution: "vite-node@npm:2.1.8" dependencies: cac: "npm:^6.7.14" - debug: "npm:^4.3.6" + debug: "npm:^4.3.7" + es-module-lexer: "npm:^1.5.4" pathe: "npm:^1.1.2" vite: "npm:^5.0.0" bin: vite-node: vite-node.mjs - checksum: 10/8ba6b145cbb02a492c7bb1f0490d02383000462f234ed61d24f650547163825c16f14e6908ee1eb661403bd0a7a3fb3cdbedf116cc015b1e5cdf7bb992872a01 + checksum: 10/0ff0ed7a6fb234d3ddc4946e4c1150229980cac9f34fb4bd7f443aab0aae2da5b73ac20ff68af1df476545807dc23189247194e8cea0dcdfa394311c73f04429 languageName: node linkType: hard @@ -8833,33 +8986,34 @@ __metadata: linkType: hard "vitest@npm:^2.1.3": - version: 2.1.3 - resolution: "vitest@npm:2.1.3" + version: 2.1.8 + resolution: "vitest@npm:2.1.8" dependencies: - "@vitest/expect": "npm:2.1.3" - "@vitest/mocker": "npm:2.1.3" - "@vitest/pretty-format": "npm:^2.1.3" - "@vitest/runner": "npm:2.1.3" - "@vitest/snapshot": "npm:2.1.3" - "@vitest/spy": "npm:2.1.3" - "@vitest/utils": "npm:2.1.3" - chai: "npm:^5.1.1" - debug: "npm:^4.3.6" - magic-string: "npm:^0.30.11" + "@vitest/expect": "npm:2.1.8" + "@vitest/mocker": "npm:2.1.8" + "@vitest/pretty-format": "npm:^2.1.8" + "@vitest/runner": "npm:2.1.8" + "@vitest/snapshot": "npm:2.1.8" + "@vitest/spy": "npm:2.1.8" + "@vitest/utils": "npm:2.1.8" + chai: "npm:^5.1.2" + debug: "npm:^4.3.7" + expect-type: "npm:^1.1.0" + magic-string: "npm:^0.30.12" pathe: "npm:^1.1.2" - std-env: "npm:^3.7.0" + std-env: "npm:^3.8.0" tinybench: "npm:^2.9.0" - tinyexec: "npm:^0.3.0" - tinypool: "npm:^1.0.0" + tinyexec: "npm:^0.3.1" + tinypool: "npm:^1.0.1" tinyrainbow: "npm:^1.2.0" vite: "npm:^5.0.0" - vite-node: "npm:2.1.3" + vite-node: "npm:2.1.8" why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 2.1.3 - "@vitest/ui": 2.1.3 + "@vitest/browser": 2.1.8 + "@vitest/ui": 2.1.8 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -8877,7 +9031,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10/f6079a88583045b551e6526c08774aeac4a9cf85b132793a03f9470c013326abd7fce3985e3c2217dc0dac2fadeee3506e3dc51e215f10862b2fe9da9289af0f + checksum: 10/c2552c068f6faac82eb4e6debb9ed505c0e8016fd6e0a0f0e0dbb5b5417922fbcde80c54af0d3b5a5503a5d6ad6862b6e95b9b59b8b7e98bb553217b9c6fc227 languageName: node linkType: hard