From 393e9350e207ea0a63c79128cc78e7387b0344fa Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Thu, 29 Feb 2024 10:43:05 +0800 Subject: [PATCH 01/43] React: Fix RSC compatibility with addon-themes --- code/renderers/react/src/entry-preview-rsc.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/code/renderers/react/src/entry-preview-rsc.tsx b/code/renderers/react/src/entry-preview-rsc.tsx index 96b04ab996ba..f19ba3366e85 100644 --- a/code/renderers/react/src/entry-preview-rsc.tsx +++ b/code/renderers/react/src/entry-preview-rsc.tsx @@ -1,13 +1,9 @@ import * as React from 'react'; import semver from 'semver'; import type { Addon_DecoratorFunction } from '@storybook/types'; -import type { StoryContext } from './types'; -export const ServerComponentDecorator = ( - Story: React.FC, - { parameters }: StoryContext -): React.ReactNode => { - if (!parameters?.react?.rsc) return ; +export const ServerComponentDecorator: Addon_DecoratorFunction = (story, { parameters }) => { + if (!parameters?.react?.rsc) return story(); const major = semver.major(React.version); const minor = semver.minor(React.version); @@ -15,14 +11,10 @@ export const ServerComponentDecorator = ( throw new Error('React Server Components require React >= 18.3'); } - return ( - - - - ); + return {story() as React.ReactNode}; }; -export const decorators: Addon_DecoratorFunction[] = [ServerComponentDecorator]; +export const decorators = [ServerComponentDecorator]; export const parameters = { react: { From 5461f739e21869d595f4210619d60e65e1c5b108 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 27 Sep 2024 15:45:25 +0200 Subject: [PATCH 02/43] support statsJson in angular schemas --- .../src/builders/build-storybook/schema.json | 35 +++++++++++++++---- .../src/builders/start-storybook/schema.json | 35 +++++++++++++++---- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/code/frameworks/angular/src/builders/build-storybook/schema.json b/code/frameworks/angular/src/builders/build-storybook/schema.json index dbc2a734417e..afd42aa4de07 100644 --- a/code/frameworks/angular/src/builders/build-storybook/schema.json +++ b/code/frameworks/angular/src/builders/build-storybook/schema.json @@ -67,16 +67,30 @@ "compodocArgs": { "type": "array", "description": "Compodoc options : https://compodoc.app/guides/options.html. Options `-p` with tsconfig path and `-d` with workspace root is always given.", - "default": ["-e", "json"], + "default": [ + "-e", + "json" + ], "items": { "type": "string" } }, "webpackStatsJson": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "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" @@ -113,7 +127,10 @@ } }, "sourceMap": { - "type": ["boolean", "object"], + "type": [ + "boolean", + "object" + ], "description": "Configure sourcemaps. See: https://angular.io/guide/workspace-config#source-map-configuration", "default": false } @@ -151,7 +168,11 @@ } }, "additionalProperties": false, - "required": ["glob", "input", "output"] + "required": [ + "glob", + "input", + "output" + ] }, { "type": "string" @@ -179,7 +200,9 @@ } }, "additionalProperties": false, - "required": ["input"] + "required": [ + "input" + ] }, { "type": "string", @@ -188,4 +211,4 @@ ] } } -} +} \ No newline at end of file diff --git a/code/frameworks/angular/src/builders/start-storybook/schema.json b/code/frameworks/angular/src/builders/start-storybook/schema.json index a2eef03e4b08..a96182ea1b1e 100644 --- a/code/frameworks/angular/src/builders/start-storybook/schema.json +++ b/code/frameworks/angular/src/builders/start-storybook/schema.json @@ -94,7 +94,10 @@ "compodocArgs": { "type": "array", "description": "Compodoc options : https://compodoc.app/guides/options.html. Options `-p` with tsconfig path and `-d` with workspace root is always given.", - "default": ["-e", "json"], + "default": [ + "-e", + "json" + ], "items": { "type": "string" } @@ -135,10 +138,21 @@ "description": "URL path to be appended when visiting Storybook for the first time" }, "webpackStatsJson": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "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" @@ -149,7 +163,10 @@ "pattern": "(silly|verbose|info|warn|silent)" }, "sourceMap": { - "type": ["boolean", "object"], + "type": [ + "boolean", + "object" + ], "description": "Configure sourcemaps. See: https://angular.io/guide/workspace-config#source-map-configuration", "default": false } @@ -187,7 +204,11 @@ } }, "additionalProperties": false, - "required": ["glob", "input", "output"] + "required": [ + "glob", + "input", + "output" + ] }, { "type": "string" @@ -215,7 +236,9 @@ } }, "additionalProperties": false, - "required": ["input"] + "required": [ + "input" + ] }, { "type": "string", @@ -224,4 +247,4 @@ ] } } -} +} \ No newline at end of file From fd7f82f1f620ba07de603f2135420cfcf451a2ba Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Tue, 5 Nov 2024 11:13:02 +0100 Subject: [PATCH 03/43] Addon Docs: Dynamically import rehype --- code/addons/docs/src/plugins/mdx-plugin.ts | 5 +++-- code/addons/docs/src/preset.ts | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/code/addons/docs/src/plugins/mdx-plugin.ts b/code/addons/docs/src/plugins/mdx-plugin.ts index 40d8e88c3f3a..36fe3b691e97 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 e418b5e2e8aa..9cff707e8472 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); From 2a02e134805f9659481f90237478a39516322c44 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Tue, 19 Nov 2024 18:14:41 +1100 Subject: [PATCH 04/43] Fix webpack fsCache not working with @storybook/nextjs Next.js overrides certain internal webpack packages (particularly `webpack-sources`), which are involved with filesystem caching, with its own versions from next/dist/compiled. For filesystem caching to work, Next.js must be allowed to perform these overrides before webpack is first initialized by @storybook/builder-webpack5. If it is not, the objects to be serialized to disk in the caching process will be instantiated using the original (non-Next.js) modules, but the serializers will be created using the Next.js modules. This mismatch between the objects to be cached and the serializers that write the filesystem cache prevents the cache from being written; instead, webpack outputs a warning message to the console for every object that it tries and fails to find a matching serializer for. This fix works by invoking Next.js to configure webpack in the `core` hook of @storybook/nextjs/preset, immediately before loading @storybook/builder-webpack5. We don't actually use this configuration that Next.js creates; the actual configuration that will be used in the build is still generated in `webpackFinal` as before. `fsCache` has a large impact on Storybook build performance. Even in a minimal project with a single story, enabling it reduces build time by 66%! This is therefore a very valuable option to be able to enable. Fixes #29621 --- code/frameworks/nextjs/src/preset.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 3463910175e1..84255fc62e20 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -36,6 +36,19 @@ export const addons: PresetProperty<'addons'> = [ export const core: PresetProperty<'core'> = async (config, options) => { 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. + const { nextConfigPath } = await options.presets.apply('frameworkOptions'); + 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, + }); + return { ...config, builder: { From 0a3e05bceafe0ad4aae8a2d3e19984c2d337b420 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Wed, 27 Nov 2024 11:33:40 +1100 Subject: [PATCH 05/43] Fix crash when framework is not specified with options --- code/frameworks/nextjs/src/preset.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 84255fc62e20..f10550acc900 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -34,19 +34,18 @@ export const addons: PresetProperty<'addons'> = [ ]; 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. - const { nextConfigPath } = await options.presets.apply('frameworkOptions'); 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, + nextConfigPath: typeof framework === 'string' ? undefined : framework.options.nextConfigPath, }); return { From 83a1f24f02c50a0b5ed4a03c6b248100183a3b01 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Tue, 3 Dec 2024 12:27:09 +1100 Subject: [PATCH 06/43] Defer all imports from webpack until after Next.js has loaded its internal instance --- code/frameworks/nextjs/src/config/webpack.ts | 14 +++++--- code/frameworks/nextjs/src/preset.ts | 35 +++++++++++--------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/code/frameworks/nextjs/src/config/webpack.ts b/code/frameworks/nextjs/src/config/webpack.ts index 76edac25c81c..030da6b2ab77 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')).DefinePlugin(definePluginConfig)); }; diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index f10550acc900..473e24655848 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -10,24 +10,10 @@ 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'))), @@ -156,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); @@ -168,7 +170,10 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, configureNextFont(baseConfig, useSWC); configureRuntimeNextjsVersionResolution(baseConfig); - configureImports({ baseConfig, configDir: options.configDir }); + configureImports({ + baseConfig, + configDir: options.configDir, + }); configureCss(baseConfig, nextConfig); configureImages(baseConfig, nextConfig); configureStyledJsx(baseConfig); From c00c054199d73cd88e6f53b713c3b944422c72c9 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Wed, 4 Dec 2024 10:42:43 +1100 Subject: [PATCH 07/43] Undo unintentional formatting change --- code/frameworks/nextjs/src/preset.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 473e24655848..6b90ae543a22 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -170,10 +170,7 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, configureNextFont(baseConfig, useSWC); configureRuntimeNextjsVersionResolution(baseConfig); - configureImports({ - baseConfig, - configDir: options.configDir, - }); + configureImports({ baseConfig, configDir: options.configDir }); configureCss(baseConfig, nextConfig); configureImages(baseConfig, nextConfig); configureStyledJsx(baseConfig); From bac7061dcdf17148b88976730734b3a98b30e4fe Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 5 Dec 2024 14:47:24 +0100 Subject: [PATCH 08/43] Nextjs: Add TS docgen support for Vite implementation --- code/frameworks/experimental-nextjs-vite/package.json | 1 + code/frameworks/experimental-nextjs-vite/src/preset.ts | 9 ++++++--- code/frameworks/react-native-web-vite/src/preset.ts | 1 - code/frameworks/react-vite/package.json | 10 ++++++++++ code/frameworks/react-vite/src/preset.ts | 2 +- code/yarn.lock | 1 + 6 files changed, 19 insertions(+), 5 deletions(-) diff --git a/code/frameworks/experimental-nextjs-vite/package.json b/code/frameworks/experimental-nextjs-vite/package.json index 356177bfabeb..d5ea27b70763 100644 --- a/code/frameworks/experimental-nextjs-vite/package.json +++ b/code/frameworks/experimental-nextjs-vite/package.json @@ -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 0a725be35804..633f62a5dceb 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/react-native-web-vite/src/preset.ts b/code/frameworks/react-native-web-vite/src/preset.ts index 8e3d7d6b58a8..bdacadb5e0a1 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 a6049e65c674..3394c59cbe3c 100644 --- a/code/frameworks/react-vite/package.json +++ b/code/frameworks/react-vite/package.json @@ -32,6 +32,16 @@ }, "./package.json": "./package.json" }, + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preset": [ + "dist/preset.d.ts" + ] + } + }, "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", diff --git a/code/frameworks/react-vite/src/preset.ts b/code/frameworks/react-vite/src/preset.ts index cef1a270f33b..a01721dadacc 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/yarn.lock b/code/yarn.lock index 389084993441..4470010205ac 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6658,6 +6658,7 @@ __metadata: dependencies: "@storybook/builder-vite": "workspace:*" "@storybook/react": "workspace:*" + "@storybook/react-vite": "workspace:*" "@storybook/test": "workspace:*" "@types/node": "npm:^18.0.0" next: "npm:^15.0.3" From a91b0ba3a6c2f885d8f965e23cbd8c7bfdb42f11 Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Tue, 1 Oct 2024 11:47:43 +0200 Subject: [PATCH 09/43] feat: add code snippet panel to single story view mode --- code/addons/docs/package.json | 13 ++++++- code/addons/docs/src/manager.tsx | 38 +++++++++++++++++++ code/addons/essentials/package.json | 5 +++ code/addons/essentials/src/docs/manager.ts | 2 + code/addons/essentials/src/index.ts | 2 +- .../vue3/src/docs/sourceDecorator.ts | 6 +-- 6 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 code/addons/docs/src/manager.tsx create mode 100644 code/addons/essentials/src/docs/manager.ts diff --git a/code/addons/docs/package.json b/code/addons/docs/package.json index f5834a822026..fbfa158c43d1 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -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 000000000000..9b8cdafb7799 --- /dev/null +++ b/code/addons/docs/src/manager.tsx @@ -0,0 +1,38 @@ +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, + 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: format ?? 'html' }); + console.log('SOURCE CODE CHANGED', codeSnippet.source); + }, + }); + + return ( + + + + ); + }, + }); +}); diff --git a/code/addons/essentials/package.json b/code/addons/essentials/package.json index 4f7ad700ec0d..73e1cbcb6f9f 100644 --- a/code/addons/essentials/package.json +++ b/code/addons/essentials/package.json @@ -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 000000000000..6101f7d79261 --- /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 5809420bc1b8..a72554227ba2 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/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 7eb8734305af..cc2f922f63f1 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -107,7 +107,6 @@ ${template}`; * Checks if the source code generation should be skipped for the given Story context. Will be true * if one of the following is true: * - * - View mode is not "docs" * - Story is no arg story * - Story has set custom source code via parameters.docs.source.code * - Story has set source type to "code" via parameters.docs.source.type @@ -120,13 +119,10 @@ export const shouldSkipSourceCodeGeneration = (context: StoryContext): boolean = } const isArgsStory = context?.parameters.__isArgsStory; - const isDocsViewMode = context?.viewMode === 'docs'; // never render if the user is forcing the block to render code, or // if the user provides code, or if it's not an args story. - return ( - !isDocsViewMode || !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE - ); + return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; /** From 107c40bf71a437b72f7c1d1755e10da0b7926a04 Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Tue, 1 Oct 2024 12:03:33 +0200 Subject: [PATCH 10/43] remove console log --- code/addons/docs/src/manager.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx index 9b8cdafb7799..ed3823ed4492 100644 --- a/code/addons/docs/src/manager.tsx +++ b/code/addons/docs/src/manager.tsx @@ -24,7 +24,6 @@ addons.register(ADDON_ID, (api) => { useChannel({ [SNIPPET_RENDERED]: ({ source, format }) => { setSourceCode({ source, format: format ?? 'html' }); - console.log('SOURCE CODE CHANGED', codeSnippet.source); }, }); From a283cea33680e379625bbc57dbd8b52c46c9ddcf Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Mon, 25 Nov 2024 10:04:25 +0100 Subject: [PATCH 11/43] use dark theme --- code/addons/docs/src/manager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx index ed3823ed4492..212956b99ebb 100644 --- a/code/addons/docs/src/manager.tsx +++ b/code/addons/docs/src/manager.tsx @@ -29,7 +29,7 @@ addons.register(ADDON_ID, (api) => { return ( - + ); }, From 1fa0e7da9125544912704155fcea4d8e1b115b0f Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Mon, 25 Nov 2024 11:15:44 +0100 Subject: [PATCH 12/43] feat: support `docs.source.addonPanel` parameter --- code/addons/docs/src/manager.tsx | 37 ++++++++++++++++++++++++++++- code/addons/essentials/src/index.ts | 2 +- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx index 212956b99ebb..09c371be238b 100644 --- a/code/addons/docs/src/manager.tsx +++ b/code/addons/docs/src/manager.tsx @@ -1,12 +1,27 @@ import React from 'react'; import { AddonPanel, type SyntaxHighlighterFormatTypes } from 'storybook/internal/components'; +import { FORCE_RE_RENDER, PRELOAD_ENTRIES } from 'storybook/internal/core-events'; 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.register(ADDON_ID, async (api) => { + // at this point, the parameters are not yet defined so we can not check whether the addon panel should + // be added or not. The "PRELOAD_ENTRIES" event seems to be the earliest point in time where the parameters + // are available + const isDisabled = await new Promise((resolve) => { + api.once(PRELOAD_ENTRIES, () => { + const parameter = api.getCurrentParameter(PARAM_KEY); + resolve(shouldDisableAddonPanel(parameter)); + }); + }); + + if (isDisabled) { + return; + } + addons.add(PANEL_ID, { title: 'Code', type: types.PANEL, @@ -34,4 +49,24 @@ addons.register(ADDON_ID, (api) => { ); }, }); + + api.emit(FORCE_RE_RENDER); }); + +const isObject = (value: unknown): value is object => { + return value != null && typeof value === 'object'; +}; + +/** + * Checks whether the addon panel should be disabled by checking the parameter.source.addonPanel + * property. + */ +const shouldDisableAddonPanel = (parameter: unknown) => { + return ( + isObject(parameter) && + 'source' in parameter && + isObject(parameter.source) && + 'addonPanel' in parameter.source && + parameter.source.addonPanel === false + ); +}; diff --git a/code/addons/essentials/src/index.ts b/code/addons/essentials/src/index.ts index a72554227ba2..5809420bc1b8 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', From 01e11e5bb9f0e8fa8626d1c8a98230210af93670 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Tue, 3 Dec 2024 22:25:15 +0800 Subject: [PATCH 13/43] Addon-docs: Fix and document source panel disabling --- MIGRATION.md | 20 ++++++++- code/addons/docs/src/manager.tsx | 42 ++----------------- .../stories/sourcePanel/index.stories.tsx | 15 +++++++ 3 files changed, 38 insertions(+), 39 deletions(-) create mode 100644 code/addons/docs/template/stories/sourcePanel/index.stories.tsx diff --git a/MIGRATION.md b/MIGRATION.md index 2e2e735fb21f..07aecbbe3973 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,7 @@

Migration

+- [From version 8.4.x to 8.5.x](#from-version-84x-to-85x) + - [Added source code pnael to docs](#added-source-code-pnael-to-docs) - [From version 8.2.x to 8.3.x](#from-version-82x-to-83x) - [Removed `experimental_SIDEBAR_BOTTOM` and deprecated `experimental_SIDEBAR_TOP` addon types](#removed-experimental_sidebar_bottom-and-deprecated-experimental_sidebar_top-addon-types) - [New parameters format for addon backgrounds](#new-parameters-format-for-addon-backgrounds) @@ -167,7 +169,7 @@ - [Angular: Drop support for calling Storybook directly](#angular-drop-support-for-calling-storybook-directly) - [Angular: Application providers and ModuleWithProviders](#angular-application-providers-and-modulewithproviders) - [Angular: Removed legacy renderer](#angular-removed-legacy-renderer) - - [Angular: initializer functions](#angular-initializer-functions) + - [Angular: Initializer functions](#angular-initializer-functions) - [Next.js: use the `@storybook/nextjs` framework](#nextjs-use-the-storybooknextjs-framework) - [SvelteKit: needs the `@storybook/sveltekit` framework](#sveltekit-needs-the-storybooksveltekit-framework) - [Vue3: replaced app export with setup](#vue3-replaced-app-export-with-setup) @@ -419,6 +421,22 @@ - [Packages renaming](#packages-renaming) - [Deprecated embedded addons](#deprecated-embedded-addons) +## From version 8.4.x to 8.5.x + +### Added source code pnael 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: { + docsSourcePanel: { disable: true }, + }, +}; +``` + ## From version 8.2.x to 8.3.x ### Removed `experimental_SIDEBAR_BOTTOM` and deprecated `experimental_SIDEBAR_TOP` addon types diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx index 09c371be238b..fdbe0375d369 100644 --- a/code/addons/docs/src/manager.tsx +++ b/code/addons/docs/src/manager.tsx @@ -1,31 +1,17 @@ import React from 'react'; import { AddonPanel, type SyntaxHighlighterFormatTypes } from 'storybook/internal/components'; -import { FORCE_RE_RENDER, PRELOAD_ENTRIES } from 'storybook/internal/core-events'; -import { ADDON_ID, PANEL_ID, PARAM_KEY, SNIPPET_RENDERED } from 'storybook/internal/docs-tools'; +import { ADDON_ID, PANEL_ID, 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, async (api) => { - // at this point, the parameters are not yet defined so we can not check whether the addon panel should - // be added or not. The "PRELOAD_ENTRIES" event seems to be the earliest point in time where the parameters - // are available - const isDisabled = await new Promise((resolve) => { - api.once(PRELOAD_ENTRIES, () => { - const parameter = api.getCurrentParameter(PARAM_KEY); - resolve(shouldDisableAddonPanel(parameter)); - }); - }); - - if (isDisabled) { - return; - } - +addons.register(ADDON_ID, (api) => { addons.add(PANEL_ID, { title: 'Code', type: types.PANEL, - paramKey: PARAM_KEY, + // disable this with `docsSourcePanel: { disable: true }` + paramKey: 'docsSourcePanel', match: ({ viewMode }) => viewMode === 'story', render: ({ active }) => { const [codeSnippet, setSourceCode] = useAddonState<{ @@ -49,24 +35,4 @@ addons.register(ADDON_ID, async (api) => { ); }, }); - - api.emit(FORCE_RE_RENDER); }); - -const isObject = (value: unknown): value is object => { - return value != null && typeof value === 'object'; -}; - -/** - * Checks whether the addon panel should be disabled by checking the parameter.source.addonPanel - * property. - */ -const shouldDisableAddonPanel = (parameter: unknown) => { - return ( - isObject(parameter) && - 'source' in parameter && - isObject(parameter.source) && - 'addonPanel' in parameter.source && - parameter.source.addonPanel === false - ); -}; 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 000000000000..dd4d2208fe6e --- /dev/null +++ b/code/addons/docs/template/stories/sourcePanel/index.stories.tsx @@ -0,0 +1,15 @@ +export default { + component: globalThis.Components.Button, + tags: ['autodocs'], + parameters: { + chromatic: { disable: true }, + docsSourcePanel: { disable: true }, + }, +}; + +export const One = { args: { label: 'One' } }; +export const Two = { args: { label: 'Two' } }; +export const WithSource = { + args: { label: 'Three' }, + parameters: { docsSourcePanel: { disable: false } }, +}; From 2be176923b7a2e00216d26b61142704851e82a92 Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Wed, 4 Dec 2024 08:10:31 +0100 Subject: [PATCH 14/43] fix typos --- MIGRATION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 07aecbbe3973..db4d3b2d15c2 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,7 +1,7 @@

Migration

- [From version 8.4.x to 8.5.x](#from-version-84x-to-85x) - - [Added source code pnael to docs](#added-source-code-pnael-to-docs) + - [Added source code panel to docs](#added-source-code-panel-to-docs) - [From version 8.2.x to 8.3.x](#from-version-82x-to-83x) - [Removed `experimental_SIDEBAR_BOTTOM` and deprecated `experimental_SIDEBAR_TOP` addon types](#removed-experimental_sidebar_bottom-and-deprecated-experimental_sidebar_top-addon-types) - [New parameters format for addon backgrounds](#new-parameters-format-for-addon-backgrounds) @@ -423,7 +423,7 @@ ## From version 8.4.x to 8.5.x -### Added source code pnael to docs +### 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). From 33ce8f93c8e9039c4566cc3e2ce484579282040d Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Wed, 4 Dec 2024 10:18:25 +0100 Subject: [PATCH 15/43] move docs source panel order --- code/addons/essentials/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/addons/essentials/src/index.ts b/code/addons/essentials/src/index.ts index 5809420bc1b8..a72554227ba2 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', From db146c34dcc878e321b167bdd743e1ec6d396ce9 Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Wed, 4 Dec 2024 10:54:04 +0100 Subject: [PATCH 16/43] fix react code snippet format --- code/addons/docs/src/manager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx index fdbe0375d369..a02fe22f0764 100644 --- a/code/addons/docs/src/manager.tsx +++ b/code/addons/docs/src/manager.tsx @@ -24,7 +24,7 @@ addons.register(ADDON_ID, (api) => { useChannel({ [SNIPPET_RENDERED]: ({ source, format }) => { - setSourceCode({ source, format: format ?? 'html' }); + setSourceCode({ source, format }); }, }); From 75f235a4bd67d2db69d98324236b9082992cbe50 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Mon, 9 Dec 2024 17:13:12 +0800 Subject: [PATCH 17/43] Fix merge conflict remnants --- MIGRATION.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 2b5ff2bcb9c6..7e46fa5bb1a2 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,10 +1,8 @@

Migration

- [From version 8.4.x to 8.5.x](#from-version-84x-to-85x) - <<<<<<< HEAD - - # [Added source code panel to docs](#added-source-code-panel-to-docs) + - [Added source code panel to docs](#added-source-code-panel-to-docs) - [Indexing behavior of @storybook/experimental-addon-test is changed](#indexing-behavior-of-storybookexperimental-addon-test-is-changed) - > > > > > > > next - [From version 8.2.x to 8.3.x](#from-version-82x-to-83x) - [Removed `experimental_SIDEBAR_BOTTOM` and deprecated `experimental_SIDEBAR_TOP` addon types](#removed-experimental_sidebar_bottom-and-deprecated-experimental_sidebar_top-addon-types) - [New parameters format for addon backgrounds](#new-parameters-format-for-addon-backgrounds) From 4591ef9fee58f4c8d6fed5d8b3fc66479e1663e2 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 10 Dec 2024 14:31:45 +0100 Subject: [PATCH 18/43] Wait for 2 seconds before showing result mismatch warning --- code/addons/test/src/components/Panel.tsx | 26 +++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/code/addons/test/src/components/Panel.tsx b/code/addons/test/src/components/Panel.tsx index 584872bad8c3..e18e26f2461d 100644 --- a/code/addons/test/src/components/Panel.tsx +++ b/code/addons/test/src/components/Panel.tsx @@ -114,6 +114,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, @@ -252,22 +253,29 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId 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; }, [isPlaying, interactions, hasException]); - const hasResultMismatch = React.useMemo(() => { - return ( - browserTestStatus !== null && - browserTestStatus !== CallStates.ACTIVE && - storyStatus?.status !== undefined && - statusMap[browserTestStatus] !== storyStatus.status - ); - }, [browserTestStatus, storyStatus]); + useEffect(() => { + const isMismatch = + browserTestStatus && + storyTestStatus && + storyTestStatus !== 'pending' && + storyTestStatus !== statusMap[browserTestStatus]; + + if (isMismatch) { + const timeout = setTimeout(() => setResultMismatch(true), 2000); + return () => clearTimeout(timeout); + } else { + setResultMismatch(false); + } + }, [browserTestStatus, storyTestStatus]); if (isErrored) { return ; From b1cc74dac1d86c517cdc50d2bb3bd83c8554bcaa Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Wed, 11 Dec 2024 12:25:50 +0100 Subject: [PATCH 19/43] Use local storybook binary instead --- code/addons/test/src/postinstall.ts | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index 55f97eb6b0da..bb22c7517127 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'; @@ -227,22 +227,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}`; } } From 69bcbaad18700c1459d8a6691ac20c676a498a83 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 11 Dec 2024 13:46:06 +0100 Subject: [PATCH 20/43] Handle telemetry where we determine the test discrepancy, not in the UI --- .../components/InteractionsPanel.stories.tsx | 1 - .../test/src/components/InteractionsPanel.tsx | 12 +------ code/addons/test/src/components/Panel.tsx | 31 ++++++++++++++----- .../TestDiscrepancyMessage.stories.tsx | 3 -- .../src/components/TestDiscrepancyMessage.tsx | 30 +++++------------- 5 files changed, 32 insertions(+), 45 deletions(-) diff --git a/code/addons/test/src/components/InteractionsPanel.stories.tsx b/code/addons/test/src/components/InteractionsPanel.stories.tsx index 24eefed2028c..f8cabbe24d53 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 164e28b782fb..896350b926c7 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) && ( (function PanelMemoized({ storyId interactionsCount: list.filter(({ method }) => method !== 'step').length, }; }); - }, [collapsed]); + }, [set, collapsed]); const controls = useMemo( () => ({ @@ -240,7 +240,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId emit(FORCE_REMOUNT, { storyId }); }, }), - [storyId] + [emit, storyId] ); const storyFilePath = useParameter('fileName', ''); @@ -262,6 +262,8 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId return isPlaying ? CallStates.ACTIVE : null; }, [isPlaying, interactions, hasException]); + const { testRunId } = storyStatus?.data || {}; + useEffect(() => { const isMismatch = browserTestStatus && @@ -270,12 +272,29 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId storyTestStatus !== statusMap[browserTestStatus]; if (isMismatch) { - const timeout = setTimeout(() => setResultMismatch(true), 2000); + 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); } - }, [browserTestStatus, storyTestStatus]); + }, [emit, browserTestStatus, storyTestStatus, storyId, testRunId]); if (isErrored) { return ; @@ -298,8 +317,6 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId pausedAt={pausedAt} endRef={endRef} onScrollToEnd={scrollTarget && scrollToTarget} - storyId={storyId} - testRunId={storyStatus?.data?.testRunId} /> ); diff --git a/code/addons/test/src/components/TestDiscrepancyMessage.stories.tsx b/code/addons/test/src/components/TestDiscrepancyMessage.stories.tsx index 840c08cdf3d3..81553cdc5a32 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 bbdf74e36a65..b23af4a7be6a 100644 --- a/code/addons/test/src/components/TestDiscrepancyMessage.tsx +++ b/code/addons/test/src/components/TestDiscrepancyMessage.tsx @@ -33,39 +33,23 @@ const Wrapper = styled.div(({ theme: { color, typography, background } }) => ({ interface TestDiscrepancyMessageProps { browserTestStatus: CallStates; - storyId: StoryId; - testRunId: string; } -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 From aa75a9877380a60d30c057886639675f69596bdd Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 11 Dec 2024 14:20:10 +0100 Subject: [PATCH 21/43] remove existing test.include config --- code/addons/test/src/vitest-plugin/index.ts | 31 ++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/code/addons/test/src/vitest-plugin/index.ts b/code/addons/test/src/vitest-plugin/index.ts index eadadb4440e5..84dbbadd20c0 100644 --- a/code/addons/test/src/vitest-plugin/index.ts +++ b/code/addons/test/src/vitest-plugin/index.ts @@ -124,7 +124,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 { @@ -148,8 +148,9 @@ export const storybookTest = async (options?: UserOptions): Promise => { setupFiles: [ '@storybook/experimental-addon-test/internal/setup-file', // 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, + 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), ...(finalOptions.storybookScript @@ -175,7 +176,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: { @@ -186,7 +188,7 @@ export const storybookTest = async (options?: UserOptions): Promise => { : {}), 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 +205,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 +216,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: { @@ -254,8 +261,12 @@ 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 + ) { + // remove the user's existing include, because we're replacing it with our own heuristic based on main.ts#stories + 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. From 9a680a3166672b182c36d288d1277a6cfbb79c9a Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 11 Dec 2024 14:25:13 +0100 Subject: [PATCH 22/43] remove unnecessary test.include in test kitchen sink --- .../portable-stories-kitchen-sink/react/vitest.workspace.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 f1657f3acddd..041918fb126d 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/vitest.workspace.ts +++ b/test-storybooks/portable-stories-kitchen-sink/react/vitest.workspace.ts @@ -10,9 +10,6 @@ export default defineWorkspace([ test: { name: "storybook", pool: "threads", - include: [ - "stories/AddonTest.stories.?(c|m)[jt]s?(x)", - ], deps: { optimizer: { web: { @@ -30,4 +27,4 @@ export default defineWorkspace([ environment: "happy-dom", }, }, -]); \ No newline at end of file +]); From 6fb17c9ab97ced368a5e6ef22195567ea15f43a6 Mon Sep 17 00:00:00 2001 From: Kyle Gach Date: Wed, 11 Dec 2024 23:30:25 -0700 Subject: [PATCH 23/43] Docs updates for Storybook Test - Focused tests - Coverage - A11y - Remove `test.include` & `test.isolate` from example Vitest configs - Streamline Test addon introduction - Address feedback from EAP --- MIGRATION.md | 8 +- .../vitest-plugin-vitest-config-alias.md | 2 + docs/_snippets/vitest-plugin-vitest-config.md | 24 +- .../vitest-plugin-vitest-workspace.md | 18 -- docs/writing-tests/accessibility-testing.mdx | 82 ++++-- docs/writing-tests/test-addon.mdx | 93 +++++-- docs/writing-tests/test-coverage.mdx | 240 +++++++++++++++++- 7 files changed, 368 insertions(+), 99 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index fd3ce9e10be5..58b42f10c0aa 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -427,12 +427,14 @@ ### 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/docs/_snippets/vitest-plugin-vitest-config-alias.md b/docs/_snippets/vitest-plugin-vitest-config-alias.md index 5777c37f2616..ac1f59857c72 100644 --- a/docs/_snippets/vitest-plugin-vitest-config-alias.md +++ b/docs/_snippets/vitest-plugin-vitest-config-alias.md @@ -17,6 +17,8 @@ export default { ``` ```js filename="vitest.config.ts" renderer="common" tabTitle="After" +import { defineConfig } from 'vitest/config'; + export default defineConfig({ // ... resolve: { diff --git a/docs/_snippets/vitest-plugin-vitest-config.md b/docs/_snippets/vitest-plugin-vitest-config.md index 4cd240e86a44..b3f271a6e636 100644 --- a/docs/_snippets/vitest-plugin-vitest-config.md +++ b/docs/_snippets/vitest-plugin-vitest-config.md @@ -18,8 +18,6 @@ export default mergeConfig( // storybookNextJsPlugin(), ], test: { - // Glob pattern to find story files - include: ['src/**/*.stories.?(m)[jt]s?(x)'], // Enable browser mode browser: { enabled: true, @@ -28,13 +26,9 @@ export default mergeConfig( provider: 'playwright', headless: true, }, - // Speed up tests and better match how they run in Storybook itself - // https://vitest.dev/config/#isolate - // Consider removing this if you have flaky tests - isolate: false, setupFiles: ['./.storybook/vitest.setup.ts'], }, - }), + }) ); ``` @@ -57,8 +51,6 @@ export default mergeConfig( storybookVuePlugin(), ], test: { - // Glob pattern to find story files - include: ['src/**/*.stories.?(m)[jt]s?(x)'], // Enable browser mode browser: { enabled: true, @@ -67,13 +59,9 @@ export default mergeConfig( provider: 'playwright', headless: true, }, - // Speed up tests and better match how they run in Storybook itself - // https://vitest.dev/config/#isolate - // Consider removing this if you have flaky tests - isolate: false, setupFiles: ['./.storybook/vitest.setup.ts'], }, - }), + }) ); ``` @@ -97,8 +85,6 @@ export default mergeConfig( // storybookSveltekitPlugin(), ], test: { - // Glob pattern to find story files - include: ['src/**/*.stories.?(m)[jt]s?(x)'], // Enable browser mode browser: { enabled: true, @@ -107,12 +93,8 @@ export default mergeConfig( provider: 'playwright', headless: true, }, - // Speed up tests and better match how they run in Storybook itself - // https://vitest.dev/config/#isolate - // Consider removing this if you have flaky tests - isolate: false, setupFiles: ['./.storybook/vitest.setup.ts'], }, - }), + }) ); ``` diff --git a/docs/_snippets/vitest-plugin-vitest-workspace.md b/docs/_snippets/vitest-plugin-vitest-workspace.md index b4e9ba800211..46b5a3e22edd 100644 --- a/docs/_snippets/vitest-plugin-vitest-workspace.md +++ b/docs/_snippets/vitest-plugin-vitest-workspace.md @@ -20,8 +20,6 @@ export default defineWorkspace([ ], test: { name: 'storybook', - // Glob pattern to find story files - include: ['src/**/*.stories.?(m)[jt]s?(x)'], // Enable browser mode browser: { enabled: true, @@ -30,10 +28,6 @@ export default defineWorkspace([ provider: 'playwright', headless: true, }, - // Speed up tests and better match how they run in Storybook itself - // https://vitest.dev/config/#isolate - // Consider removing this if you have flaky tests - isolate: false, setupFiles: ['./.storybook/vitest.setup.ts'], }, }, @@ -63,8 +57,6 @@ export default defineWorkspace([ ], test: { name: 'storybook', - // Glob pattern to find story files - include: ['src/**/*.stories.?(m)[jt]s?(x)'], // Enable browser mode browser: { enabled: true, @@ -73,10 +65,6 @@ export default defineWorkspace([ provider: 'playwright', headless: true, }, - // Speed up tests and better match how they run in Storybook itself - // https://vitest.dev/config/#isolate - // Consider removing this if you have flaky tests - isolate: false, setupFiles: ['./.storybook/vitest.setup.ts'], }, }, @@ -107,8 +95,6 @@ export default defineWorkspace([ ], test: { name: 'storybook', - // Glob pattern to find story files - include: ['src/**/*.stories.?(m)[jt]s?(x)'], // Enable browser mode browser: { enabled: true, @@ -117,10 +103,6 @@ export default defineWorkspace([ provider: 'playwright', headless: true, }, - // Speed up tests and better match how they run in Storybook itself - // https://vitest.dev/config/#isolate - // Consider removing this if you have flaky tests - isolate: false, setupFiles: ['./.storybook/vitest.setup.ts'], }, }, diff --git a/docs/writing-tests/accessibility-testing.mdx b/docs/writing-tests/accessibility-testing.mdx index 7072edb154ad..a0eb50683630 100644 --- a/docs/writing-tests/accessibility-testing.mdx +++ b/docs/writing-tests/accessibility-testing.mdx @@ -67,17 +67,17 @@ If you need to dismiss an accessibility rule or modify its settings across all s #### Component-level a11y configuration - + - You can also customize your own set of rules for all stories of a component. If you're using Svelte CSF with the native templating syntax, you can update the `defineMeta` function or the default export of the CSF story and provide the required configuration: + You can also customize your own set of rules for all stories of a component. If you're using Svelte CSF with the native templating syntax, you can update the `defineMeta` function. If you're using regular CSF, you can update the default export of the story file. - + - + - You can also customize your own set of rules for all stories of a component. Update your story's default export and add parameters and globals with the required configuration: + You can also customize your own set of rules for all stories of a component. Update the story file's default export and add parameters and globals with the required configuration: - + {/* prettier-ignore-start */} @@ -97,17 +97,17 @@ Customize the a11y ruleset at the story level by updating your story to include #### Turn off automated a11y tests - + - If you are using Svelte CSF, you can turn off automated accessibility testing for stories or components by adding globals to your story or adjusting the defineMeta function with the required configuration. With a regular CSF story, you can add the following to your story's export or component's default export: + If you are using Svelte CSF, you can turn off automated accessibility testing for stories or components by adding globals to your story or adjusting the `defineMeta` function with the required configuration. With a regular CSF story, you can add the following to your story's export or component's default export: - + - + - Disable automated accessibility testing for stories or components by adding the following globals to your story’s export or component’s default export respectively: + Disable automated accessibility testing for stories or components by adding the following globals to your story’s export or component’s default export: - + {/* prettier-ignore-start */} @@ -120,43 +120,69 @@ Customize the a11y ruleset at the story level by updating your story to include ## Test addon integration -The accessibility addon provides seamless integration with Storybook's [test addon](./test-addon.mdx), enabling you to run automated accessibility checks for all your tests in the background while you run component tests. If there are any violations, the test will fail, and you will see the results in the sidebar without any additional setup. +The accessibility addon provides seamless integration with the [Test addon](./test-addon.mdx), enabling you to run automated accessibility checks for all your tests in the background while you run component tests. If there are any violations, the test will fail, and you will see the results in the sidebar without any additional setup. {/* TODO: add asset of the changed UI here */} ### Manual upgrade -If you enabled the addon and you're manually upgrading to Storybook 8.5 or later, you'll need to adjust your existing configuration (i.e., `.storybook/vitest.config.ts`) to enable the integration as follows: +If you enabled the addon and you're manually upgrading to Storybook 8.5 or later, you'll need to adjust your existing configuration (i.e., `.storybook/vitest.setup.ts`) to enable the integration as follows: -### Override accessibility violation levels +### Configure accessibility tests with the test addon -By default, when the accessibility addon runs with the test addon enabled, it interprets all violations as errors. This means that if a story has a minor accessibility violation, the test will fail. However, you can override this behavior by setting the `warnings` parameter in the `a11y` configuration object to define an array of impact levels that should be considered warnings. +Like the Test addon, the accessibility addon also supports [tags](../writing-stories/tags.mdx) to filter the tests you want to run. By default, the addon applies the `a11ytest` tag to all stories. If you need to exclude a story from being accessibility tested, you can remove that tag by applying the `!a11ytest` tag to the story. This also works at the project (in `.storybook/preview.js|ts`) or component level (default export in the story file). -{/* prettier-ignore-start */} +You can use tags to progressively work toward a more accessible UI by enabling accessibility tests for a subset of stories and gradually increasing the coverage. For example, a typical workflow might look like this: - +1. Run accessibility tests for your entire project. +1. Find that many stories have accessibility issues (and maybe feel a bit overwhelmed!). +1. Temporarily exclude all stories from accessibility tests. -{/* prettier-ignore-end */} + ```ts title=".storybook/preview.ts" + // Replace your-renderer with the renderer you are using (e.g., react, vue3) + import { Preview } from '@storybook/your-renderer'; + + const preview: Preview = { + // ... + // 👇 Temporarily remove the a11ytest tag from all stories + tags: ['!a11ytest'], + }; + + export default preview; + ``` + +1. Pick a good starting point (we recommend something like Button, for its simplicity and likelihood of being used within other components) and re-include it in the accessibility tests. + + ```ts title="Button.stories.ts" + // Replace your-renderer with the renderer you are using (e.g., react, vue3) + import { Meta } from '@storybook/your-renderer'; + + import { Button } from './Button'; + + const meta = { + component: Button, + // 👇 Re-apply the a11ytest tag for this component's stories + tags: ['a11ytest'], + }; + + export default preview; + ``` -In the example above, we configured all the `minor` or `moderate` accessibility violations to be considered warnings. All other levels (i.e., `serious` or `critical`) will continue to be considered errors, fail the test, and report the results accordingly in the Storybook UI or Vitest. +1. Pick another component and repeat the process until you've covered all your components and you're an accessibility hero! -### Configure accessibility tests with the test addon +### Override accessibility violation levels -If you want to run accessibility tests only for a subset of your stories, you can use the [tags](../writing-stories/tags.mdx) mechanism to filter the tests you want to run with the test addon. For example, to turn off accessibility tests for a specific story, add the `!a11ytest` tag to the story's meta or directly to the story's `tags` configuration option. For example: +By default, when the accessibility addon runs with the test addon enabled, it interprets all violations as errors. This means that if a story has a minor accessibility violation, the test will fail. However, you can override this behavior by setting the `warnings` parameter in the `a11y` configuration object to define an array of impact levels that should be considered warnings. {/* prettier-ignore-start */} - + {/* prettier-ignore-end */} - - - Tags can be applied at the project, component (meta), or story levels. Read our [documentation](../writing-stories/tags.mdx) for more information on configuring tags. - - +In the example above, we configured all the `minor` or `moderate` accessibility violations to be considered warnings. All other levels (i.e., `serious` or `critical`) will continue to be considered errors, fail the test, and report the results accordingly in the Storybook UI or CLI output. diff --git a/docs/writing-tests/test-addon.mdx b/docs/writing-tests/test-addon.mdx index fbf331c69dd8..550e634401b0 100644 --- a/docs/writing-tests/test-addon.mdx +++ b/docs/writing-tests/test-addon.mdx @@ -7,9 +7,9 @@ sidebar: - The Test addon, powered by [Vitest](https://vitest.dev/), is currently only supported in [React](?renderer=react), [Vue](?renderer=vue) and [Svelte](?renderer=svelte) projects, which use the [Vite builder](../builders/vite.mdx) (or the [Next.js framework](../get-started/frameworks/nextjs.mdx)). + The Test addon is currently only supported in [React](?renderer=react), [Vue](?renderer=vue) and [Svelte](?renderer=svelte) projects, which use the [Vite builder](../builders/vite.mdx) (or the [Next.js framework with Vite](../get-started/frameworks/nextjs.mdx#with-vite)). - If you are using a different renderer, you can use the [Storyboook test runner](./test-runner.mdx) to test your stories. + If you are using a different renderer (such as Angular) or the Webpack builder, you can use the [Storyboook test runner](./test-runner.mdx) to test your stories. {/* End non-supported renderers */} @@ -23,11 +23,11 @@ sidebar: While this addon is experimental, it is published as the `@storybook/experimental-addon-test` package and the API may change in future releases. We welcome feedback and contributions to help improve this feature. -Storybook's Test addon allows you to test your components directly inside Storybook. It does this by using a Vitest plugin to transform your [stories](../writing-stories/index.mdx) into [Vitest](https://vitest.dev) tests using [portable stories](../api/portable-stories/portable-stories-vitest.mdx). +Storybook's Test addon allows you to test your components directly inside Storybook. On its own, it transforms your [stories](../writing-stories/index.mdx) into [component tests](./component-testing.mdx), which test the rendering and behavior of your components in a real browser environment. It can also calculate project [coverage](./test-coverage.mdx) provided by your stories. -Stories are tested in two ways: a smoke test to ensure it renders and, if a [play function](../writing-tests/component-testing#write-a-component-test) is defined, that function is run and any [assertions made](../writing-tests/component-testing.mdx#assert-tests-with-vitests-apis) within it are validated. +If your project is using other testing addons, such as the [Visual tests addon](./visual-testing.mdx) or the [Accessibility addon](./accessibility-testing.mdx), you can run those tests alongside your component tests. -If a test fails, it will be marked as such in the sidebar and you can click on the story to see the failure. You can also run tests in watch mode, which will automatically re-run tests when you make changes to your components or stories. +When tests are run for a story, the status is shown in the sidebar. You can filter it down to only show failures and failing status indicators open a menu to debug the failure. You can also run tests in watch mode, which will automatically re-run tests when you make changes to your components or stories.