From 592f78a2489d90dbc7914c37e2d9d84b8ee74c6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:19:16 +0000 Subject: [PATCH 01/14] Initial plan From a649502bba07c2e611db638e9aa1e5cb7b167fe0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:32:23 +0000 Subject: [PATCH 02/14] fix: handle additional vitest config patterns in postinstall setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes AddonVitestPostinstallError for these patterns: - defineConfig(mergeConfig(...)) – defineConfig wrapping mergeConfig - defineConfig(mergeConfig(...) satisfies ViteUserConfig) – satisfies operator - mergeConfig(...) as ViteUserConfig – TSAsExpression wrapper - const config = mergeConfig(...); export default config – re-exported variable - const vitestConfig = {...}; export default mergeConfig(viteConfig, vitestConfig) – constant arg - defineProject({...}) – defineProject as alias for defineConfig - const test = {...}; mergeConfig(viteConfig, { test }) – shorthand property New helpers added to updateVitestFile.ts: - unwrapTSExpression: peels off TSAsExpression/TSSatisfiesExpression/TSTypeAssertion - resolveExpression: resolves through variable references + TS type wrappers - isDefineConfigLike: recognizes defineConfig and defineProject - getEffectiveMergeConfigCall: finds the mergeConfig call through any wrappers 26 unit tests passing (19 existing + 7 new). Co-authored-by: valentinpalkovic <5889929+valentinpalkovic@users.noreply.github.com> --- .../vitest/src/updateVitestFile.test.ts | 560 ++++++++++++++++++ code/addons/vitest/src/updateVitestFile.ts | 511 ++++++++-------- 2 files changed, 837 insertions(+), 234 deletions(-) diff --git a/code/addons/vitest/src/updateVitestFile.test.ts b/code/addons/vitest/src/updateVitestFile.test.ts index 9ff9385b80ab..274f432dddd0 100644 --- a/code/addons/vitest/src/updateVitestFile.test.ts +++ b/code/addons/vitest/src/updateVitestFile.test.ts @@ -1211,6 +1211,566 @@ describe('updateConfigFile', () => { }));" `); }); + + it('supports defineConfig wrapping mergeConfig', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default defineConfig(mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + })) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig(mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('supports defineConfig wrapping mergeConfig with satisfies operator', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + import type { ViteUserConfig } from 'vitest/config' + + export default defineConfig( + mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) satisfies ViteUserConfig + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + import type { ViteUserConfig } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig(mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }) satisfies ViteUserConfig);" + `); + }); + + it('supports mergeConfig with as operator (TSAsExpression)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + import type { ViteUserConfig } from 'vitest/config' + + export default mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) as ViteUserConfig + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + import type { ViteUserConfig } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }) as ViteUserConfig;" + `); + }); + + it('supports mergeConfig with test defined as a constant (shorthand property)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const test = { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + } + + export default mergeConfig(viteConfig, { test }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const test = { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'] + }; + export default mergeConfig(viteConfig, { + + - test + - + + test: { + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + });" + `); + }); + + it('supports const defined config re-exported (export default config)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const config = mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) + ) + + export default config + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const config = mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + })); + export default config;" + `); + }); + + it('supports defineProject instead of defineConfig', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineProject } from 'vitest/config' + + export default defineProject({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineProject } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineProject({ + test: { + name: 'node', + environment: 'happy-dom', + + - include: ['**/*.test.ts'] + - + + include: ['**/*.test.ts'], + + workspace: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('supports mergeConfig with config object as a constant variable', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const vitestConfig = { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + } + } + + export default mergeConfig(viteConfig, vitestConfig) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const vitestConfig = { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }; + export default mergeConfig(viteConfig, vitestConfig);" + `); + }); }); describe('updateWorkspaceFile', () => { diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts index ca9a01bffd9a..9d0490d26d83 100644 --- a/code/addons/vitest/src/updateVitestFile.ts +++ b/code/addons/vitest/src/updateVitestFile.ts @@ -68,56 +68,108 @@ const mergeProperties = ( } }; +/** + * Recursively unwraps TypeScript type annotation expressions (as X, satisfies X, expr). + */ +const unwrapTSExpression = (expr: t.Expression | t.Declaration): t.Expression => { + if ( + expr.type === 'TSAsExpression' || + expr.type === 'TSSatisfiesExpression' || + expr.type === 'TSTypeAssertion' + ) { + return unwrapTSExpression((expr as t.TSAsExpression).expression); + } + return expr as t.Expression; +}; + +/** + * Resolves an expression through variable references and TypeScript type annotations. + * Handles: Identifier (variable lookup), TSAsExpression, TSSatisfiesExpression, TSTypeAssertion. + */ +const resolveExpression = ( + expr: t.Expression | t.Declaration | null | undefined, + ast: BabelFile['ast'] +): t.Expression | null => { + if (!expr) return null; + const unwrapped = unwrapTSExpression(expr as t.Expression | t.Declaration); + if (unwrapped.type !== 'Identifier') return unwrapped; + const varName = (unwrapped as t.Identifier).name; + const varDecl = ast.program.body.find( + (n): n is t.VariableDeclaration => + n.type === 'VariableDeclaration' && + n.declarations.some( + (d) => d.id.type === 'Identifier' && (d.id as t.Identifier).name === varName + ) + ); + if (!varDecl) return unwrapped; + const declarator = varDecl.declarations.find( + (d) => d.id.type === 'Identifier' && (d.id as t.Identifier).name === varName + ); + if (!declarator?.init) return unwrapped; + return resolveExpression(declarator.init, ast); +}; + +/** + * Returns true if the call expression is a defineConfig or defineProject call. + */ +const isDefineConfigLike = (node: t.CallExpression): boolean => + node.callee.type === 'Identifier' && + (node.callee.name === 'defineConfig' || node.callee.name === 'defineProject'); + +/** + * Extracts the effective mergeConfig call from a declaration, handling wrappers: + * - TypeScript type annotations (as X, satisfies X) + * - defineConfig(mergeConfig(...)) outer wrapper + * - variable references (export default config where config = mergeConfig(...)) + */ +const getEffectiveMergeConfigCall = ( + decl: t.Expression | t.Declaration, + ast: BabelFile['ast'] +): t.CallExpression | null => { + const resolved = resolveExpression(decl, ast); + if (!resolved || resolved.type !== 'CallExpression') return null; + + // Handle defineConfig(mergeConfig(...)) – arg may itself be wrapped in a TS type expression + if (isDefineConfigLike(resolved) && resolved.arguments.length > 0) { + const innerArg = resolveExpression(resolved.arguments[0] as t.Expression, ast); + if ( + innerArg?.type === 'CallExpression' && + innerArg.callee.type === 'Identifier' && + innerArg.callee.name === 'mergeConfig' + ) { + return innerArg; + } + } + + // Handle mergeConfig(...) directly + if (resolved.callee.type === 'Identifier' && resolved.callee.name === 'mergeConfig') { + return resolved; + } + + return null; +}; + /** * Resolves the target's default export to the actual config object expression we can merge into. - * Handles: export default defineConfig({}), export default {}, and export default config (where - * config is a variable holding defineConfig({}) or {}). + * Handles: export default defineConfig({}), export default defineProject({}), + * export default {}, and export default config (where config is a variable holding one of those), + * as well as TypeScript type annotations on the declaration. */ const getTargetConfigObject = ( target: BabelFile['ast'], exportDefault: t.ExportDefaultDeclaration ): t.ObjectExpression | null => { - const decl = exportDefault.declaration; - if (decl.type === 'ObjectExpression') { - return decl; + const resolved = resolveExpression(exportDefault.declaration, target); + if (!resolved) return null; + if (resolved.type === 'ObjectExpression') { + return resolved; } if ( - decl.type === 'CallExpression' && - decl.callee.type === 'Identifier' && - decl.callee.name === 'defineConfig' && - decl.arguments[0]?.type === 'ObjectExpression' + resolved.type === 'CallExpression' && + isDefineConfigLike(resolved) && + resolved.arguments[0]?.type === 'ObjectExpression' ) { - return decl.arguments[0] as t.ObjectExpression; - } - if (decl.type === 'Identifier') { - const varName = decl.name; - const varDecl = target.program.body.find( - (n): n is t.VariableDeclaration => - n.type === 'VariableDeclaration' && - n.declarations.some((d) => d.id.type === 'Identifier' && d.id.name === varName) - ); - if (!varDecl) { - return null; - } - const declarator = varDecl.declarations.find( - (d) => d.id.type === 'Identifier' && d.id.name === varName - ); - if (!declarator?.init) { - return null; - } - const init = declarator.init; - if ( - init.type === 'CallExpression' && - init.callee.type === 'Identifier' && - init.callee.name === 'defineConfig' && - init.arguments[0]?.type === 'ObjectExpression' - ) { - return init.arguments[0] as t.ObjectExpression; - } - if (init.type === 'ObjectExpression') { - return init; - } - return null; + return resolved.arguments[0] as t.ObjectExpression; } return null; }; @@ -165,50 +217,24 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as return false; } - // Check if this is a function notation that we don't support - const rejectFunctionNotation = (decl: t.ExportDefaultDeclaration['declaration']) => { - if ( - decl.type === 'CallExpression' && - decl.callee.type === 'Identifier' && - decl.callee.name === 'defineConfig' && - decl.arguments.length > 0 && - decl.arguments[0].type === 'ArrowFunctionExpression' - ) { - return true; - } - return false; - }; + // Check if this is a function notation that we don't support (defineConfig(() => ({}))) + // Resolve through TS type wrappers and variable references before checking. + const effectiveDecl = resolveExpression(targetExportDefault.declaration, target); if ( - targetExportDefault.declaration.type === 'CallExpression' && - rejectFunctionNotation(targetExportDefault.declaration) + effectiveDecl?.type === 'CallExpression' && + isDefineConfigLike(effectiveDecl) && + effectiveDecl.arguments.length > 0 && + effectiveDecl.arguments[0].type === 'ArrowFunctionExpression' ) { return false; } - if (targetExportDefault.declaration.type === 'Identifier') { - const varName = targetExportDefault.declaration.name; - const varDecl = target.program.body.find( - (n): n is t.VariableDeclaration => - n.type === 'VariableDeclaration' && - n.declarations.some((d) => d.id.type === 'Identifier' && d.id.name === varName) - ); - const declarator = varDecl?.declarations.find( - (d) => d.id.type === 'Identifier' && d.id.name === varName - ); - if (declarator?.init?.type === 'CallExpression' && rejectFunctionNotation(declarator.init)) { - return false; - } - } - // Check if we can handle mergeConfig patterns (including export default config where config = defineConfig({})) + // Check if we can handle the config pattern (direct object, defineConfig/defineProject, + // mergeConfig, or any of these wrapped in TS type annotations / variable references) let canHandleConfig = false; if (getTargetConfigObject(target, targetExportDefault) !== null) { canHandleConfig = true; - } else if ( - targetExportDefault.declaration.type === 'CallExpression' && - targetExportDefault.declaration.callee.type === 'Identifier' && - targetExportDefault.declaration.callee.name === 'mergeConfig' && - targetExportDefault.declaration.arguments.length >= 2 - ) { + } else if (getEffectiveMergeConfigCall(targetExportDefault.declaration, target) !== null) { canHandleConfig = true; } @@ -260,194 +286,211 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as if (targetConfigObject !== null) { mergeProperties(properties, targetConfigObject.properties); updated = true; - } else if ( - exportDefault.declaration.type === 'CallExpression' && - exportDefault.declaration.callee.type === 'Identifier' && - exportDefault.declaration.callee.name === 'mergeConfig' && - exportDefault.declaration.arguments.length >= 2 - ) { - // We first collect all the potential config object nodes from mergeConfig, these can be: - // - defineConfig({ ... }) calls - // - plain object expressions { ... } without a defineConfig helper - const configObjectNodes: t.ObjectExpression[] = []; - - for (const arg of exportDefault.declaration.arguments) { - if ( - arg?.type === 'CallExpression' && - arg.callee.type === 'Identifier' && - arg.callee.name === 'defineConfig' && - arg.arguments[0]?.type === 'ObjectExpression' - ) { - configObjectNodes.push(arg.arguments[0] as t.ObjectExpression); - } else if (arg?.type === 'ObjectExpression') { - configObjectNodes.push(arg); - } - } + } else { + const mergeConfigCall = getEffectiveMergeConfigCall(exportDefault.declaration, target); + if (mergeConfigCall && mergeConfigCall.arguments.length >= 2) { + // Collect all potential config object nodes from mergeConfig arguments. + // Each argument may be: defineConfig/defineProject({...}), a plain object {…}, + // an Identifier (variable reference), or wrapped in a TS type annotation. + const configObjectNodes: t.ObjectExpression[] = []; - // Prefer a config object that already contains a `test` property - const configObjectWithTest = configObjectNodes.find((obj) => - obj.properties.some( - (p) => - p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'test' - ) - ); + for (const arg of mergeConfigCall.arguments) { + const resolved = resolveExpression(arg as t.Expression, target); + if (resolved?.type === 'ObjectExpression') { + configObjectNodes.push(resolved); + } else if ( + resolved?.type === 'CallExpression' && + isDefineConfigLike(resolved) && + resolved.arguments[0]?.type === 'ObjectExpression' + ) { + configObjectNodes.push(resolved.arguments[0] as t.ObjectExpression); + } + } - const targetConfigObject = configObjectWithTest || configObjectNodes[0]; + // Prefer a config object that already contains a `test` property + const configObjectWithTest = configObjectNodes.find((obj) => + obj.properties.some( + (p) => + p.type === 'ObjectProperty' && + p.key.type === 'Identifier' && + p.key.name === 'test' + ) + ); - if (!targetConfigObject) { - return false; - } + const targetConfigObject = configObjectWithTest || configObjectNodes[0]; - // Check if there's already a test property in the target config - const existingTestProp = targetConfigObject.properties.find( - (p) => - p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'test' - ) as t.ObjectProperty | undefined; + if (!targetConfigObject) { + return false; + } - if (existingTestProp && existingTestProp.value.type === 'ObjectExpression') { - // Find the test property from the template (either workspace or projects) - const templateTestProp = properties.find( + // Check if there's already a test property in the target config + const existingTestProp = targetConfigObject.properties.find( (p) => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'test' ) as t.ObjectProperty | undefined; - const hasProjectsProp = ( - p: t.ObjectMethod | t.ObjectProperty | t.SpreadElement - ): p is t.ObjectProperty => - p.type === 'ObjectProperty' && - p.key.type === 'Identifier' && - p.key.name === 'projects' && - p.value.type === 'ArrayExpression'; - - // Check if the existing config already uses a projects array (multi-project setup). - // If so, we must append the storybook project to that array instead of wrapping - // the entire test config as a single project (which would cause double nesting). - const existingProjectsProp = existingTestProp.value.properties.find(hasProjectsProp); - - if (existingProjectsProp) { - // Existing config already has test.projects: append storybook project(s) to it - if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { - const templateProjectsProp = - templateTestProp.value.properties.find(hasProjectsProp); - if (templateProjectsProp && templateProjectsProp.value.type === 'ArrayExpression') { - const templateElements = (templateProjectsProp.value as t.ArrayExpression) - .elements; - (existingProjectsProp.value as t.ArrayExpression).elements.push( - ...templateElements - ); - } - // Merge other test-level options from template (e.g. coverage) into existing test - for (const templateProp of templateTestProp.value.properties) { - if ( - templateProp.type === 'ObjectProperty' && - templateProp.key.type === 'Identifier' && - (templateProp.key as t.Identifier).name !== 'projects' - ) { - const existingProp = existingTestProp.value.properties.find( - (p) => - p.type === 'ObjectProperty' && - p.key.type === 'Identifier' && - (p.key as t.Identifier).name === (templateProp.key as t.Identifier).name + // Resolve the test value – it may be a shorthand reference to a variable + // e.g. `const test = {...}; export default mergeConfig(viteConfig, { test })` + const resolvedTestValue: t.ObjectExpression | null = existingTestProp + ? existingTestProp.value.type === 'ObjectExpression' + ? existingTestProp.value + : (() => { + const r = resolveExpression( + existingTestProp.value as t.Expression, + target ); - if (!existingProp && templateProp.type === 'ObjectProperty') { - existingTestProp.value.properties.push(templateProp); - } - } - } - } - // Merge only non-test properties from template to avoid re-adding storybook project - const otherTemplateProps = properties.filter( - (p) => - !( - p.type === 'ObjectProperty' && - p.key.type === 'Identifier' && - p.key.name === 'test' - ) - ); - if (otherTemplateProps.length > 0) { - mergeProperties(otherTemplateProps, targetConfigObject.properties); - } - } else if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { - // Existing test has no projects array: wrap entire test config as one project - const workspaceOrProjectsProp = templateTestProp.value.properties.find( + return r?.type === 'ObjectExpression' ? r : null; + })() + : null; + + if (existingTestProp && resolvedTestValue !== null) { + // Find the test property from the template (either workspace or projects) + const templateTestProp = properties.find( (p) => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && - (p.key.name === 'workspace' || p.key.name === 'projects') + p.key.name === 'test' ) as t.ObjectProperty | undefined; - if ( - workspaceOrProjectsProp && - workspaceOrProjectsProp.value.type === 'ArrayExpression' - ) { - // Extract coverage config before creating the test project - const coverageProp = existingTestProp.value.properties.find( + const hasProjectsProp = ( + p: t.ObjectMethod | t.ObjectProperty | t.SpreadElement + ): p is t.ObjectProperty => + p.type === 'ObjectProperty' && + p.key.type === 'Identifier' && + p.key.name === 'projects' && + p.value.type === 'ArrayExpression'; + + // Check if the existing config already uses a projects array (multi-project setup). + // If so, we must append the storybook project to that array instead of wrapping + // the entire test config as a single project (which would cause double nesting). + const existingProjectsProp = resolvedTestValue.properties.find(hasProjectsProp); + + if (existingProjectsProp) { + // Existing config already has test.projects: append storybook project(s) to it + if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { + const templateProjectsProp = + templateTestProp.value.properties.find(hasProjectsProp); + if (templateProjectsProp && templateProjectsProp.value.type === 'ArrayExpression') { + const templateElements = (templateProjectsProp.value as t.ArrayExpression) + .elements; + (existingProjectsProp.value as t.ArrayExpression).elements.push( + ...templateElements + ); + } + // Merge other test-level options from template (e.g. coverage) into existing test + for (const templateProp of templateTestProp.value.properties) { + if ( + templateProp.type === 'ObjectProperty' && + templateProp.key.type === 'Identifier' && + (templateProp.key as t.Identifier).name !== 'projects' + ) { + const existingProp = resolvedTestValue.properties.find( + (p) => + p.type === 'ObjectProperty' && + p.key.type === 'Identifier' && + (p.key as t.Identifier).name === + (templateProp.key as t.Identifier).name + ); + if (!existingProp && templateProp.type === 'ObjectProperty') { + resolvedTestValue.properties.push(templateProp); + } + } + } + } + // Merge only non-test properties from template to avoid re-adding storybook project + const otherTemplateProps = properties.filter( + (p) => + !( + p.type === 'ObjectProperty' && + p.key.type === 'Identifier' && + p.key.name === 'test' + ) + ); + if (otherTemplateProps.length > 0) { + mergeProperties(otherTemplateProps, targetConfigObject.properties); + } + } else if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { + // Existing test has no projects array: wrap entire test config as one project + const workspaceOrProjectsProp = templateTestProp.value.properties.find( (p) => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && - p.key.name === 'coverage' + (p.key.name === 'workspace' || p.key.name === 'projects') ) as t.ObjectProperty | undefined; - // Create a new test config without the coverage property - const testPropsWithoutCoverage = existingTestProp.value.properties.filter( - (p) => p !== coverageProp - ); + if ( + workspaceOrProjectsProp && + workspaceOrProjectsProp.value.type === 'ArrayExpression' + ) { + // Extract coverage config before creating the test project + const coverageProp = resolvedTestValue.properties.find( + (p) => + p.type === 'ObjectProperty' && + p.key.type === 'Identifier' && + p.key.name === 'coverage' + ) as t.ObjectProperty | undefined; - const testConfigForProject: t.ObjectExpression = { - type: 'ObjectExpression', - properties: testPropsWithoutCoverage, - }; - - // Create the existing test project - const existingTestProject: t.ObjectExpression = { - type: 'ObjectExpression', - properties: [ - { - type: 'ObjectProperty', - key: { type: 'Identifier', name: 'extends' }, - value: { type: 'BooleanLiteral', value: true }, - computed: false, - shorthand: false, - }, - { - type: 'ObjectProperty', - key: { type: 'Identifier', name: 'test' }, - value: testConfigForProject, - computed: false, - shorthand: false, - }, - ], - }; - - // Add the existing test project to the template's array - workspaceOrProjectsProp.value.elements.unshift(existingTestProject); - - // Remove the existing test property from the target config since we're moving it to the array - targetConfigObject.properties = targetConfigObject.properties.filter( - (p) => p !== existingTestProp - ); + // Create a new test config without the coverage property + const testPropsWithoutCoverage = resolvedTestValue.properties.filter( + (p) => p !== coverageProp + ); - // If there was a coverage config, add it to the template's test config (at the top level of the test object) - // Insert it at the beginning so it appears before workspace/projects - if (coverageProp && templateTestProp.value.type === 'ObjectExpression') { - templateTestProp.value.properties.unshift(coverageProp); - } + const testConfigForProject: t.ObjectExpression = { + type: 'ObjectExpression', + properties: testPropsWithoutCoverage, + }; - // Merge the template properties (which now include our existing test project in the array) - mergeProperties(properties, targetConfigObject.properties); + // Create the existing test project + const existingTestProject: t.ObjectExpression = { + type: 'ObjectExpression', + properties: [ + { + type: 'ObjectProperty', + key: { type: 'Identifier', name: 'extends' }, + value: { type: 'BooleanLiteral', value: true }, + computed: false, + shorthand: false, + }, + { + type: 'ObjectProperty', + key: { type: 'Identifier', name: 'test' }, + value: testConfigForProject, + computed: false, + shorthand: false, + }, + ], + }; + + // Add the existing test project to the template's array + workspaceOrProjectsProp.value.elements.unshift(existingTestProject); + + // Remove the existing test property from the target config since we're moving it to the array + targetConfigObject.properties = targetConfigObject.properties.filter( + (p) => p !== existingTestProp + ); + + // If there was a coverage config, add it to the template's test config (at the top level of the test object) + // Insert it at the beginning so it appears before workspace/projects + if (coverageProp && templateTestProp.value.type === 'ObjectExpression') { + templateTestProp.value.properties.unshift(coverageProp); + } + + // Merge the template properties (which now include our existing test project in the array) + mergeProperties(properties, targetConfigObject.properties); + } else { + // Fallback to original behavior if template structure is unexpected + mergeProperties(properties, targetConfigObject.properties); + } } else { - // Fallback to original behavior if template structure is unexpected + // Fallback to original behavior if template doesn't have expected structure mergeProperties(properties, targetConfigObject.properties); } } else { - // Fallback to original behavior if template doesn't have expected structure + // No existing test config, just merge normally mergeProperties(properties, targetConfigObject.properties); } - } else { - // No existing test config, just merge normally - mergeProperties(properties, targetConfigObject.properties); + updated = true; } - updated = true; } } } From 7346a4cb630462ac5229fb6707890e4cf4d849c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:34:51 +0000 Subject: [PATCH 03/14] fix: address code review - improve type safety and readability - Use proper union type for TS expression unwrapping (TSAsExpression | TSSatisfiesExpression | TSTypeAssertion) - Extract resolveTestPropValue helper to replace nested ternary with a named function Co-authored-by: valentinpalkovic <5889929+valentinpalkovic@users.noreply.github.com> --- code/addons/vitest/src/updateVitestFile.ts | 31 ++++++++++++++-------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts index 9d0490d26d83..eb637ebbb63c 100644 --- a/code/addons/vitest/src/updateVitestFile.ts +++ b/code/addons/vitest/src/updateVitestFile.ts @@ -77,7 +77,9 @@ const unwrapTSExpression = (expr: t.Expression | t.Declaration): t.Expression => expr.type === 'TSSatisfiesExpression' || expr.type === 'TSTypeAssertion' ) { - return unwrapTSExpression((expr as t.TSAsExpression).expression); + return unwrapTSExpression( + (expr as t.TSAsExpression | t.TSSatisfiesExpression | t.TSTypeAssertion).expression + ); } return expr as t.Expression; }; @@ -116,6 +118,21 @@ const isDefineConfigLike = (node: t.CallExpression): boolean => node.callee.type === 'Identifier' && (node.callee.name === 'defineConfig' || node.callee.name === 'defineProject'); +/** + * Resolves the value of a `test` ObjectProperty to an ObjectExpression. + * Handles both inline objects and shorthand identifier references, e.g.: + * `{ test: { ... } }` → returns the inline ObjectExpression + * `const test = {...}; { test }` → resolves the identifier to its initializer + */ +const resolveTestPropValue = ( + testProp: t.ObjectProperty, + ast: BabelFile['ast'] +): t.ObjectExpression | null => { + if (testProp.value.type === 'ObjectExpression') return testProp.value; + const resolved = resolveExpression(testProp.value as t.Expression, ast); + return resolved?.type === 'ObjectExpression' ? resolved : null; +}; + /** * Extracts the effective mergeConfig call from a declaration, handling wrappers: * - TypeScript type annotations (as X, satisfies X) @@ -331,16 +348,8 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as // Resolve the test value – it may be a shorthand reference to a variable // e.g. `const test = {...}; export default mergeConfig(viteConfig, { test })` - const resolvedTestValue: t.ObjectExpression | null = existingTestProp - ? existingTestProp.value.type === 'ObjectExpression' - ? existingTestProp.value - : (() => { - const r = resolveExpression( - existingTestProp.value as t.Expression, - target - ); - return r?.type === 'ObjectExpression' ? r : null; - })() + const resolvedTestValue = existingTestProp + ? resolveTestPropValue(existingTestProp, target) : null; if (existingTestProp && resolvedTestValue !== null) { From 20e48e6f545069378727fcaf413fffdc11a5b50a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:22:40 +0000 Subject: [PATCH 04/14] fix: resolve eslint/prettier issues in updateVitestFile.ts Co-authored-by: valentinpalkovic <5889929+valentinpalkovic@users.noreply.github.com> --- code/addons/vitest/src/updateVitestFile.ts | 66 +++++++++++++--------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts index eb637ebbb63c..64120c90dc4d 100644 --- a/code/addons/vitest/src/updateVitestFile.ts +++ b/code/addons/vitest/src/updateVitestFile.ts @@ -68,9 +68,7 @@ const mergeProperties = ( } }; -/** - * Recursively unwraps TypeScript type annotation expressions (as X, satisfies X, expr). - */ +/** Recursively unwraps TypeScript type annotation expressions (as X, satisfies X, expr). */ const unwrapTSExpression = (expr: t.Expression | t.Declaration): t.Expression => { if ( expr.type === 'TSAsExpression' || @@ -85,16 +83,20 @@ const unwrapTSExpression = (expr: t.Expression | t.Declaration): t.Expression => }; /** - * Resolves an expression through variable references and TypeScript type annotations. - * Handles: Identifier (variable lookup), TSAsExpression, TSSatisfiesExpression, TSTypeAssertion. + * Resolves an expression through variable references and TypeScript type annotations. Handles: + * Identifier (variable lookup), TSAsExpression, TSSatisfiesExpression, TSTypeAssertion. */ const resolveExpression = ( expr: t.Expression | t.Declaration | null | undefined, ast: BabelFile['ast'] ): t.Expression | null => { - if (!expr) return null; + if (!expr) { + return null; + } const unwrapped = unwrapTSExpression(expr as t.Expression | t.Declaration); - if (unwrapped.type !== 'Identifier') return unwrapped; + if (unwrapped.type !== 'Identifier') { + return unwrapped; + } const varName = (unwrapped as t.Identifier).name; const varDecl = ast.program.body.find( (n): n is t.VariableDeclaration => @@ -103,48 +105,54 @@ const resolveExpression = ( (d) => d.id.type === 'Identifier' && (d.id as t.Identifier).name === varName ) ); - if (!varDecl) return unwrapped; + if (!varDecl) { + return unwrapped; + } const declarator = varDecl.declarations.find( (d) => d.id.type === 'Identifier' && (d.id as t.Identifier).name === varName ); - if (!declarator?.init) return unwrapped; + if (!declarator?.init) { + return unwrapped; + } return resolveExpression(declarator.init, ast); }; -/** - * Returns true if the call expression is a defineConfig or defineProject call. - */ +/** Returns true if the call expression is a defineConfig or defineProject call. */ const isDefineConfigLike = (node: t.CallExpression): boolean => node.callee.type === 'Identifier' && (node.callee.name === 'defineConfig' || node.callee.name === 'defineProject'); /** - * Resolves the value of a `test` ObjectProperty to an ObjectExpression. - * Handles both inline objects and shorthand identifier references, e.g.: - * `{ test: { ... } }` → returns the inline ObjectExpression - * `const test = {...}; { test }` → resolves the identifier to its initializer + * Resolves the value of a `test` ObjectProperty to an ObjectExpression. Handles both inline objects + * and shorthand identifier references, e.g.: `{ test: { ... } }` → returns the inline + * ObjectExpression `const test = {...}; { test }` → resolves the identifier to its initializer */ const resolveTestPropValue = ( testProp: t.ObjectProperty, ast: BabelFile['ast'] ): t.ObjectExpression | null => { - if (testProp.value.type === 'ObjectExpression') return testProp.value; + if (testProp.value.type === 'ObjectExpression') { + return testProp.value; + } const resolved = resolveExpression(testProp.value as t.Expression, ast); return resolved?.type === 'ObjectExpression' ? resolved : null; }; /** * Extracts the effective mergeConfig call from a declaration, handling wrappers: + * * - TypeScript type annotations (as X, satisfies X) - * - defineConfig(mergeConfig(...)) outer wrapper - * - variable references (export default config where config = mergeConfig(...)) + * - DefineConfig(mergeConfig(...)) outer wrapper + * - Variable references (export default config where config = mergeConfig(...)) */ const getEffectiveMergeConfigCall = ( decl: t.Expression | t.Declaration, ast: BabelFile['ast'] ): t.CallExpression | null => { const resolved = resolveExpression(decl, ast); - if (!resolved || resolved.type !== 'CallExpression') return null; + if (!resolved || resolved.type !== 'CallExpression') { + return null; + } // Handle defineConfig(mergeConfig(...)) – arg may itself be wrapped in a TS type expression if (isDefineConfigLike(resolved) && resolved.arguments.length > 0) { @@ -168,16 +176,18 @@ const getEffectiveMergeConfigCall = ( /** * Resolves the target's default export to the actual config object expression we can merge into. - * Handles: export default defineConfig({}), export default defineProject({}), - * export default {}, and export default config (where config is a variable holding one of those), - * as well as TypeScript type annotations on the declaration. + * Handles: export default defineConfig({}), export default defineProject({}), export default {}, + * and export default config (where config is a variable holding one of those), as well as + * TypeScript type annotations on the declaration. */ const getTargetConfigObject = ( target: BabelFile['ast'], exportDefault: t.ExportDefaultDeclaration ): t.ObjectExpression | null => { const resolved = resolveExpression(exportDefault.declaration, target); - if (!resolved) return null; + if (!resolved) { + return null; + } if (resolved.type === 'ObjectExpression') { return resolved; } @@ -379,7 +389,10 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { const templateProjectsProp = templateTestProp.value.properties.find(hasProjectsProp); - if (templateProjectsProp && templateProjectsProp.value.type === 'ArrayExpression') { + if ( + templateProjectsProp && + templateProjectsProp.value.type === 'ArrayExpression' + ) { const templateElements = (templateProjectsProp.value as t.ArrayExpression) .elements; (existingProjectsProp.value as t.ArrayExpression).elements.push( @@ -397,8 +410,7 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as (p) => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && - (p.key as t.Identifier).name === - (templateProp.key as t.Identifier).name + (p.key as t.Identifier).name === (templateProp.key as t.Identifier).name ); if (!existingProp && templateProp.type === 'ObjectProperty') { resolvedTestValue.properties.push(templateProp); From 653e55f397847b606a61a1eaba18fce69c29918b Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Wed, 11 Mar 2026 22:22:06 +0100 Subject: [PATCH 05/14] fix: implement expression resolver for TypeScript type annotations --- code/addons/vitest/src/updateVitestFile.ts | 349 +++++++++------------ code/core/src/babel/expression-resolver.ts | 44 +++ code/core/src/babel/index.ts | 1 + 3 files changed, 200 insertions(+), 194 deletions(-) create mode 100644 code/core/src/babel/expression-resolver.ts diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts index 64120c90dc4d..11663331a5a2 100644 --- a/code/addons/vitest/src/updateVitestFile.ts +++ b/code/addons/vitest/src/updateVitestFile.ts @@ -1,3 +1,4 @@ +import { resolveExpression } from 'storybook/internal/babel'; import type { BabelFile, types as t } from 'storybook/internal/babel'; import { normalize } from 'pathe'; @@ -68,55 +69,6 @@ const mergeProperties = ( } }; -/** Recursively unwraps TypeScript type annotation expressions (as X, satisfies X, expr). */ -const unwrapTSExpression = (expr: t.Expression | t.Declaration): t.Expression => { - if ( - expr.type === 'TSAsExpression' || - expr.type === 'TSSatisfiesExpression' || - expr.type === 'TSTypeAssertion' - ) { - return unwrapTSExpression( - (expr as t.TSAsExpression | t.TSSatisfiesExpression | t.TSTypeAssertion).expression - ); - } - return expr as t.Expression; -}; - -/** - * Resolves an expression through variable references and TypeScript type annotations. Handles: - * Identifier (variable lookup), TSAsExpression, TSSatisfiesExpression, TSTypeAssertion. - */ -const resolveExpression = ( - expr: t.Expression | t.Declaration | null | undefined, - ast: BabelFile['ast'] -): t.Expression | null => { - if (!expr) { - return null; - } - const unwrapped = unwrapTSExpression(expr as t.Expression | t.Declaration); - if (unwrapped.type !== 'Identifier') { - return unwrapped; - } - const varName = (unwrapped as t.Identifier).name; - const varDecl = ast.program.body.find( - (n): n is t.VariableDeclaration => - n.type === 'VariableDeclaration' && - n.declarations.some( - (d) => d.id.type === 'Identifier' && (d.id as t.Identifier).name === varName - ) - ); - if (!varDecl) { - return unwrapped; - } - const declarator = varDecl.declarations.find( - (d) => d.id.type === 'Identifier' && (d.id as t.Identifier).name === varName - ); - if (!declarator?.init) { - return unwrapped; - } - return resolveExpression(declarator.init, ast); -}; - /** Returns true if the call expression is a defineConfig or defineProject call. */ const isDefineConfigLike = (node: t.CallExpression): boolean => node.callee.type === 'Identifier' && @@ -138,6 +90,144 @@ const resolveTestPropValue = ( return resolved?.type === 'ObjectExpression' ? resolved : null; }; +/** Finds a named ObjectProperty in an object expression's properties. */ +const findNamedProp = ( + properties: t.ObjectExpression['properties'], + name: string +): t.ObjectProperty | undefined => + properties.find( + (p): p is t.ObjectProperty => + p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === name + ); + +/** Type guard for a property that is a `projects` key with an ArrayExpression value. */ +const isProjectsArrayProp = ( + p: t.ObjectMethod | t.ObjectProperty | t.SpreadElement +): p is t.ObjectProperty => + p.type === 'ObjectProperty' && + p.key.type === 'Identifier' && + p.key.name === 'projects' && + p.value.type === 'ArrayExpression'; + +/** + * Appends storybook project(s) from template into an existing `test.projects` array, then merges + * any additional test-level options (e.g. coverage) that don't already exist. + */ +const appendToExistingProjects = ( + existingProjectsProp: t.ObjectProperty, + resolvedTestValue: t.ObjectExpression, + templateTestProp: t.ObjectProperty | undefined, + properties: t.ObjectExpression['properties'], + targetConfigObject: t.ObjectExpression +) => { + if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { + // Append template projects to existing projects array + const templateProjectsProp = templateTestProp.value.properties.find(isProjectsArrayProp); + if (templateProjectsProp && templateProjectsProp.value.type === 'ArrayExpression') { + (existingProjectsProp.value as t.ArrayExpression).elements.push( + ...(templateProjectsProp.value as t.ArrayExpression).elements + ); + } + + // Merge other test-level options from template (e.g. coverage) that don't already exist + const existingTestPropNames = new Set( + resolvedTestValue.properties + .filter( + (p): p is t.ObjectProperty => p.type === 'ObjectProperty' && p.key.type === 'Identifier' + ) + .map((p) => (p.key as t.Identifier).name) + ); + for (const templateProp of templateTestProp.value.properties) { + if ( + templateProp.type === 'ObjectProperty' && + templateProp.key.type === 'Identifier' && + (templateProp.key as t.Identifier).name !== 'projects' && + !existingTestPropNames.has((templateProp.key as t.Identifier).name) + ) { + resolvedTestValue.properties.push(templateProp); + } + } + } + + // Merge only non-test properties from template + const otherTemplateProps = properties.filter( + (p) => !(p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'test') + ); + if (otherTemplateProps.length > 0) { + mergeProperties(otherTemplateProps, targetConfigObject.properties); + } +}; + +/** + * Wraps the existing test config as one project entry inside the template's workspace/projects + * array, extracting coverage to the top-level test object. + */ +const wrapTestConfigAsProject = ( + resolvedTestValue: t.ObjectExpression, + existingTestProp: t.ObjectProperty, + templateTestProp: t.ObjectProperty, + properties: t.ObjectExpression['properties'], + targetConfigObject: t.ObjectExpression +) => { + const workspaceOrProjectsProp = + templateTestProp.value.type === 'ObjectExpression' + ? (templateTestProp.value.properties.find( + (p) => + p.type === 'ObjectProperty' && + p.key.type === 'Identifier' && + (p.key.name === 'workspace' || p.key.name === 'projects') + ) as t.ObjectProperty | undefined) + : undefined; + + if (!workspaceOrProjectsProp || workspaceOrProjectsProp.value.type !== 'ArrayExpression') { + mergeProperties(properties, targetConfigObject.properties); + return; + } + + // Extract coverage config before creating the test project + const coverageProp = findNamedProp(resolvedTestValue.properties, 'coverage'); + const testPropsWithoutCoverage = resolvedTestValue.properties.filter((p) => p !== coverageProp); + + // Create the existing test project: { extends: true, test: { ...existingTestProps } } + const existingTestProject: t.ObjectExpression = { + type: 'ObjectExpression', + properties: [ + { + type: 'ObjectProperty', + key: { type: 'Identifier', name: 'extends' } as t.Identifier, + value: { type: 'BooleanLiteral', value: true } as t.BooleanLiteral, + computed: false, + shorthand: false, + } as t.ObjectProperty, + { + type: 'ObjectProperty', + key: { type: 'Identifier', name: 'test' } as t.Identifier, + value: { + type: 'ObjectExpression', + properties: testPropsWithoutCoverage, + } as t.ObjectExpression, + computed: false, + shorthand: false, + } as t.ObjectProperty, + ], + }; + + // Add the existing test project to the template's array + workspaceOrProjectsProp.value.elements.unshift(existingTestProject); + + // Remove the existing test property from the target config (it's now in the array) + targetConfigObject.properties = targetConfigObject.properties.filter( + (p) => p !== existingTestProp + ); + + // Hoist coverage to the top-level test object so it applies to all projects + if (coverageProp && templateTestProp.value.type === 'ObjectExpression') { + templateTestProp.value.properties.unshift(coverageProp); + } + + mergeProperties(properties, targetConfigObject.properties); +}; + /** * Extracts the effective mergeConfig call from a declaration, handling wrappers: * @@ -350,164 +440,35 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as return false; } - // Check if there's already a test property in the target config - const existingTestProp = targetConfigObject.properties.find( - (p) => - p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'test' - ) as t.ObjectProperty | undefined; - - // Resolve the test value – it may be a shorthand reference to a variable - // e.g. `const test = {...}; export default mergeConfig(viteConfig, { test })` + const existingTestProp = findNamedProp(targetConfigObject.properties, 'test'); const resolvedTestValue = existingTestProp ? resolveTestPropValue(existingTestProp, target) : null; + const templateTestProp = findNamedProp(properties, 'test'); if (existingTestProp && resolvedTestValue !== null) { - // Find the test property from the template (either workspace or projects) - const templateTestProp = properties.find( - (p) => - p.type === 'ObjectProperty' && - p.key.type === 'Identifier' && - p.key.name === 'test' - ) as t.ObjectProperty | undefined; - - const hasProjectsProp = ( - p: t.ObjectMethod | t.ObjectProperty | t.SpreadElement - ): p is t.ObjectProperty => - p.type === 'ObjectProperty' && - p.key.type === 'Identifier' && - p.key.name === 'projects' && - p.value.type === 'ArrayExpression'; - - // Check if the existing config already uses a projects array (multi-project setup). - // If so, we must append the storybook project to that array instead of wrapping - // the entire test config as a single project (which would cause double nesting). - const existingProjectsProp = resolvedTestValue.properties.find(hasProjectsProp); + const existingProjectsProp = resolvedTestValue.properties.find(isProjectsArrayProp); if (existingProjectsProp) { - // Existing config already has test.projects: append storybook project(s) to it - if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { - const templateProjectsProp = - templateTestProp.value.properties.find(hasProjectsProp); - if ( - templateProjectsProp && - templateProjectsProp.value.type === 'ArrayExpression' - ) { - const templateElements = (templateProjectsProp.value as t.ArrayExpression) - .elements; - (existingProjectsProp.value as t.ArrayExpression).elements.push( - ...templateElements - ); - } - // Merge other test-level options from template (e.g. coverage) into existing test - for (const templateProp of templateTestProp.value.properties) { - if ( - templateProp.type === 'ObjectProperty' && - templateProp.key.type === 'Identifier' && - (templateProp.key as t.Identifier).name !== 'projects' - ) { - const existingProp = resolvedTestValue.properties.find( - (p) => - p.type === 'ObjectProperty' && - p.key.type === 'Identifier' && - (p.key as t.Identifier).name === (templateProp.key as t.Identifier).name - ); - if (!existingProp && templateProp.type === 'ObjectProperty') { - resolvedTestValue.properties.push(templateProp); - } - } - } - } - // Merge only non-test properties from template to avoid re-adding storybook project - const otherTemplateProps = properties.filter( - (p) => - !( - p.type === 'ObjectProperty' && - p.key.type === 'Identifier' && - p.key.name === 'test' - ) + appendToExistingProjects( + existingProjectsProp, + resolvedTestValue, + templateTestProp, + properties, + targetConfigObject ); - if (otherTemplateProps.length > 0) { - mergeProperties(otherTemplateProps, targetConfigObject.properties); - } } else if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { - // Existing test has no projects array: wrap entire test config as one project - const workspaceOrProjectsProp = templateTestProp.value.properties.find( - (p) => - p.type === 'ObjectProperty' && - p.key.type === 'Identifier' && - (p.key.name === 'workspace' || p.key.name === 'projects') - ) as t.ObjectProperty | undefined; - - if ( - workspaceOrProjectsProp && - workspaceOrProjectsProp.value.type === 'ArrayExpression' - ) { - // Extract coverage config before creating the test project - const coverageProp = resolvedTestValue.properties.find( - (p) => - p.type === 'ObjectProperty' && - p.key.type === 'Identifier' && - p.key.name === 'coverage' - ) as t.ObjectProperty | undefined; - - // Create a new test config without the coverage property - const testPropsWithoutCoverage = resolvedTestValue.properties.filter( - (p) => p !== coverageProp - ); - - const testConfigForProject: t.ObjectExpression = { - type: 'ObjectExpression', - properties: testPropsWithoutCoverage, - }; - - // Create the existing test project - const existingTestProject: t.ObjectExpression = { - type: 'ObjectExpression', - properties: [ - { - type: 'ObjectProperty', - key: { type: 'Identifier', name: 'extends' }, - value: { type: 'BooleanLiteral', value: true }, - computed: false, - shorthand: false, - }, - { - type: 'ObjectProperty', - key: { type: 'Identifier', name: 'test' }, - value: testConfigForProject, - computed: false, - shorthand: false, - }, - ], - }; - - // Add the existing test project to the template's array - workspaceOrProjectsProp.value.elements.unshift(existingTestProject); - - // Remove the existing test property from the target config since we're moving it to the array - targetConfigObject.properties = targetConfigObject.properties.filter( - (p) => p !== existingTestProp - ); - - // If there was a coverage config, add it to the template's test config (at the top level of the test object) - // Insert it at the beginning so it appears before workspace/projects - if (coverageProp && templateTestProp.value.type === 'ObjectExpression') { - templateTestProp.value.properties.unshift(coverageProp); - } - - // Merge the template properties (which now include our existing test project in the array) - mergeProperties(properties, targetConfigObject.properties); - } else { - // Fallback to original behavior if template structure is unexpected - mergeProperties(properties, targetConfigObject.properties); - } + wrapTestConfigAsProject( + resolvedTestValue, + existingTestProp, + templateTestProp, + properties, + targetConfigObject + ); } else { - // Fallback to original behavior if template doesn't have expected structure mergeProperties(properties, targetConfigObject.properties); } } else { - // No existing test config, just merge normally mergeProperties(properties, targetConfigObject.properties); } updated = true; diff --git a/code/core/src/babel/expression-resolver.ts b/code/core/src/babel/expression-resolver.ts new file mode 100644 index 000000000000..7ecbf312e80b --- /dev/null +++ b/code/core/src/babel/expression-resolver.ts @@ -0,0 +1,44 @@ +import type * as t from '@babel/types'; + +/** Recursively unwraps TypeScript type annotation expressions (as X, satisfies X, expr). */ +export const unwrapTSExpression = (expr: t.Expression | t.Declaration): t.Expression => { + if ( + expr.type === 'TSAsExpression' || + expr.type === 'TSSatisfiesExpression' || + expr.type === 'TSTypeAssertion' + ) { + return unwrapTSExpression( + (expr as t.TSAsExpression | t.TSSatisfiesExpression | t.TSTypeAssertion).expression + ); + } + return expr as t.Expression; +}; + +/** + * Resolves an expression through variable references and TypeScript type annotations. Handles: + * Identifier (variable lookup), TSAsExpression, TSSatisfiesExpression, TSTypeAssertion. Limits + * recursion depth to prevent infinite loops on circular variable references. + */ +export const resolveExpression = ( + expr: t.Expression | t.Declaration | null | undefined, + ast: t.File, + depth = 0, + maxDepth = 10 +): t.Expression | null => { + if (!expr || depth > maxDepth) { + return null; + } + const unwrapped = unwrapTSExpression(expr as t.Expression | t.Declaration); + if (unwrapped.type !== 'Identifier') { + return unwrapped; + } + const varName = (unwrapped as t.Identifier).name; + const declarator = ast.program.body + .filter((n): n is t.VariableDeclaration => n.type === 'VariableDeclaration') + .flatMap((varDecl) => varDecl.declarations) + .find((d) => d.id.type === 'Identifier' && (d.id as t.Identifier).name === varName); + if (!declarator?.init) { + return unwrapped; + } + return resolveExpression(declarator.init, ast, depth + 1, maxDepth); +}; diff --git a/code/core/src/babel/index.ts b/code/core/src/babel/index.ts index 8aee3d8f5391..79e6fdd08d73 100644 --- a/code/core/src/babel/index.ts +++ b/code/core/src/babel/index.ts @@ -14,6 +14,7 @@ import * as types from '@babel/types'; import * as recast from 'recast'; export * from './babelParse'; +export { unwrapTSExpression, resolveExpression } from './expression-resolver'; // @ts-expect-error (needed due to it's use of `exports.default`) const traverse = (bt.default || bt) as typeof bt; From d5acaa693044c77abe328e6e35b23ae320cdf887 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Wed, 11 Mar 2026 23:08:51 +0100 Subject: [PATCH 06/14] Split tests into multiple files and fine-tune workspace vs projects detection --- code/addons/vitest/src/postinstall.ts | 49 +- .../src/updateVitestFile.config.3.2.test.ts | 1777 ++++++++++++++++ .../src/updateVitestFile.config.4.test.ts | 1797 ++++++++++++++++ .../src/updateVitestFile.config.test.ts | 1774 ++++++++++++++++ .../updateVitestFile.config.workspace.test.ts | 135 ++ .../vitest/src/updateVitestFile.test.ts | 1874 +---------------- 6 files changed, 5531 insertions(+), 1875 deletions(-) create mode 100644 code/addons/vitest/src/updateVitestFile.config.3.2.test.ts create mode 100644 code/addons/vitest/src/updateVitestFile.config.4.test.ts create mode 100644 code/addons/vitest/src/updateVitestFile.config.test.ts create mode 100644 code/addons/vitest/src/updateVitestFile.config.workspace.test.ts diff --git a/code/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts index 543d5c579b4f..546e0943cebb 100644 --- a/code/addons/vitest/src/postinstall.ts +++ b/code/addons/vitest/src/postinstall.ts @@ -190,10 +190,16 @@ export default async function postInstall(options: PostinstallOptions) { ); } - const getTemplateName = () => { + const getTemplateName = (configContent?: string) => { if (isVitest4OrNewer) { return 'vitest.config.4.template'; } else if (isVitest3_2To4) { + // In Vitest 3.2, `workspace` was deprecated in favor of `projects` but still works. + // If the user's existing config already uses `workspace`, use the old template that + // also uses `workspace` so that the merge doesn't introduce both keys. + if (configContent && configUsesWorkspace(configContent)) { + return 'vitest.config.template'; + } return 'vitest.config.3.2.template'; } return 'vitest.config.template'; @@ -255,7 +261,7 @@ export default async function postInstall(options: PostinstallOptions) { /\/\/\/\s*/ ); - const templateName = getTemplateName(); + const templateName = getTemplateName(configFile); const alreadyConfigured = isConfigAlreadySetup(rootConfig, configFile); @@ -437,3 +443,42 @@ export function isConfigAlreadySetup(_configPath: string, configContent: string) return pluginReferenced; } + +/** + * Checks whether an existing config file uses `test.workspace` (Vitest 3.0-3.1 style) rather than + * `test.projects` (Vitest 3.2+ style). + */ +function configUsesWorkspace(configContent: string): boolean { + let ast: ReturnType; + try { + ast = babelParse(configContent); + } catch { + return false; + } + + let found = false; + + traverse(ast, { + ObjectProperty(path) { + if (found) { + path.stop(); + return; + } + const key = path.node.key; + if (key.type === 'Identifier' && key.name === 'workspace') { + // Check that this is inside a `test` property to avoid false positives + const parent = path.parentPath?.parentPath; + if ( + parent?.isObjectProperty() && + parent.node.key.type === 'Identifier' && + parent.node.key.name === 'test' + ) { + found = true; + path.stop(); + } + } + }, + }); + + return found; +} diff --git a/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts b/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts new file mode 100644 index 000000000000..b629341520af --- /dev/null +++ b/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts @@ -0,0 +1,1777 @@ +import { join } from 'node:path'; + +import { describe, expect, it, vi } from 'vitest'; + +import * as babel from 'storybook/internal/babel'; + +import { getDiff } from '../../../core/src/core-server/utils/save-story/getDiff'; +import { loadTemplate, updateConfigFile } from './updateVitestFile'; + +vi.mock('storybook/internal/node-logger', () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, +})); + +vi.mock('../../../core/src/shared/utils/module', () => ({ + resolvePackageDir: vi.fn().mockImplementation(() => join(__dirname, '..')), +})); + +describe('updateConfigFile', () => { + it('updates vite config file with existing workspace (falls back to workspace template)', async () => { + // When Vitest 3.2 user still has deprecated `workspace` key, postinstall should + // detect this and use the old workspace-based template to append to the existing array + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + workspace: ['packages/*'] + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly — appends to existing workspace array + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + + - workspace: ['packages/*'] + - + + workspace: ['packages/*', { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('supports object notation without defineConfig with existing workspace (falls back to workspace template)', async () => { + // Same as above: existing `workspace` in target means postinstall uses old template + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default { + plugins: [react()], + test: { + globals: true, + workspace: ['packages/*'] + }, + } + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly — appends to existing workspace array + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default { + plugins: [react()], + test: { + globals: true, + + - workspace: ['packages/*'] + - + + workspace: ['packages/*', { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + };" + `); + }); + + it('does not support function notation', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig(() => ({ + plugins: [react()], + test: { + globals: true, + projects: ['packages/*'] + }, + })) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(false); + + const after = babel.generate(target).code; + + // check if the code was NOT updated + expect(after).toBe(before); + }); + + it('adds projects property to test config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + + - globals: true + - + + globals: true, + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('updates config which is not exported immediately', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig } from 'vite' + import viteReact from '@vitejs/plugin-react' + import { fileURLToPath, URL } from 'url' + + const config = defineConfig({ + resolve: { + preserveSymlinks: true, + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + plugins: [ + viteReact(), + ], + }) + + export default config + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig } from 'vite'; + import viteReact from '@vitejs/plugin-react'; + import { fileURLToPath, URL } from 'url'; + + + import path from 'node:path'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const config = defineConfig({ + resolve: { + preserveSymlinks: true, + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + + - plugins: [viteReact()] + - + + plugins: [viteReact()], + + test: { + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + }); + export default config;" +`); + }); + + it('edits projects property of test config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + projects: ['packages/*', {some: 'config'}] + } + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + projects: ['packages/*', { + some: 'config' + + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + + }] + } + });" + `); + }); + + it('adds workspace property to test config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + + - globals: true + - + + globals: true, + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('adds test property to vite config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + + - plugins: [react()] + - + + plugins: [react()], + + test: { + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + });" + `); + }); + + it('supports mergeConfig with multiple defineConfig calls, finding the one with test', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vite' + import { defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + plugins: [react()], + }), + defineConfig({ + test: { + environment: 'jsdom', + } + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vite'; + import { defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + plugins: [react()] + }), defineConfig({ + test: { + + - environment: 'jsdom' + - + + projects: [{ + + extends: true, + + test: { + + environment: 'jsdom' + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + it('supports mergeConfig without defineConfig calls', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vite' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + { + plugins: [react()], + test: { + environment: 'jsdom', + } + } + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vite'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, { + plugins: [react()], + test: { + + - environment: 'jsdom' + - + + projects: [{ + + extends: true, + + test: { + + environment: 'jsdom' + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('supports mergeConfig without config containing test property', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vite' + import { defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + plugins: [react()], + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vite'; + import { defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + + - plugins: [react()] + - + + plugins: [react()], + + test: { + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + }));" + `); + }); + + it('supports mergeConfig with defineConfig pattern using projects (Vitest 3.2+)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + // https://vite.dev/config/ + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + globals: true, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import viteConfig from './vite.config'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - globals: true + - + + projects: [{ + + extends: true, + + test: { + + globals: true + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('appends storybook project to existing test.projects array (no double nesting)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + expect: { requireAssertions: true }, + projects: [ + { + extends: "./vite.config.ts", + test: { name: "client" }, + }, + { + extends: "./vite.config.ts", + test: { name: "server" }, + }, + ], + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly (storybook project appended to existing projects, no double nesting) + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + expect: { + requireAssertions: true + ... + test: { + name: "server" + } + + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + + }] + } + }));" + `); + }); + + it('extracts coverage config and keeps it at top level when using workspace', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + coverage: { + exclude: [ + 'storybook.setup.ts', + '**/*.stories.*', + ], + }, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + // Coverage should stay at the top level, not moved into the workspace + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'], + - + coverage: { + exclude: ['storybook.setup.ts', '**/*.stories.*'] + + - } + - + + }, + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('extracts coverage config and keeps it at top level when using projects', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + coverage: { + exclude: [ + 'storybook.setup.ts', + '**/*.stories.*', + ], + }, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + // Coverage should stay at the top level, not moved into the projects + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'], + - + coverage: { + exclude: ['storybook.setup.ts', '**/*.stories.*'] + + - } + - + + }, + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('supports defineConfig wrapping mergeConfig', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default defineConfig(mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + })) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig(mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('supports defineConfig wrapping mergeConfig with satisfies operator', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + import type { ViteUserConfig } from 'vitest/config' + + export default defineConfig( + mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) satisfies ViteUserConfig + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + import type { ViteUserConfig } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig(mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }) satisfies ViteUserConfig);" + `); + }); + + it('supports mergeConfig with as operator (TSAsExpression)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + import type { ViteUserConfig } from 'vitest/config' + + export default mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) as ViteUserConfig + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + import type { ViteUserConfig } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }) as ViteUserConfig;" + `); + }); + + it('supports mergeConfig with test defined as a constant (shorthand property)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const test = { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + } + + export default mergeConfig(viteConfig, { test }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const test = { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'] + }; + export default mergeConfig(viteConfig, { + + - test + - + + test: { + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + });" + `); + }); + + it('supports const defined config re-exported (export default config)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const config = mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) + ) + + export default config + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const config = mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + })); + export default config;" + `); + }); + + it('supports defineProject instead of defineConfig', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineProject } from 'vitest/config' + + export default defineProject({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineProject } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineProject({ + test: { + name: 'node', + environment: 'happy-dom', + + - include: ['**/*.test.ts'] + - + + include: ['**/*.test.ts'], + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('supports mergeConfig with config object as a constant variable', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const vitestConfig = { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + } + } + + export default mergeConfig(viteConfig, vitestConfig) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const vitestConfig = { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }; + export default mergeConfig(viteConfig, vitestConfig);" + `); + }); +}); diff --git a/code/addons/vitest/src/updateVitestFile.config.4.test.ts b/code/addons/vitest/src/updateVitestFile.config.4.test.ts new file mode 100644 index 000000000000..59566b1c7624 --- /dev/null +++ b/code/addons/vitest/src/updateVitestFile.config.4.test.ts @@ -0,0 +1,1797 @@ +import { join } from 'node:path'; + +import { describe, expect, it, vi } from 'vitest'; + +import * as babel from 'storybook/internal/babel'; + +import { getDiff } from '../../../core/src/core-server/utils/save-story/getDiff'; +import { loadTemplate, updateConfigFile } from './updateVitestFile'; + +vi.mock('storybook/internal/node-logger', () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, +})); + +vi.mock('../../../core/src/shared/utils/module', () => ({ + resolvePackageDir: vi.fn().mockImplementation(() => join(__dirname, '..')), +})); + +describe('updateConfigFile', () => { + it('updates vite config file with existing projects', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + // In Vitest 4, `workspace` doesn't exist — users have `projects` + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + projects: ['packages/*'] + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly — appends to existing projects array + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + + - projects: ['packages/*'] + - + + projects: ['packages/*', { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('supports object notation without defineConfig with existing projects', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + // In Vitest 4, `workspace` doesn't exist — users have `projects` + const target = babel.babelParse(` + /// + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default { + plugins: [react()], + test: { + globals: true, + projects: ['packages/*'] + }, + } + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly — appends to existing projects array + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default { + plugins: [react()], + test: { + globals: true, + + - projects: ['packages/*'] + - + + projects: ['packages/*', { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + };" + `); + }); + + it('does not support function notation', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig(() => ({ + plugins: [react()], + test: { + globals: true, + projects: ['packages/*'] + }, + })) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(false); + + const after = babel.generate(target).code; + + // check if the code was NOT updated + expect(after).toBe(before); + }); + + it('adds projects property to test config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + + - globals: true + - + + globals: true, + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('updates config which is not exported immediately', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig } from 'vite' + import viteReact from '@vitejs/plugin-react' + import { fileURLToPath, URL } from 'url' + + const config = defineConfig({ + resolve: { + preserveSymlinks: true, + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + plugins: [ + viteReact(), + ], + }) + + export default config + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig } from 'vite'; + import viteReact from '@vitejs/plugin-react'; + import { fileURLToPath, URL } from 'url'; + + + import path from 'node:path'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const config = defineConfig({ + resolve: { + preserveSymlinks: true, + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + + - plugins: [viteReact()] + - + + plugins: [viteReact()], + + test: { + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + }); + export default config;" + `); + }); + + it('edits projects property of test config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + projects: ['packages/*', {some: 'config'}] + } + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + projects: ['packages/*', { + some: 'config' + + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + + }] + } + });" + `); + }); + + it('adds projects property to test config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + + - globals: true + - + + globals: true, + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('adds test property to vite config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + + - plugins: [react()] + - + + plugins: [react()], + + test: { + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + });" + `); + }); + + it('supports mergeConfig with multiple defineConfig calls, finding the one with test', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vite' + import { defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + plugins: [react()], + }), + defineConfig({ + test: { + environment: 'jsdom', + } + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vite'; + import { defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + plugins: [react()] + }), defineConfig({ + test: { + + - environment: 'jsdom' + - + + projects: [{ + + extends: true, + + test: { + + environment: 'jsdom' + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + it('supports mergeConfig without defineConfig calls', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vite' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + { + plugins: [react()], + test: { + environment: 'jsdom', + } + } + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vite'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, { + plugins: [react()], + test: { + + - environment: 'jsdom' + - + + projects: [{ + + extends: true, + + test: { + + environment: 'jsdom' + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('supports mergeConfig without config containing test property', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vite' + import { defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + plugins: [react()], + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vite'; + import { defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + + - plugins: [react()] + - + + plugins: [react()], + + test: { + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + }));" + `); + }); + + it('supports mergeConfig with defineConfig pattern using projects (Vitest 3.2+)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + // https://vite.dev/config/ + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + globals: true, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import viteConfig from './vite.config'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - globals: true + - + + projects: [{ + + extends: true, + + test: { + + globals: true + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('appends storybook project to existing test.projects array (no double nesting)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + expect: { requireAssertions: true }, + projects: [ + { + extends: "./vite.config.ts", + test: { name: "client" }, + }, + { + extends: "./vite.config.ts", + test: { name: "server" }, + }, + ], + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly (storybook project appended to existing projects, no double nesting) + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + expect: { + requireAssertions: true + ... + test: { + name: "server" + } + + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + + }] + } + }));" + `); + }); + + it('extracts coverage config and keeps it at top level when using workspace', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + coverage: { + exclude: [ + 'storybook.setup.ts', + '**/*.stories.*', + ], + }, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + // Coverage should stay at the top level, not moved into the workspace + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'], + - + coverage: { + exclude: ['storybook.setup.ts', '**/*.stories.*'] + + - } + - + + }, + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('extracts coverage config and keeps it at top level when using projects', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + coverage: { + exclude: [ + 'storybook.setup.ts', + '**/*.stories.*', + ], + }, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + // Coverage should stay at the top level, not moved into the projects + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'], + - + coverage: { + exclude: ['storybook.setup.ts', '**/*.stories.*'] + + - } + - + + }, + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('supports defineConfig wrapping mergeConfig', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default defineConfig(mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + })) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig(mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('supports defineConfig wrapping mergeConfig with satisfies operator', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + import type { ViteUserConfig } from 'vitest/config' + + export default defineConfig( + mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) satisfies ViteUserConfig + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + import type { ViteUserConfig } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig(mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }) satisfies ViteUserConfig);" + `); + }); + + it('supports mergeConfig with as operator (TSAsExpression)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + import type { ViteUserConfig } from 'vitest/config' + + export default mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) as ViteUserConfig + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + import type { ViteUserConfig } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }) as ViteUserConfig;" + `); + }); + + it('supports mergeConfig with test defined as a constant (shorthand property)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const test = { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + } + + export default mergeConfig(viteConfig, { test }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const test = { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'] + }; + export default mergeConfig(viteConfig, { + + - test + - + + test: { + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + });" + `); + }); + + it('supports const defined config re-exported (export default config)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const config = mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) + ) + + export default config + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const config = mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + })); + export default config;" + `); + }); + + it('supports defineProject instead of defineConfig', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineProject } from 'vitest/config' + + export default defineProject({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineProject } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineProject({ + test: { + name: 'node', + environment: 'happy-dom', + + - include: ['**/*.test.ts'] + - + + include: ['**/*.test.ts'], + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('supports mergeConfig with config object as a constant variable', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const vitestConfig = { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + } + } + + export default mergeConfig(viteConfig, vitestConfig) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const vitestConfig = { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }; + export default mergeConfig(viteConfig, vitestConfig);" + `); + }); +}); diff --git a/code/addons/vitest/src/updateVitestFile.config.test.ts b/code/addons/vitest/src/updateVitestFile.config.test.ts new file mode 100644 index 000000000000..c3f9999d7752 --- /dev/null +++ b/code/addons/vitest/src/updateVitestFile.config.test.ts @@ -0,0 +1,1774 @@ +import { join } from 'node:path'; + +import { describe, expect, it, vi } from 'vitest'; + +import * as babel from 'storybook/internal/babel'; + +import { getDiff } from '../../../core/src/core-server/utils/save-story/getDiff'; +import { loadTemplate, updateConfigFile } from './updateVitestFile'; + +vi.mock('storybook/internal/node-logger', () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, +})); + +vi.mock('../../../core/src/shared/utils/module', () => ({ + resolvePackageDir: vi.fn().mockImplementation(() => join(__dirname, '..')), +})); + +describe('updateConfigFile', () => { + it('updates vite config file', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + workspace: ['packages/*'] + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + + - workspace: ['packages/*'] + - + + workspace: ['packages/*', { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('supports object notation without defineConfig', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default { + plugins: [react()], + test: { + globals: true, + workspace: ['packages/*'] + }, + } + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default { + plugins: [react()], + test: { + globals: true, + + - workspace: ['packages/*'] + - + + workspace: ['packages/*', { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + };" + `); + }); + + it('does not support function notation', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig(() => ({ + plugins: [react()], + test: { + globals: true, + workspace: ['packages/*'] + }, + })) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(false); + + const after = babel.generate(target).code; + + // check if the code was NOT updated + expect(after).toBe(before); + }); + + it('adds projects property to test config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + + - globals: true + - + + globals: true, + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('updates config which is not exported immediately', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig } from 'vite' + import viteReact from '@vitejs/plugin-react' + import { fileURLToPath, URL } from 'url' + + const config = defineConfig({ + resolve: { + preserveSymlinks: true, + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + plugins: [ + viteReact(), + ], + }) + + export default config + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig } from 'vite'; + import viteReact from '@vitejs/plugin-react'; + import { fileURLToPath, URL } from 'url'; + + + import path from 'node:path'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const config = defineConfig({ + resolve: { + preserveSymlinks: true, + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + + - plugins: [viteReact()] + - + + plugins: [viteReact()], + + test: { + + projects: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + }); + export default config;" +`); + }); + + it('edits projects property of test config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + projects: ['packages/*', {some: 'config'}] + } + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + projects: ['packages/*', { + some: 'config' + + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + + }] + } + });" + `); + }); + + it('adds workspace property to test config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + test: { + globals: true, + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + plugins: [react()], + test: { + + - globals: true + - + + globals: true, + + workspace: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('adds test property to vite config', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vite.dev/config/ + export default defineConfig({ + plugins: [react()], + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import react from '@vitejs/plugin-react'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig({ + + - plugins: [react()] + - + + plugins: [react()], + + test: { + + workspace: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + });" + `); + }); + + it('supports mergeConfig with multiple defineConfig calls, finding the one with test', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vite' + import { defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + plugins: [react()], + }), + defineConfig({ + test: { + environment: 'jsdom', + } + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vite'; + import { defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + plugins: [react()] + }), defineConfig({ + test: { + + - environment: 'jsdom' + - + + workspace: [{ + + extends: true, + + test: { + + environment: 'jsdom' + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + it('supports mergeConfig without defineConfig calls', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vite' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + { + plugins: [react()], + test: { + environment: 'jsdom', + } + } + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vite'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, { + plugins: [react()], + test: { + + - environment: 'jsdom' + - + + workspace: [{ + + extends: true, + + test: { + + environment: 'jsdom' + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('supports mergeConfig without config containing test property', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vite' + import { defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + plugins: [react()], + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vite'; + import { defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + + - plugins: [react()] + - + + plugins: [react()], + + test: { + + workspace: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + }));" + `); + }); + + it('supports mergeConfig with defineConfig pattern using projects (Vitest 3.2+)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + /// + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + // https://vite.dev/config/ + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + globals: true, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import viteConfig from './vite.config'; + + // https://vite.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - globals: true + - + + projects: [{ + + extends: true, + + test: { + + globals: true + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('appends storybook project to existing test.projects array (no double nesting)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + expect: { requireAssertions: true }, + projects: [ + { + extends: "./vite.config.ts", + test: { name: "client" }, + }, + { + extends: "./vite.config.ts", + test: { name: "server" }, + }, + ], + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly (storybook project appended to existing projects, no double nesting) + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + expect: { + requireAssertions: true + ... + test: { + name: "server" + } + + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + + }] + } + }));" + `); + }); + + it('extracts coverage config and keeps it at top level when using workspace', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + coverage: { + exclude: [ + 'storybook.setup.ts', + '**/*.stories.*', + ], + }, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + // Coverage should stay at the top level, not moved into the workspace + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'], + - + coverage: { + exclude: ['storybook.setup.ts', '**/*.stories.*'] + + - } + - + + }, + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('extracts coverage config and keeps it at top level when using projects', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + coverage: { + exclude: [ + 'storybook.setup.ts', + '**/*.stories.*', + ], + }, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + // Coverage should stay at the top level, not moved into the projects + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'], + - + coverage: { + exclude: ['storybook.setup.ts', '**/*.stories.*'] + + - } + - + + }, + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('supports defineConfig wrapping mergeConfig', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default defineConfig(mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + })) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig(mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); + + it('supports defineConfig wrapping mergeConfig with satisfies operator', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + import type { ViteUserConfig } from 'vitest/config' + + export default defineConfig( + mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) satisfies ViteUserConfig + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + import type { ViteUserConfig } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineConfig(mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }) satisfies ViteUserConfig);" + `); + }); + + it('supports mergeConfig with as operator (TSAsExpression)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + import type { ViteUserConfig } from 'vitest/config' + + export default mergeConfig(viteConfig, { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) as ViteUserConfig + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + import type { ViteUserConfig } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }) as ViteUserConfig;" + `); + }); + + it('supports mergeConfig with test defined as a constant (shorthand property)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const test = { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + } + + export default mergeConfig(viteConfig, { test }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const test = { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'] + }; + export default mergeConfig(viteConfig, { + + - test + - + + test: { + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + } + + + });" + `); + }); + + it('supports const defined config re-exported (export default config)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const config = mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) + ) + + export default config + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const config = mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + })); + export default config;" + `); + }); + + it('supports defineProject instead of defineConfig', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineProject } from 'vitest/config' + + export default defineProject({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineProject } from 'vitest/config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default defineProject({ + test: { + name: 'node', + environment: 'happy-dom', + + - include: ['**/*.test.ts'] + - + + include: ['**/*.test.ts'], + + workspace: [{ + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + });" + `); + }); + + it('supports mergeConfig with config object as a constant variable', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + const vitestConfig = { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + } + } + + export default mergeConfig(viteConfig, vitestConfig) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const vitestConfig = { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }; + export default mergeConfig(viteConfig, vitestConfig);" + `); + }); +}); diff --git a/code/addons/vitest/src/updateVitestFile.config.workspace.test.ts b/code/addons/vitest/src/updateVitestFile.config.workspace.test.ts new file mode 100644 index 000000000000..5f5784816d21 --- /dev/null +++ b/code/addons/vitest/src/updateVitestFile.config.workspace.test.ts @@ -0,0 +1,135 @@ +import { join } from 'node:path'; + +import { describe, expect, it, vi } from 'vitest'; + +import * as babel from 'storybook/internal/babel'; + +import { getDiff } from '../../../core/src/core-server/utils/save-story/getDiff'; +import { loadTemplate, updateWorkspaceFile } from './updateVitestFile'; + +vi.mock('storybook/internal/node-logger', () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, +})); + +vi.mock('../../../core/src/shared/utils/module', () => ({ + resolvePackageDir: vi.fn().mockImplementation(() => join(__dirname, '..')), +})); + +describe('updateWorkspaceFile', () => { + it('updates vitest workspace file using array syntax', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.workspace.template', { + EXTENDS_WORKSPACE: '', + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + export default ['packages/*'] + `); + + const before = babel.generate(target).code; + const updated = updateWorkspaceFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + "- export default ['packages/*']; + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineWorkspace } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + export default ['packages/*', 'ROOT_CONFIG', { + + extends: '.', + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }];" + `); + }); + + it('updates vitest workspace file using defineWorkspace syntax', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.workspace.template', { + EXTENDS_WORKSPACE: '', + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineWorkspace } from 'vitest/config' + + export default defineWorkspace(['packages/*']) + `); + + const before = babel.generate(target).code; + const updated = updateWorkspaceFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineWorkspace } from 'vitest/config'; + + - export default defineWorkspace(['packages/*']); + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + export default defineWorkspace(['packages/*', 'ROOT_CONFIG', { + + extends: '.', + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }]);" + `); + }); +}); diff --git a/code/addons/vitest/src/updateVitestFile.test.ts b/code/addons/vitest/src/updateVitestFile.test.ts index 274f432dddd0..321918e77221 100644 --- a/code/addons/vitest/src/updateVitestFile.test.ts +++ b/code/addons/vitest/src/updateVitestFile.test.ts @@ -2,10 +2,7 @@ import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import * as babel from 'storybook/internal/babel'; - -import { getDiff } from '../../../core/src/core-server/utils/save-story/getDiff'; -import { loadTemplate, updateConfigFile, updateWorkspaceFile } from './updateVitestFile'; +import { loadTemplate } from './updateVitestFile'; vi.mock('storybook/internal/node-logger', () => ({ logger: { @@ -19,1875 +16,6 @@ vi.mock('../../../core/src/shared/utils/module', () => ({ resolvePackageDir: vi.fn().mockImplementation(() => join(__dirname, '..')), })); -describe('updateConfigFile', () => { - it('updates vite config file', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - /// - import { defineConfig } from 'vite' - import react from '@vitejs/plugin-react' - - // https://vite.dev/config/ - export default defineConfig({ - plugins: [react()], - test: { - globals: true, - workspace: ['packages/*'] - }, - }) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " ... - import react from '@vitejs/plugin-react'; - - // https://vite.dev/config/ - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default defineConfig({ - plugins: [react()], - test: { - globals: true, - - - workspace: ['packages/*'] - - - + workspace: ['packages/*', { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - });" - `); - }); - - it('supports object notation without defineConfig', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - /// - import react from '@vitejs/plugin-react' - - // https://vite.dev/config/ - export default { - plugins: [react()], - test: { - globals: true, - workspace: ['packages/*'] - }, - } - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " ... - import react from '@vitejs/plugin-react'; - - // https://vite.dev/config/ - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { defineConfig } from 'vitest/config'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default { - plugins: [react()], - test: { - globals: true, - - - workspace: ['packages/*'] - - - + workspace: ['packages/*', { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - };" - `); - }); - - it('does not support function notation', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - /// - import react from '@vitejs/plugin-react' - - // https://vite.dev/config/ - export default defineConfig(() => ({ - plugins: [react()], - test: { - globals: true, - workspace: ['packages/*'] - }, - })) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(false); - - const after = babel.generate(target).code; - - // check if the code was NOT updated - expect(after).toBe(before); - }); - - it('adds projects property to test config', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.3.2.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - /// - import { defineConfig } from 'vite' - import react from '@vitejs/plugin-react' - - // https://vite.dev/config/ - export default defineConfig({ - plugins: [react()], - test: { - globals: true, - }, - }) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " ... - import react from '@vitejs/plugin-react'; - - // https://vite.dev/config/ - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default defineConfig({ - plugins: [react()], - test: { - - - globals: true - - - + globals: true, - + projects: [{ - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - });" - `); - }); - - it('updates config which is not exported immediately', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.3.2.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { defineConfig } from 'vite' - import viteReact from '@vitejs/plugin-react' - import { fileURLToPath, URL } from 'url' - - const config = defineConfig({ - resolve: { - preserveSymlinks: true, - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, - plugins: [ - viteReact(), - ], - }) - - export default config - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { defineConfig } from 'vite'; - import viteReact from '@vitejs/plugin-react'; - import { fileURLToPath, URL } from 'url'; - - + import path from 'node:path'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - const config = defineConfig({ - resolve: { - preserveSymlinks: true, - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)) - } - }, - - - plugins: [viteReact()] - - - + plugins: [viteReact()], - + test: { - + projects: [{ - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + } - + - }); - export default config;" -`); - }); - - it('edits projects property of test config', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.3.2.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - /// - import { defineConfig } from 'vite' - import react from '@vitejs/plugin-react' - - // https://vite.dev/config/ - export default defineConfig({ - plugins: [react()], - test: { - globals: true, - projects: ['packages/*', {some: 'config'}] - } - }) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " ... - import react from '@vitejs/plugin-react'; - - // https://vite.dev/config/ - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default defineConfig({ - plugins: [react()], - test: { - globals: true, - projects: ['packages/*', { - some: 'config' - - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + - }] - } - });" - `); - }); - - it('adds workspace property to test config', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - /// - import { defineConfig } from 'vite' - import react from '@vitejs/plugin-react' - - // https://vite.dev/config/ - export default defineConfig({ - plugins: [react()], - test: { - globals: true, - }, - }) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " ... - import react from '@vitejs/plugin-react'; - - // https://vite.dev/config/ - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default defineConfig({ - plugins: [react()], - test: { - - - globals: true - - - + globals: true, - + workspace: [{ - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - });" - `); - }); - - it('adds test property to vite config', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - /// - import { defineConfig } from 'vite' - import react from '@vitejs/plugin-react' - - // https://vite.dev/config/ - export default defineConfig({ - plugins: [react()], - }) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " ... - import react from '@vitejs/plugin-react'; - - // https://vite.dev/config/ - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default defineConfig({ - - - plugins: [react()] - - - + plugins: [react()], - + test: { - + workspace: [{ - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + } - + - });" - `); - }); - - it('supports mergeConfig with multiple defineConfig calls, finding the one with test', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { mergeConfig } from 'vite' - import { defineConfig } from 'vitest/config' - import viteConfig from './vite.config' - - export default mergeConfig( - viteConfig, - defineConfig({ - plugins: [react()], - }), - defineConfig({ - test: { - environment: 'jsdom', - } - }) - ) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { mergeConfig } from 'vite'; - import { defineConfig } from 'vitest/config'; - import viteConfig from './vite.config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default mergeConfig(viteConfig, defineConfig({ - plugins: [react()] - }), defineConfig({ - test: { - - - environment: 'jsdom' - - - + workspace: [{ - + extends: true, - + test: { - + environment: 'jsdom' - + } - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - }));" - `); - }); - it('supports mergeConfig without defineConfig calls', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { mergeConfig } from 'vite' - import viteConfig from './vite.config' - - export default mergeConfig( - viteConfig, - { - plugins: [react()], - test: { - environment: 'jsdom', - } - } - ) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { mergeConfig } from 'vite'; - import viteConfig from './vite.config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { defineConfig } from 'vitest/config'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default mergeConfig(viteConfig, { - plugins: [react()], - test: { - - - environment: 'jsdom' - - - + workspace: [{ - + extends: true, - + test: { - + environment: 'jsdom' - + } - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - });" - `); - }); - - it('supports mergeConfig without config containing test property', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { mergeConfig } from 'vite' - import { defineConfig } from 'vitest/config' - import viteConfig from './vite.config' - - export default mergeConfig( - viteConfig, - defineConfig({ - plugins: [react()], - }) - ) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { mergeConfig } from 'vite'; - import { defineConfig } from 'vitest/config'; - import viteConfig from './vite.config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default mergeConfig(viteConfig, defineConfig({ - - - plugins: [react()] - - - + plugins: [react()], - + test: { - + workspace: [{ - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + } - + - }));" - `); - }); - - it('supports mergeConfig with defineConfig pattern using projects (Vitest 3.2+)', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.3.2.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - /// - import { mergeConfig, defineConfig } from 'vitest/config' - import viteConfig from './vite.config' - - // https://vite.dev/config/ - export default mergeConfig( - viteConfig, - defineConfig({ - test: { - globals: true, - }, - }) - ) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " ... - import viteConfig from './vite.config'; - - // https://vite.dev/config/ - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default mergeConfig(viteConfig, defineConfig({ - test: { - - - globals: true - - - + projects: [{ - + extends: true, - + test: { - + globals: true - + } - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - }));" - `); - }); - - it('appends storybook project to existing test.projects array (no double nesting)', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.3.2.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { mergeConfig, defineConfig } from 'vitest/config' - import viteConfig from './vite.config' - - export default mergeConfig( - viteConfig, - defineConfig({ - test: { - expect: { requireAssertions: true }, - projects: [ - { - extends: "./vite.config.ts", - test: { name: "client" }, - }, - { - extends: "./vite.config.ts", - test: { name: "server" }, - }, - ], - }, - }) - ) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly (storybook project appended to existing projects, no double nesting) - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { mergeConfig, defineConfig } from 'vitest/config'; - import viteConfig from './vite.config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default mergeConfig(viteConfig, defineConfig({ - test: { - expect: { - requireAssertions: true - ... - test: { - name: "server" - } - - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + - }] - } - }));" - `); - }); - - it('extracts coverage config and keeps it at top level when using workspace', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { mergeConfig, defineConfig } from 'vitest/config' - import viteConfig from './vite.config' - - export default mergeConfig( - viteConfig, - defineConfig({ - test: { - name: 'node', - environment: 'happy-dom', - include: ['**/*.test.ts'], - coverage: { - exclude: [ - 'storybook.setup.ts', - '**/*.stories.*', - ], - }, - }, - }) - ) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - // Coverage should stay at the top level, not moved into the workspace - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { mergeConfig, defineConfig } from 'vitest/config'; - import viteConfig from './vite.config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default mergeConfig(viteConfig, defineConfig({ - test: { - - - name: 'node', - - environment: 'happy-dom', - - include: ['**/*.test.ts'], - - - coverage: { - exclude: ['storybook.setup.ts', '**/*.stories.*'] - - - } - - - + }, - + workspace: [{ - + extends: true, - + test: { - + name: 'node', - + environment: 'happy-dom', - + include: ['**/*.test.ts'] - + } - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - }));" - `); - }); - - it('extracts coverage config and keeps it at top level when using projects', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.3.2.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { mergeConfig, defineConfig } from 'vitest/config' - import viteConfig from './vite.config' - - export default mergeConfig( - viteConfig, - defineConfig({ - test: { - name: 'node', - environment: 'happy-dom', - include: ['**/*.test.ts'], - coverage: { - exclude: [ - 'storybook.setup.ts', - '**/*.stories.*', - ], - }, - }, - }) - ) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - // Coverage should stay at the top level, not moved into the projects - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { mergeConfig, defineConfig } from 'vitest/config'; - import viteConfig from './vite.config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default mergeConfig(viteConfig, defineConfig({ - test: { - - - name: 'node', - - environment: 'happy-dom', - - include: ['**/*.test.ts'], - - - coverage: { - exclude: ['storybook.setup.ts', '**/*.stories.*'] - - - } - - - + }, - + projects: [{ - + extends: true, - + test: { - + name: 'node', - + environment: 'happy-dom', - + include: ['**/*.test.ts'] - + } - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - }));" - `); - }); - - it('supports defineConfig wrapping mergeConfig', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { defineConfig, mergeConfig } from 'vitest/config' - import viteConfig from './vite.config' - - export default defineConfig(mergeConfig(viteConfig, { - test: { - name: 'node', - environment: 'happy-dom', - include: ['**/*.test.ts'], - }, - })) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - expect(after).not.toBe(before); - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { defineConfig, mergeConfig } from 'vitest/config'; - import viteConfig from './vite.config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default defineConfig(mergeConfig(viteConfig, { - test: { - - - name: 'node', - - environment: 'happy-dom', - - include: ['**/*.test.ts'] - - - + workspace: [{ - + extends: true, - + test: { - + name: 'node', - + environment: 'happy-dom', - + include: ['**/*.test.ts'] - + } - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - }));" - `); - }); - - it('supports defineConfig wrapping mergeConfig with satisfies operator', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { defineConfig, mergeConfig } from 'vitest/config' - import viteConfig from './vite.config' - import type { ViteUserConfig } from 'vitest/config' - - export default defineConfig( - mergeConfig(viteConfig, { - test: { - name: 'node', - environment: 'happy-dom', - include: ['**/*.test.ts'], - }, - }) satisfies ViteUserConfig - ) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - expect(after).not.toBe(before); - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { defineConfig, mergeConfig } from 'vitest/config'; - import viteConfig from './vite.config'; - import type { ViteUserConfig } from 'vitest/config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default defineConfig(mergeConfig(viteConfig, { - test: { - - - name: 'node', - - environment: 'happy-dom', - - include: ['**/*.test.ts'] - - - + workspace: [{ - + extends: true, - + test: { - + name: 'node', - + environment: 'happy-dom', - + include: ['**/*.test.ts'] - + } - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - }) satisfies ViteUserConfig);" - `); - }); - - it('supports mergeConfig with as operator (TSAsExpression)', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { mergeConfig } from 'vitest/config' - import viteConfig from './vite.config' - import type { ViteUserConfig } from 'vitest/config' - - export default mergeConfig(viteConfig, { - test: { - name: 'node', - environment: 'happy-dom', - include: ['**/*.test.ts'], - }, - }) as ViteUserConfig - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - expect(after).not.toBe(before); - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { mergeConfig } from 'vitest/config'; - import viteConfig from './vite.config'; - import type { ViteUserConfig } from 'vitest/config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { defineConfig } from 'vitest/config'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default mergeConfig(viteConfig, { - test: { - - - name: 'node', - - environment: 'happy-dom', - - include: ['**/*.test.ts'] - - - + workspace: [{ - + extends: true, - + test: { - + name: 'node', - + environment: 'happy-dom', - + include: ['**/*.test.ts'] - + } - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - }) as ViteUserConfig;" - `); - }); - - it('supports mergeConfig with test defined as a constant (shorthand property)', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { mergeConfig } from 'vitest/config' - import viteConfig from './vite.config' - - const test = { - name: 'node', - environment: 'happy-dom', - include: ['**/*.test.ts'], - } - - export default mergeConfig(viteConfig, { test }) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - expect(after).not.toBe(before); - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { mergeConfig } from 'vitest/config'; - import viteConfig from './vite.config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { defineConfig } from 'vitest/config'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - const test = { - name: 'node', - environment: 'happy-dom', - include: ['**/*.test.ts'] - }; - export default mergeConfig(viteConfig, { - - - test - - - + test: { - + workspace: [{ - + extends: true, - + test: { - + name: 'node', - + environment: 'happy-dom', - + include: ['**/*.test.ts'] - + } - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + } - + - });" - `); - }); - - it('supports const defined config re-exported (export default config)', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { defineConfig, mergeConfig } from 'vitest/config' - import viteConfig from './vite.config' - - const config = mergeConfig( - viteConfig, - defineConfig({ - test: { - name: 'node', - environment: 'happy-dom', - include: ['**/*.test.ts'], - }, - }) - ) - - export default config - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - expect(after).not.toBe(before); - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { defineConfig, mergeConfig } from 'vitest/config'; - import viteConfig from './vite.config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - const config = mergeConfig(viteConfig, defineConfig({ - test: { - - - name: 'node', - - environment: 'happy-dom', - - include: ['**/*.test.ts'] - - - + workspace: [{ - + extends: true, - + test: { - + name: 'node', - + environment: 'happy-dom', - + include: ['**/*.test.ts'] - + } - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - })); - export default config;" - `); - }); - - it('supports defineProject instead of defineConfig', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { defineProject } from 'vitest/config' - - export default defineProject({ - test: { - name: 'node', - environment: 'happy-dom', - include: ['**/*.test.ts'], - }, - }) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - expect(after).not.toBe(before); - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { defineProject } from 'vitest/config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { defineConfig } from 'vitest/config'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - export default defineProject({ - test: { - name: 'node', - environment: 'happy-dom', - - - include: ['**/*.test.ts'] - - - + include: ['**/*.test.ts'], - + workspace: [{ - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - });" - `); - }); - - it('supports mergeConfig with config object as a constant variable', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.config.template', { - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { mergeConfig } from 'vitest/config' - import viteConfig from './vite.config' - - const vitestConfig = { - test: { - name: 'node', - environment: 'happy-dom', - include: ['**/*.test.ts'], - } - } - - export default mergeConfig(viteConfig, vitestConfig) - `); - - const before = babel.generate(target).code; - const updated = updateConfigFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - expect(after).not.toBe(before); - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { mergeConfig } from 'vitest/config'; - import viteConfig from './vite.config'; - - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { defineConfig } from 'vitest/config'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + - const vitestConfig = { - test: { - - - name: 'node', - - environment: 'happy-dom', - - include: ['**/*.test.ts'] - - - + workspace: [{ - + extends: true, - + test: { - + name: 'node', - + environment: 'happy-dom', - + include: ['**/*.test.ts'] - + } - + }, { - + extends: true, - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }] - + - } - }; - export default mergeConfig(viteConfig, vitestConfig);" - `); - }); -}); - -describe('updateWorkspaceFile', () => { - it('updates vitest workspace file using array syntax', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.workspace.template', { - EXTENDS_WORKSPACE: '', - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - export default ['packages/*'] - `); - - const before = babel.generate(target).code; - const updated = updateWorkspaceFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - "- export default ['packages/*']; - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { defineWorkspace } from 'vitest/config'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + export default ['packages/*', 'ROOT_CONFIG', { - + extends: '.', - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }];" - `); - }); - - it('updates vitest workspace file using defineWorkspace syntax', async () => { - const source = babel.babelParse( - await loadTemplate('vitest.workspace.template', { - EXTENDS_WORKSPACE: '', - CONFIG_DIR: '.storybook', - BROWSER_CONFIG: "{ provider: 'playwright' }", - SETUP_FILE: '../.storybook/vitest.setup.ts', - }) - ); - const target = babel.babelParse(` - import { defineWorkspace } from 'vitest/config' - - export default defineWorkspace(['packages/*']) - `); - - const before = babel.generate(target).code; - const updated = updateWorkspaceFile(source, target); - expect(updated).toBe(true); - - const after = babel.generate(target).code; - - // check if the code was updated at all - expect(after).not.toBe(before); - - // check if the code was updated correctly - expect(getDiff(before, after)).toMatchInlineSnapshot(` - " import { defineWorkspace } from 'vitest/config'; - - - export default defineWorkspace(['packages/*']); - + import path from 'node:path'; - + import { fileURLToPath } from 'node:url'; - + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - + - + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon - + export default defineWorkspace(['packages/*', 'ROOT_CONFIG', { - + extends: '.', - + plugins: [ - + // The plugin will run tests for the stories defined in your Storybook config - + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - + storybookTest({ - + configDir: path.join(dirname, '.storybook') - + })], - + test: { - + name: 'storybook', - + browser: { - + enabled: true, - + headless: true, - + provider: 'playwright', - + instances: [{ - + browser: 'chromium' - + }] - + } - + } - + }]);" - `); - }); -}); - describe('loadTemplate', () => { it('normalizes Windows paths to forward slashes', async () => { // Windows-style path with backslashes (need to escape them in JS strings) From d8b7f7c23111f48c911b4f03fd2b0f222da439a6 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Wed, 11 Mar 2026 23:29:43 +0100 Subject: [PATCH 07/14] Extend top_level_test_properties --- .../src/updateVitestFile.config.3.2.test.ts | 106 +++++++++++++++++ .../src/updateVitestFile.config.4.test.ts | 107 ++++++++++++++++++ .../src/updateVitestFile.config.test.ts | 106 +++++++++++++++++ code/addons/vitest/src/updateVitestFile.ts | 53 +++++++-- 4 files changed, 362 insertions(+), 10 deletions(-) diff --git a/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts b/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts index b629341520af..e386fa508564 100644 --- a/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts +++ b/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts @@ -1774,4 +1774,110 @@ describe('updateConfigFile', () => { export default mergeConfig(viteConfig, vitestConfig);" `); }); + + it('keeps coverage at top level instead of moving into projects', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.3.2.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'unit', + environment: 'happy-dom', + include: ['**/*.test.ts'], + env: { CI: 'true' }, + pool: 'forks', + maxWorkers: 4, + coverage: { + provider: 'v8', + exclude: ['**/*.stories.*'], + }, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'unit', + - environment: 'happy-dom', + - include: ['**/*.test.ts'], + - env: { + - CI: 'true' + - }, + - pool: 'forks', + - maxWorkers: 4, + - + coverage: { + provider: 'v8', + exclude: ['**/*.stories.*'] + + - } + - + + }, + + projects: [{ + + extends: true, + + test: { + + name: 'unit', + + environment: 'happy-dom', + + include: ['**/*.test.ts'], + + env: { + + CI: 'true' + + }, + + pool: 'forks', + + maxWorkers: 4 + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); }); diff --git a/code/addons/vitest/src/updateVitestFile.config.4.test.ts b/code/addons/vitest/src/updateVitestFile.config.4.test.ts index 59566b1c7624..3946098bae26 100644 --- a/code/addons/vitest/src/updateVitestFile.config.4.test.ts +++ b/code/addons/vitest/src/updateVitestFile.config.4.test.ts @@ -1794,4 +1794,111 @@ describe('updateConfigFile', () => { export default mergeConfig(viteConfig, vitestConfig);" `); }); + + it('keeps coverage at top level instead of moving into projects', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'unit', + environment: 'happy-dom', + include: ['**/*.test.ts'], + env: { CI: 'true' }, + pool: 'forks', + maxWorkers: 4, + coverage: { + provider: 'v8', + exclude: ['**/*.stories.*'], + }, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'unit', + - environment: 'happy-dom', + - include: ['**/*.test.ts'], + - env: { + - CI: 'true' + - }, + - pool: 'forks', + - maxWorkers: 4, + - + coverage: { + provider: 'v8', + exclude: ['**/*.stories.*'] + + - } + - + + }, + + projects: [{ + + extends: true, + + test: { + + name: 'unit', + + environment: 'happy-dom', + + include: ['**/*.test.ts'], + + env: { + + CI: 'true' + + }, + + pool: 'forks', + + maxWorkers: 4 + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); }); diff --git a/code/addons/vitest/src/updateVitestFile.config.test.ts b/code/addons/vitest/src/updateVitestFile.config.test.ts index c3f9999d7752..b71cc3f084c9 100644 --- a/code/addons/vitest/src/updateVitestFile.config.test.ts +++ b/code/addons/vitest/src/updateVitestFile.config.test.ts @@ -1771,4 +1771,110 @@ describe('updateConfigFile', () => { export default mergeConfig(viteConfig, vitestConfig);" `); }); + + it('keeps coverage at top level instead of moving into workspace', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig, defineConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export default mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'unit', + environment: 'happy-dom', + include: ['**/*.test.ts'], + env: { CI: 'true' }, + pool: 'forks', + maxWorkers: 4, + coverage: { + provider: 'v8', + exclude: ['**/*.stories.*'], + }, + }, + }) + ) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig, defineConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export default mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'unit', + - environment: 'happy-dom', + - include: ['**/*.test.ts'], + - env: { + - CI: 'true' + - }, + - pool: 'forks', + - maxWorkers: 4, + - + coverage: { + provider: 'v8', + exclude: ['**/*.stories.*'] + + - } + - + + }, + + workspace: [{ + + extends: true, + + test: { + + name: 'unit', + + environment: 'happy-dom', + + include: ['**/*.test.ts'], + + env: { + + CI: 'true' + + }, + + pool: 'forks', + + maxWorkers: 4 + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }));" + `); + }); }); diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts index 11663331a5a2..187d52ff2a83 100644 --- a/code/addons/vitest/src/updateVitestFile.ts +++ b/code/addons/vitest/src/updateVitestFile.ts @@ -160,7 +160,8 @@ const appendToExistingProjects = ( /** * Wraps the existing test config as one project entry inside the template's workspace/projects - * array, extracting coverage to the top-level test object. + * array, hoisting shared properties (coverage, env, pool, maxWorkers) to the top-level test + * object. */ const wrapTestConfigAsProject = ( resolvedTestValue: t.ObjectExpression, @@ -184,11 +185,43 @@ const wrapTestConfigAsProject = ( return; } - // Extract coverage config before creating the test project - const coverageProp = findNamedProp(resolvedTestValue.properties, 'coverage'); - const testPropsWithoutCoverage = resolvedTestValue.properties.filter((p) => p !== coverageProp); - - // Create the existing test project: { extends: true, test: { ...existingTestProps } } + // Properties that should stay at the top-level test object (shared across all projects) + const TOP_LEVEL_TEST_PROPERTIES = [ + 'shard', + 'watch', + 'run', + 'cache', + 'update', + 'reporters', + 'outputFile', + 'teardownTimeout', + 'silent', + 'forceRerunTriggers', + 'testNamePattern', + 'ui', + 'open', + 'uiBase', + 'snapshotFormat', + 'resolveSnapshotPath', + 'passWithNoTests', + 'onConsoleLog', + 'onStackTrace', + 'dangerouslyIgnoreUnhandledErrors', + 'slowTestThreshold', + 'inspect', + 'inspectBrk', + 'coverage', + 'watchTriggerPatterns', + ]; + + const topLevelProps = TOP_LEVEL_TEST_PROPERTIES.map((name) => + findNamedProp(resolvedTestValue.properties, name) + ).filter(Boolean); + + const topLevelPropSet = new Set(topLevelProps); + const projectTestProps = resolvedTestValue.properties.filter((p) => !topLevelPropSet.has(p)); + + // Create the existing test project: { extends: true, test: { ...projectTestProps } } const existingTestProject: t.ObjectExpression = { type: 'ObjectExpression', properties: [ @@ -204,7 +237,7 @@ const wrapTestConfigAsProject = ( key: { type: 'Identifier', name: 'test' } as t.Identifier, value: { type: 'ObjectExpression', - properties: testPropsWithoutCoverage, + properties: projectTestProps, } as t.ObjectExpression, computed: false, shorthand: false, @@ -220,9 +253,9 @@ const wrapTestConfigAsProject = ( (p) => p !== existingTestProp ); - // Hoist coverage to the top-level test object so it applies to all projects - if (coverageProp && templateTestProp.value.type === 'ObjectExpression') { - templateTestProp.value.properties.unshift(coverageProp); + // Hoist top-level properties to the test object so they apply to all projects + if (topLevelProps.length > 0 && templateTestProp.value.type === 'ObjectExpression') { + templateTestProp.value.properties.unshift(...topLevelProps); } mergeProperties(properties, targetConfigObject.properties); From 00a02968a058c537fa22f472c09245b99c5abb76 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 12 Mar 2026 09:06:35 +0100 Subject: [PATCH 08/14] Resolve code rabbit comments --- .../src/updateVitestFile.config.4.test.ts | 168 ++++++++++++++++++ .../src/updateVitestFile.config.test.ts | 166 +++++++++++++++++ code/addons/vitest/src/updateVitestFile.ts | 6 +- .../src/babel/expression-resolver.test.ts | 150 ++++++++++++++++ code/core/src/babel/expression-resolver.ts | 24 ++- 5 files changed, 508 insertions(+), 6 deletions(-) create mode 100644 code/core/src/babel/expression-resolver.test.ts diff --git a/code/addons/vitest/src/updateVitestFile.config.4.test.ts b/code/addons/vitest/src/updateVitestFile.config.4.test.ts index 3946098bae26..8a8eb5a56dd1 100644 --- a/code/addons/vitest/src/updateVitestFile.config.4.test.ts +++ b/code/addons/vitest/src/updateVitestFile.config.4.test.ts @@ -1712,6 +1712,174 @@ describe('updateConfigFile', () => { `); }); + it('supports export const config re-exported as default (ExportNamedDeclaration)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export const config = mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) + ) + + export default config + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export const config = mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + })); + export default config;" + `); + }); + + it('supports mergeConfig with config object as an exported constant (ExportNamedDeclaration)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export const vitestConfig = { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + } + } + + export default mergeConfig(viteConfig, vitestConfig) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + import { playwright } from '@vitest/browser-playwright'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export const vitestConfig = { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + projects: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }; + export default mergeConfig(viteConfig, vitestConfig);" + `); + }); + it('supports mergeConfig with config object as a constant variable', async () => { const source = babel.babelParse( await loadTemplate('vitest.config.4.template', { diff --git a/code/addons/vitest/src/updateVitestFile.config.test.ts b/code/addons/vitest/src/updateVitestFile.config.test.ts index b71cc3f084c9..01b877868630 100644 --- a/code/addons/vitest/src/updateVitestFile.config.test.ts +++ b/code/addons/vitest/src/updateVitestFile.config.test.ts @@ -1690,6 +1690,172 @@ describe('updateConfigFile', () => { `); }); + it('supports export const config re-exported as default (ExportNamedDeclaration)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { defineConfig, mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export const config = mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + }, + }) + ) + + export default config + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { defineConfig, mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export const config = mergeConfig(viteConfig, defineConfig({ + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + })); + export default config;" + `); + }); + + it('supports mergeConfig with config object as an exported constant (ExportNamedDeclaration)', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import { mergeConfig } from 'vitest/config' + import viteConfig from './vite.config' + + export const vitestConfig = { + test: { + name: 'node', + environment: 'happy-dom', + include: ['**/*.test.ts'], + } + } + + export default mergeConfig(viteConfig, vitestConfig) + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " import { mergeConfig } from 'vitest/config'; + import viteConfig from './vite.config'; + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { defineConfig } from 'vitest/config'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + export const vitestConfig = { + test: { + + - name: 'node', + - environment: 'happy-dom', + - include: ['**/*.test.ts'] + - + + workspace: [{ + + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }; + export default mergeConfig(viteConfig, vitestConfig);" + `); + }); + it('supports mergeConfig with config object as a constant variable', async () => { const source = babel.babelParse( await loadTemplate('vitest.config.template', { diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts index 187d52ff2a83..d63ce88c3811 100644 --- a/code/addons/vitest/src/updateVitestFile.ts +++ b/code/addons/vitest/src/updateVitestFile.ts @@ -216,10 +216,12 @@ const wrapTestConfigAsProject = ( const topLevelProps = TOP_LEVEL_TEST_PROPERTIES.map((name) => findNamedProp(resolvedTestValue.properties, name) - ).filter(Boolean); + ).filter(Boolean) as t.ObjectProperty[]; const topLevelPropSet = new Set(topLevelProps); - const projectTestProps = resolvedTestValue.properties.filter((p) => !topLevelPropSet.has(p)); + const projectTestProps = resolvedTestValue.properties.filter( + (p) => !topLevelPropSet.has(p as any) + ); // Create the existing test project: { extends: true, test: { ...projectTestProps } } const existingTestProject: t.ObjectExpression = { diff --git a/code/core/src/babel/expression-resolver.test.ts b/code/core/src/babel/expression-resolver.test.ts new file mode 100644 index 000000000000..e1d5c93b681f --- /dev/null +++ b/code/core/src/babel/expression-resolver.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from 'vitest'; + +import * as parser from '@babel/parser'; + +import { resolveExpression, unwrapTSExpression } from './expression-resolver'; + +const parse = (code: string) => + parser.parse(code, { + sourceType: 'module', + plugins: ['typescript'], + }); + +describe('unwrapTSExpression', () => { + it('returns non-TS-wrapped expressions unchanged', () => { + const ast = parse('42'); + const numLiteral = (ast.program.body[0] as any).expression; + expect(unwrapTSExpression(numLiteral)).toBe(numLiteral); + }); + + it('unwraps TSAsExpression', () => { + const ast = parse('foo as string'); + const asExpr = (ast.program.body[0] as any).expression; + const result = unwrapTSExpression(asExpr); + expect(result.type).toBe('Identifier'); + expect((result as any).name).toBe('foo'); + }); + + it('unwraps TSSatisfiesExpression', () => { + const ast = parse('foo satisfies string'); + const satisfiesExpr = (ast.program.body[0] as any).expression; + const result = unwrapTSExpression(satisfiesExpr); + expect(result.type).toBe('Identifier'); + expect((result as any).name).toBe('foo'); + }); + + it('unwraps TSTypeAssertion (foo)', () => { + const ast = parser.parse('foo', { + sourceType: 'module', + plugins: [['typescript', { dts: false }]], + }); + const typeAssertion = (ast.program.body[0] as any).expression; + const result = unwrapTSExpression(typeAssertion); + expect(result.type).toBe('Identifier'); + expect((result as any).name).toBe('foo'); + }); + + it('unwraps nested TS wrappers', () => { + const ast = parse('(foo as any) satisfies string'); + const outer = (ast.program.body[0] as any).expression; + const result = unwrapTSExpression(outer); + expect(result.type).toBe('Identifier'); + expect((result as any).name).toBe('foo'); + }); +}); + +describe('resolveExpression', () => { + it('returns null for null/undefined input', () => { + const ast = parse(''); + expect(resolveExpression(null, ast)).toBeNull(); + expect(resolveExpression(undefined, ast)).toBeNull(); + }); + + it('returns non-Identifier expressions directly', () => { + const ast = parse('42'); + const numLiteral = (ast.program.body[0] as any).expression; + expect(resolveExpression(numLiteral, ast)).toBe(numLiteral); + }); + + it('resolves a bare VariableDeclaration', () => { + const ast = parse(` + const foo = { a: 1 }; + export default foo; + `); + const defaultExport = ast.program.body[1] as any; + const result = resolveExpression(defaultExport.declaration, ast); + expect(result?.type).toBe('ObjectExpression'); + }); + + it('resolves an exported const (ExportNamedDeclaration)', () => { + const ast = parse(` + export const config = { a: 1 }; + export default config; + `); + const defaultExport = ast.program.body[1] as any; + const result = resolveExpression(defaultExport.declaration, ast); + expect(result?.type).toBe('ObjectExpression'); + }); + + it('resolves a chain of variable references', () => { + const ast = parse(` + const inner = { a: 1 }; + const outer = inner; + export default outer; + `); + const defaultExport = ast.program.body[2] as any; + const result = resolveExpression(defaultExport.declaration, ast); + expect(result?.type).toBe('ObjectExpression'); + }); + + it('resolves through TSAsExpression', () => { + const ast = parse(` + const foo = { a: 1 }; + export default foo as any; + `); + const defaultExport = ast.program.body[1] as any; + const result = resolveExpression(defaultExport.declaration, ast); + expect(result?.type).toBe('ObjectExpression'); + }); + + it('resolves through TSSatisfiesExpression', () => { + const ast = parse(` + const foo = { a: 1 }; + export default foo satisfies object; + `); + const defaultExport = ast.program.body[1] as any; + const result = resolveExpression(defaultExport.declaration, ast); + expect(result?.type).toBe('ObjectExpression'); + }); + + it('returns the Identifier node when variable is not found', () => { + const ast = parse(`export default unknown;`); + const defaultExport = ast.program.body[0] as any; + const result = resolveExpression(defaultExport.declaration, ast); + expect(result?.type).toBe('Identifier'); + expect((result as any).name).toBe('unknown'); + }); + + it('returns the Identifier node when variable has no initializer', () => { + const ast = parse(` + let foo; + export default foo; + `); + const defaultExport = ast.program.body[1] as any; + const result = resolveExpression(defaultExport.declaration, ast); + expect(result?.type).toBe('Identifier'); + expect((result as any).name).toBe('foo'); + }); + + it('returns null when maxDepth is exceeded', () => { + // Create a chain longer than maxDepth (default 10) + const lines = Array.from({ length: 12 }, (_, i) => + i === 0 ? `const v0 = { a: 1 };` : `const v${i} = v${i - 1};` + ).join('\n'); + const ast = parse(`${lines}\nexport default v11;`); + const defaultExport = ast.program.body[ast.program.body.length - 1] as any; + const result = resolveExpression(defaultExport.declaration, ast); + // Depth exceeded — returns null + expect(result).toBeNull(); + }); +}); diff --git a/code/core/src/babel/expression-resolver.ts b/code/core/src/babel/expression-resolver.ts index 7ecbf312e80b..3b2ab1d4ca80 100644 --- a/code/core/src/babel/expression-resolver.ts +++ b/code/core/src/babel/expression-resolver.ts @@ -33,10 +33,26 @@ export const resolveExpression = ( return unwrapped; } const varName = (unwrapped as t.Identifier).name; - const declarator = ast.program.body - .filter((n): n is t.VariableDeclaration => n.type === 'VariableDeclaration') - .flatMap((varDecl) => varDecl.declarations) - .find((d) => d.id.type === 'Identifier' && (d.id as t.Identifier).name === varName); + let declarator: t.VariableDeclarator | undefined; + for (const node of ast.program.body) { + let declarations: t.VariableDeclarator[] | undefined; + if (node.type === 'VariableDeclaration') { + declarations = node.declarations; + } else if ( + node.type === 'ExportNamedDeclaration' && + node.declaration?.type === 'VariableDeclaration' + ) { + declarations = node.declaration.declarations; + } + if (declarations) { + declarator = declarations.find( + (d) => d.id.type === 'Identifier' && (d.id as t.Identifier).name === varName + ); + if (declarator) { + break; + } + } + } if (!declarator?.init) { return unwrapped; } From bc75a647d474f86abf47ce50cd38f2b0628523f2 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 12 Mar 2026 10:02:40 +0100 Subject: [PATCH 09/14] Support proper project/workspace inlining for default exports --- .../src/updateVitestFile.config.3.2.test.ts | 24 +++- .../src/updateVitestFile.config.4.test.ts | 24 +++- .../src/updateVitestFile.config.test.ts | 24 +++- code/addons/vitest/src/updateVitestFile.ts | 117 +++++++++++------- 4 files changed, 130 insertions(+), 59 deletions(-) diff --git a/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts b/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts index e386fa508564..477c8c0ddcba 100644 --- a/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts +++ b/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts @@ -264,9 +264,13 @@ describe('updateConfigFile', () => { - globals: true - - + globals: true, + projects: [{ + extends: true, + + test: { + + globals: true + + } + + }, { + + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest @@ -505,9 +509,13 @@ describe('updateConfigFile', () => { - globals: true - - + globals: true, + projects: [{ + extends: true, + + test: { + + globals: true + + } + + }, { + + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest @@ -1661,14 +1669,20 @@ describe('updateConfigFile', () => { + export default defineProject({ test: { - name: 'node', - environment: 'happy-dom', + - name: 'node', + - environment: 'happy-dom', - include: ['**/*.test.ts'] - - + include: ['**/*.test.ts'], + projects: [{ + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest diff --git a/code/addons/vitest/src/updateVitestFile.config.4.test.ts b/code/addons/vitest/src/updateVitestFile.config.4.test.ts index 8a8eb5a56dd1..b8c7f2e10761 100644 --- a/code/addons/vitest/src/updateVitestFile.config.4.test.ts +++ b/code/addons/vitest/src/updateVitestFile.config.4.test.ts @@ -266,9 +266,13 @@ describe('updateConfigFile', () => { - globals: true - - + globals: true, + projects: [{ + extends: true, + + test: { + + globals: true + + } + + }, { + + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest @@ -510,9 +514,13 @@ describe('updateConfigFile', () => { - globals: true - - + globals: true, + projects: [{ + extends: true, + + test: { + + globals: true + + } + + }, { + + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest @@ -1680,14 +1688,20 @@ describe('updateConfigFile', () => { + export default defineProject({ test: { - name: 'node', - environment: 'happy-dom', + - name: 'node', + - environment: 'happy-dom', - include: ['**/*.test.ts'] - - + include: ['**/*.test.ts'], + projects: [{ + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest diff --git a/code/addons/vitest/src/updateVitestFile.config.test.ts b/code/addons/vitest/src/updateVitestFile.config.test.ts index 01b877868630..adfb71f7e876 100644 --- a/code/addons/vitest/src/updateVitestFile.config.test.ts +++ b/code/addons/vitest/src/updateVitestFile.config.test.ts @@ -261,9 +261,13 @@ describe('updateConfigFile', () => { - globals: true - - + globals: true, + projects: [{ + extends: true, + + test: { + + globals: true + + } + + }, { + + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest @@ -502,9 +506,13 @@ describe('updateConfigFile', () => { - globals: true - - + globals: true, + workspace: [{ + extends: true, + + test: { + + globals: true + + } + + }, { + + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest @@ -1658,14 +1666,20 @@ describe('updateConfigFile', () => { + export default defineProject({ test: { - name: 'node', - environment: 'happy-dom', + - name: 'node', + - environment: 'happy-dom', - include: ['**/*.test.ts'] - - + include: ['**/*.test.ts'], + workspace: [{ + extends: true, + + test: { + + name: 'node', + + environment: 'happy-dom', + + include: ['**/*.test.ts'] + + } + + }, { + + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts index d63ce88c3811..bea8eda5fd84 100644 --- a/code/addons/vitest/src/updateVitestFile.ts +++ b/code/addons/vitest/src/updateVitestFile.ts @@ -100,32 +100,40 @@ const findNamedProp = ( p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === name ); -/** Type guard for a property that is a `projects` key with an ArrayExpression value. */ -const isProjectsArrayProp = ( +/** Type guard for a property that is a `workspace` or `projects` key with an array value. */ +const isWorkspaceOrProjectsArrayProp = ( p: t.ObjectMethod | t.ObjectProperty | t.SpreadElement ): p is t.ObjectProperty => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && - p.key.name === 'projects' && + (p.key.name === 'workspace' || p.key.name === 'projects') && p.value.type === 'ArrayExpression'; /** - * Appends storybook project(s) from template into an existing `test.projects` array, then merges - * any additional test-level options (e.g. coverage) that don't already exist. + * Appends storybook project(s) from template into an existing `test.workspace`/`test.projects` + * array, then merges any additional test-level options (e.g. coverage) that don't already exist. */ -const appendToExistingProjects = ( - existingProjectsProp: t.ObjectProperty, +const appendToExistingProjectRefs = ( + existingProjectRefsProp: t.ObjectProperty, resolvedTestValue: t.ObjectExpression, templateTestProp: t.ObjectProperty | undefined, properties: t.ObjectExpression['properties'], targetConfigObject: t.ObjectExpression ) => { + const existingKeyName = + existingProjectRefsProp.key.type === 'Identifier' ? existingProjectRefsProp.key.name : null; + if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { - // Append template projects to existing projects array - const templateProjectsProp = templateTestProp.value.properties.find(isProjectsArrayProp); - if (templateProjectsProp && templateProjectsProp.value.type === 'ArrayExpression') { - (existingProjectsProp.value as t.ArrayExpression).elements.push( - ...(templateProjectsProp.value as t.ArrayExpression).elements + // Append template workspace/projects entries to existing workspace/projects array + const templateProjectRefsProp = templateTestProp.value.properties.find( + (p): p is t.ObjectProperty => + isWorkspaceOrProjectsArrayProp(p) && + (existingKeyName === null || + (p.key.type === 'Identifier' && p.key.name === existingKeyName)) + ); + if (templateProjectRefsProp && templateProjectRefsProp.value.type === 'ArrayExpression') { + (existingProjectRefsProp.value as t.ArrayExpression).elements.push( + ...(templateProjectRefsProp.value as t.ArrayExpression).elements ); } @@ -142,6 +150,7 @@ const appendToExistingProjects = ( templateProp.type === 'ObjectProperty' && templateProp.key.type === 'Identifier' && (templateProp.key as t.Identifier).name !== 'projects' && + (templateProp.key as t.Identifier).name !== 'workspace' && !existingTestPropNames.has((templateProp.key as t.Identifier).name) ) { resolvedTestValue.properties.push(templateProp); @@ -263,6 +272,56 @@ const wrapTestConfigAsProject = ( mergeProperties(properties, targetConfigObject.properties); }; +/** + * Merges template properties into a config object, handling Vitest `test.projects` migration + * semantics: + * + * - Append when projects already exists + * - Wrap existing test config as a project when template introduces projects/workspace + * - Otherwise perform a regular merge + */ +const mergeTemplateIntoConfigObject = ( + targetConfigObject: t.ObjectExpression, + properties: t.ObjectExpression['properties'], + target: BabelFile['ast'] +) => { + const existingTestProp = findNamedProp(targetConfigObject.properties, 'test'); + const resolvedTestValue = existingTestProp + ? resolveTestPropValue(existingTestProp, target) + : null; + const templateTestProp = findNamedProp(properties, 'test'); + + if (existingTestProp && resolvedTestValue !== null) { + const existingProjectRefsProp = resolvedTestValue.properties.find( + isWorkspaceOrProjectsArrayProp + ); + + if (existingProjectRefsProp) { + appendToExistingProjectRefs( + existingProjectRefsProp, + resolvedTestValue, + templateTestProp, + properties, + targetConfigObject + ); + return; + } + + if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { + wrapTestConfigAsProject( + resolvedTestValue, + existingTestProp, + templateTestProp, + properties, + targetConfigObject + ); + return; + } + } + + mergeProperties(properties, targetConfigObject.properties); +}; + /** * Extracts the effective mergeConfig call from a declaration, handling wrappers: * @@ -436,7 +495,7 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as const { properties } = sourceNode.declaration.arguments[0]; const targetConfigObject = getTargetConfigObject(target, exportDefault); if (targetConfigObject !== null) { - mergeProperties(properties, targetConfigObject.properties); + mergeTemplateIntoConfigObject(targetConfigObject, properties, target); updated = true; } else { const mergeConfigCall = getEffectiveMergeConfigCall(exportDefault.declaration, target); @@ -475,37 +534,7 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as return false; } - const existingTestProp = findNamedProp(targetConfigObject.properties, 'test'); - const resolvedTestValue = existingTestProp - ? resolveTestPropValue(existingTestProp, target) - : null; - const templateTestProp = findNamedProp(properties, 'test'); - - if (existingTestProp && resolvedTestValue !== null) { - const existingProjectsProp = resolvedTestValue.properties.find(isProjectsArrayProp); - - if (existingProjectsProp) { - appendToExistingProjects( - existingProjectsProp, - resolvedTestValue, - templateTestProp, - properties, - targetConfigObject - ); - } else if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { - wrapTestConfigAsProject( - resolvedTestValue, - existingTestProp, - templateTestProp, - properties, - targetConfigObject - ); - } else { - mergeProperties(properties, targetConfigObject.properties); - } - } else { - mergeProperties(properties, targetConfigObject.properties); - } + mergeTemplateIntoConfigObject(targetConfigObject, properties, target); updated = true; } } From 3b1fd51bf2b61550522e59485bed5f70d5c31d91 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 12 Mar 2026 12:01:36 +0100 Subject: [PATCH 10/14] Enhance config handling with support for mergeConfig and aliased defineConfig --- .../src/updateVitestFile.config.4.test.ts | 130 ++++++++++++++++++ code/addons/vitest/src/updateVitestFile.ts | 74 +++++++--- 2 files changed, 187 insertions(+), 17 deletions(-) diff --git a/code/addons/vitest/src/updateVitestFile.config.4.test.ts b/code/addons/vitest/src/updateVitestFile.config.4.test.ts index b8c7f2e10761..4cf0e1f576f4 100644 --- a/code/addons/vitest/src/updateVitestFile.config.4.test.ts +++ b/code/addons/vitest/src/updateVitestFile.config.4.test.ts @@ -1977,6 +1977,136 @@ describe('updateConfigFile', () => { `); }); + it('supports mergeConfig with aliased defineConfig and updates the config that contains test', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { + CONFIG_DIR: '.storybook', + BROWSER_CONFIG: "{ provider: 'playwright' }", + SETUP_FILE: '../.storybook/vitest.setup.ts', + }) + ); + const target = babel.babelParse(` + import react from "@vitejs/plugin-react"; + import { playwright } from "@vitest/browser-playwright"; + import { defineConfig, mergeConfig } from "vite"; + import tsconfigPaths from "vite-tsconfig-paths"; + import { defineConfig as defineVitestConfig } from "vitest/config"; + + // https://vitejs.dev/config/ + const viteConfig = defineConfig({ + plugins: [tsconfigPaths(), react()], + optimizeDeps: { + exclude: ["@xmtp/wasm-bindings"], + }, + server: { + allowedHosts: true, + }, + build: { + sourcemap: true, + }, + }); + + const vitestConfig = defineVitestConfig({ + test: { + browser: { + provider: playwright(), + enabled: true, + headless: true, + screenshotFailures: false, + instances: [ + { + browser: "chromium", + }, + ], + }, + testTimeout: 120000, + }, + }); + + export default mergeConfig(viteConfig, vitestConfig); + `); + + const before = babel.generate(target).code; + const updated = updateConfigFile(source, target); + expect(updated).toBe(true); + + const after = babel.generate(target).code; + expect(after).not.toBe(before); + + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + import { defineConfig as defineVitestConfig } from "vitest/config"; + + // https://vitejs.dev/config/ + + + import path from 'node:path'; + + import { fileURLToPath } from 'node:url'; + + import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; + + const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + + + + // More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon + + + const viteConfig = defineConfig({ + plugins: [tsconfigPaths(), react()], + optimizeDeps: { + exclude: ["@xmtp/wasm-bindings"] + ... + }); + const vitestConfig = defineVitestConfig({ + test: { + + - browser: { + - provider: playwright(), + - enabled: true, + - headless: true, + - screenshotFailures: false, + - instances: [{ + - browser: "chromium" + - }] + - }, + - testTimeout: 120000 + - + + projects: [{ + + extends: true, + + test: { + + browser: { + + provider: playwright(), + + enabled: true, + + headless: true, + + screenshotFailures: false, + + instances: [{ + + browser: "chromium" + + }] + + }, + + testTimeout: 120000 + + } + + }, { + + extends: true, + + plugins: [ + + // The plugin will run tests for the stories defined in your Storybook config + + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + + storybookTest({ + + configDir: path.join(dirname, '.storybook') + + })], + + test: { + + name: 'storybook', + + browser: { + + enabled: true, + + headless: true, + + provider: playwright({}), + + instances: [{ + + browser: 'chromium' + + }] + + } + + } + + }] + + + } + }); + export default mergeConfig(viteConfig, vitestConfig);" + `); + }); + it('keeps coverage at top level instead of moving into projects', async () => { const source = babel.babelParse( await loadTemplate('vitest.config.4.template', { diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts index bea8eda5fd84..8aa792b52a1e 100644 --- a/code/addons/vitest/src/updateVitestFile.ts +++ b/code/addons/vitest/src/updateVitestFile.ts @@ -69,10 +69,56 @@ const mergeProperties = ( } }; -/** Returns true if the call expression is a defineConfig or defineProject call. */ -const isDefineConfigLike = (node: t.CallExpression): boolean => +/** + * Returns true if the identifier is a local alias for `defineConfig`/`defineProject` imported from + * either `vitest/config` or `vite`. + */ +const isImportedDefineConfigLikeIdentifier = (localName: string, ast: BabelFile['ast']): boolean => + ast.program.body.some( + (node): boolean => + node.type === 'ImportDeclaration' && + (node.source.value === 'vitest/config' || node.source.value === 'vite') && + node.specifiers.some( + (specifier) => + specifier.type === 'ImportSpecifier' && + specifier.local.type === 'Identifier' && + specifier.local.name === localName && + specifier.imported.type === 'Identifier' && + (specifier.imported.name === 'defineConfig' || + specifier.imported.name === 'defineProject') + ) + ); + +/** Returns true if the call expression is a defineConfig or defineProject call (including aliases). */ +const isDefineConfigLike = (node: t.CallExpression, ast: BabelFile['ast']): boolean => node.callee.type === 'Identifier' && - (node.callee.name === 'defineConfig' || node.callee.name === 'defineProject'); + (node.callee.name === 'defineConfig' || + node.callee.name === 'defineProject' || + isImportedDefineConfigLikeIdentifier(node.callee.name, ast)); + +/** + * Resolves a mergeConfig argument to a config object expression when possible. Supports both direct + * object args and wrapped forms like `defineConfig({ ... })`. + */ +const getConfigObjectFromMergeArg = ( + arg: t.Expression, + ast: BabelFile['ast'] +): t.ObjectExpression | null => { + const resolved = resolveExpression(arg, ast); + if (!resolved) { + return null; + } + + if (resolved.type === 'ObjectExpression') { + return resolved; + } + + if (resolved.type === 'CallExpression' && resolved.arguments[0]?.type === 'ObjectExpression') { + return resolved.arguments[0] as t.ObjectExpression; + } + + return null; +}; /** * Resolves the value of a `test` ObjectProperty to an ObjectExpression. Handles both inline objects @@ -339,7 +385,7 @@ const getEffectiveMergeConfigCall = ( } // Handle defineConfig(mergeConfig(...)) – arg may itself be wrapped in a TS type expression - if (isDefineConfigLike(resolved) && resolved.arguments.length > 0) { + if (isDefineConfigLike(resolved, ast) && resolved.arguments.length > 0) { const innerArg = resolveExpression(resolved.arguments[0] as t.Expression, ast); if ( innerArg?.type === 'CallExpression' && @@ -377,7 +423,7 @@ const getTargetConfigObject = ( } if ( resolved.type === 'CallExpression' && - isDefineConfigLike(resolved) && + isDefineConfigLike(resolved, target) && resolved.arguments[0]?.type === 'ObjectExpression' ) { return resolved.arguments[0] as t.ObjectExpression; @@ -433,7 +479,7 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as const effectiveDecl = resolveExpression(targetExportDefault.declaration, target); if ( effectiveDecl?.type === 'CallExpression' && - isDefineConfigLike(effectiveDecl) && + isDefineConfigLike(effectiveDecl, target) && effectiveDecl.arguments.length > 0 && effectiveDecl.arguments[0].type === 'ArrowFunctionExpression' ) { @@ -501,24 +547,18 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as const mergeConfigCall = getEffectiveMergeConfigCall(exportDefault.declaration, target); if (mergeConfigCall && mergeConfigCall.arguments.length >= 2) { // Collect all potential config object nodes from mergeConfig arguments. - // Each argument may be: defineConfig/defineProject({...}), a plain object {…}, + // Each argument may be a plain object, a wrapper call with object argument, // an Identifier (variable reference), or wrapped in a TS type annotation. const configObjectNodes: t.ObjectExpression[] = []; for (const arg of mergeConfigCall.arguments) { - const resolved = resolveExpression(arg as t.Expression, target); - if (resolved?.type === 'ObjectExpression') { - configObjectNodes.push(resolved); - } else if ( - resolved?.type === 'CallExpression' && - isDefineConfigLike(resolved) && - resolved.arguments[0]?.type === 'ObjectExpression' - ) { - configObjectNodes.push(resolved.arguments[0] as t.ObjectExpression); + const configObject = getConfigObjectFromMergeArg(arg as t.Expression, target); + if (configObject) { + configObjectNodes.push(configObject); } } - // Prefer a config object that already contains a `test` property + // Prefer the config object that already has an immediate `test` property. const configObjectWithTest = configObjectNodes.find((obj) => obj.properties.some( (p) => From d83eb16009843ab7230420bcedecf7a984dd1f09 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 16 Mar 2026 22:52:37 +0100 Subject: [PATCH 11/14] Vite: Add mock entries to optimizeDeps.entries --- .../storybook-optimize-deps-plugin.test.ts | 34 ++++++++++++++++++- .../plugins/storybook-optimize-deps-plugin.ts | 32 +++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts index f42d94a5fe12..b9f1062943ba 100644 --- a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts +++ b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { escapeGlobPath } from './storybook-optimize-deps-plugin'; +import { escapeGlobPath, getMockRedirectIncludeEntries } from './storybook-optimize-deps-plugin'; describe('escapeGlobPath', () => { it('should not modify a plain path without special characters', () => { @@ -42,3 +42,35 @@ describe('escapeGlobPath', () => { ); }); }); + +describe('getMockRedirectIncludeEntries', () => { + it('should include only manual mock redirect paths', () => { + expect( + getMockRedirectIncludeEntries([ + { redirectPath: '/project/src/lib/__mocks__/db.ts' }, + { redirectPath: null }, + ]) + ).toEqual(['/project/src/lib/__mocks__/db.ts']); + }); + + it('should escape special glob characters in redirect paths', () => { + expect( + getMockRedirectIncludeEntries([ + { redirectPath: '/project/src/(group)/__mocks__/db.ts' }, + { redirectPath: '/project/src/[id]/__mocks__/db.ts' }, + ]) + ).toEqual([ + '/project/src/\\(group\\)/__mocks__/db.ts', + '/project/src/\\[id\\]/__mocks__/db.ts', + ]); + }); + + it('should dedupe redirect paths', () => { + expect( + getMockRedirectIncludeEntries([ + { redirectPath: '/project/src/lib/__mocks__/db.ts' }, + { redirectPath: '/project/src/lib/__mocks__/db.ts' }, + ]) + ).toEqual(['/project/src/lib/__mocks__/db.ts']); + }); +}); diff --git a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts index e75ab293dff1..cf3e2e8cc3f0 100644 --- a/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts +++ b/code/builders/builder-vite/src/plugins/storybook-optimize-deps-plugin.ts @@ -1,5 +1,6 @@ import { loadPreviewOrConfigFile } from 'storybook/internal/common'; import type { StoryIndexGenerator } from 'storybook/internal/core-server'; +import { babelParser, extractMockCalls, findMockRedirect } from 'storybook/internal/mocking-utils'; import type { Options, PreviewAnnotation, StoryIndex } from 'storybook/internal/types'; import { resolve } from 'pathe'; @@ -18,6 +19,20 @@ export function escapeGlobPath(filePath: string): string { return filePath.replace(/[()[\]{}!*?|+@]/g, '\\$&'); } +/** Converts extracted sb.mock calls into optimizeDeps include entries for manual **mocks** files. */ +export function getMockRedirectIncludeEntries( + mockCalls: Array<{ redirectPath: string | null }> +): string[] { + return Array.from( + new Set( + mockCalls + .map((mockCall) => mockCall.redirectPath) + .filter((redirectPath): redirectPath is string => redirectPath !== null) + .map(escapeGlobPath) + ) + ); +} + /** A Vite plugin that configures dependency optimization for Storybook's dev server. */ export function storybookOptimizeDepsPlugin(options: Options): Plugin { return { @@ -41,6 +56,22 @@ export function storybookOptimizeDepsPlugin(options: Options): Plugin { // Include the user's preview file and all addon/framework/renderer preview annotations // as optimizer entries so Vite can discover all transitive CJS dependencies automatically. const previewOrConfigFile = loadPreviewOrConfigFile({ configDir: options.configDir }); + + const mockRedirectIncludeEntries = previewOrConfigFile + ? getMockRedirectIncludeEntries( + extractMockCalls( + { + previewConfigPath: previewOrConfigFile, + coreOptions: { disableTelemetry: true }, + configDir: options.configDir, + }, + babelParser, + projectRoot, + findMockRedirect + ) + ) + : []; + const previewAnnotationEntries = [...previewAnnotations, previewOrConfigFile] .filter((path): path is PreviewAnnotation => path !== undefined) .map((path) => processPreviewAnnotation(path, projectRoot)); @@ -56,6 +87,7 @@ export function storybookOptimizeDepsPlugin(options: Options): Plugin { ...(typeof config.optimizeDeps?.entries === 'string' ? [config.optimizeDeps.entries] : (config.optimizeDeps?.entries ?? [])), + ...mockRedirectIncludeEntries, ...getUniqueImportPaths(index).map(escapeGlobPath), ...previewAnnotationEntries.map(escapeGlobPath), ], From 9a263d4c38b6eceb65abd0f93092c7268c0420bc Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 17 Mar 2026 14:23:05 +0100 Subject: [PATCH 12/14] Update test runner configuration to use a single worker for builds --- scripts/tasks/test-runner-build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tasks/test-runner-build.ts b/scripts/tasks/test-runner-build.ts index 9d3e070c52d0..85e07ac5db75 100644 --- a/scripts/tasks/test-runner-build.ts +++ b/scripts/tasks/test-runner-build.ts @@ -23,7 +23,7 @@ export const testRunnerBuild: Task & { port: number } = { const flags = [ `--url http://localhost:${port}`, '--junit', - '--maxWorkers=2', + '--maxWorkers=1', '--failOnConsole', '--index-json', ]; From 21e7cb3d7267ffd8d919f2a4e4de06f282179f70 Mon Sep 17 00:00:00 2001 From: storybook-bot <32066757+storybook-bot@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:06:49 +0000 Subject: [PATCH 13/14] Write changelog for 10.3.0-beta.2 [skip ci] --- CHANGELOG.prerelease.md | 8 ++++++++ code/package.json | 3 ++- docs/versions/next.json | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 9f2bf2961789..448764798116 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,11 @@ +## 10.3.0-beta.2 + +- UI: Hide addon panel Drag on pages without a panel - [#34162](https://github.com/storybookjs/storybook/pull/34162), thanks @Sidnioulz! +- UI: Hide manifest tag for now - [#34165](https://github.com/storybookjs/storybook/pull/34165), thanks @Sidnioulz! +- UI: Make disabled Buttons keyboard-focusable - [#34166](https://github.com/storybookjs/storybook/pull/34166), thanks @Sidnioulz! +- UI: Use correct selector for addon panel focus check - [#34164](https://github.com/storybookjs/storybook/pull/34164), thanks @Sidnioulz! +- Vue: Make globals reactive in decorators - [#34116](https://github.com/storybookjs/storybook/pull/34116), thanks @Sidnioulz! + ## 10.3.0-beta.1 - Addon-Docs: Add React as optimizeDeps entry - [#34176](https://github.com/storybookjs/storybook/pull/34176), thanks @valentinpalkovic! diff --git a/code/package.json b/code/package.json index 01f43cf9050d..cccfff08f4b9 100644 --- a/code/package.json +++ b/code/package.json @@ -214,5 +214,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "10.3.0-beta.2" } diff --git a/docs/versions/next.json b/docs/versions/next.json index dca93cb3ae84..482f457d8234 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"10.3.0-beta.1","info":{"plain":"- Addon-Docs: Add React as optimizeDeps entry - [#34176](https://github.com/storybookjs/storybook/pull/34176), thanks @valentinpalkovic!\n- CLI: Avoid hanging of postinstall during init - [#34175](https://github.com/storybookjs/storybook/pull/34175), thanks @valentinpalkovic!"}} \ No newline at end of file +{"version":"10.3.0-beta.2","info":{"plain":"- UI: Hide addon panel Drag on pages without a panel - [#34162](https://github.com/storybookjs/storybook/pull/34162), thanks @Sidnioulz!\n- UI: Hide manifest tag for now - [#34165](https://github.com/storybookjs/storybook/pull/34165), thanks @Sidnioulz!\n- UI: Make disabled Buttons keyboard-focusable - [#34166](https://github.com/storybookjs/storybook/pull/34166), thanks @Sidnioulz!\n- UI: Use correct selector for addon panel focus check - [#34164](https://github.com/storybookjs/storybook/pull/34164), thanks @Sidnioulz!\n- Vue: Make globals reactive in decorators - [#34116](https://github.com/storybookjs/storybook/pull/34116), thanks @Sidnioulz!"}} \ No newline at end of file From af5b7de899701eb55e511197dbb0420850156125 Mon Sep 17 00:00:00 2001 From: storybook-bot <32066757+storybook-bot@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:50:29 +0000 Subject: [PATCH 14/14] Bump version from "10.3.0-beta.1" to "10.3.0-beta.2" [skip ci] --- code/addons/a11y/package.json | 2 +- code/addons/docs/package.json | 2 +- code/addons/links/package.json | 2 +- code/addons/onboarding/package.json | 2 +- code/addons/pseudo-states/package.json | 2 +- code/addons/themes/package.json | 2 +- code/addons/vitest/package.json | 2 +- code/builders/builder-vite/package.json | 2 +- code/builders/builder-webpack5/package.json | 2 +- code/core/package.json | 2 +- code/core/src/common/versions.ts | 84 +++++++++---------- code/core/src/manager-api/version.ts | 2 +- code/frameworks/angular/package.json | 2 +- code/frameworks/ember/package.json | 2 +- code/frameworks/html-vite/package.json | 2 +- code/frameworks/nextjs-vite/package.json | 2 +- code/frameworks/nextjs/package.json | 2 +- code/frameworks/preact-vite/package.json | 2 +- .../react-native-web-vite/package.json | 2 +- code/frameworks/react-vite/package.json | 2 +- code/frameworks/react-webpack5/package.json | 2 +- code/frameworks/server-webpack5/package.json | 2 +- code/frameworks/svelte-vite/package.json | 2 +- code/frameworks/sveltekit/package.json | 2 +- code/frameworks/vue3-vite/package.json | 2 +- .../web-components-vite/package.json | 2 +- code/lib/cli-sb/package.json | 2 +- code/lib/cli-storybook/package.json | 2 +- code/lib/codemod/package.json | 2 +- code/lib/core-webpack/package.json | 2 +- code/lib/create-storybook/package.json | 2 +- code/lib/csf-plugin/package.json | 2 +- code/lib/eslint-plugin/package.json | 2 +- code/lib/react-dom-shim/package.json | 2 +- code/package.json | 5 +- code/presets/create-react-app/package.json | 2 +- code/presets/react-webpack/package.json | 2 +- code/presets/server-webpack/package.json | 2 +- code/renderers/html/package.json | 2 +- code/renderers/preact/package.json | 2 +- code/renderers/react/package.json | 2 +- code/renderers/server/package.json | 2 +- code/renderers/svelte/package.json | 2 +- code/renderers/vue3/package.json | 2 +- code/renderers/web-components/package.json | 2 +- 45 files changed, 87 insertions(+), 88 deletions(-) diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json index 2902a1040c72..3cea3b5a714b 100644 --- a/code/addons/a11y/package.json +++ b/code/addons/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-a11y", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Addon A11y: Test UI component compliance with WCAG web accessibility standards", "keywords": [ "a11y", diff --git a/code/addons/docs/package.json b/code/addons/docs/package.json index b681f538b9e3..56198211a649 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-docs", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Docs: Document UI components automatically with stories and MDX", "keywords": [ "docs", diff --git a/code/addons/links/package.json b/code/addons/links/package.json index 26dc0f479d73..0738d131c489 100644 --- a/code/addons/links/package.json +++ b/code/addons/links/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-links", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Links: Link stories together to build demos and prototypes with your UI components", "keywords": [ "storybook", diff --git a/code/addons/onboarding/package.json b/code/addons/onboarding/package.json index a13c19b15e33..94c970b3aa32 100644 --- a/code/addons/onboarding/package.json +++ b/code/addons/onboarding/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-onboarding", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Onboarding: Help new users learn how to write stories", "keywords": [ "storybook", diff --git a/code/addons/pseudo-states/package.json b/code/addons/pseudo-states/package.json index f8686701f659..e88d8ca9c77e 100644 --- a/code/addons/pseudo-states/package.json +++ b/code/addons/pseudo-states/package.json @@ -1,6 +1,6 @@ { "name": "storybook-addon-pseudo-states", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Pseudo-states addon: Manipulate CSS pseudo states", "keywords": [ "storybook", diff --git a/code/addons/themes/package.json b/code/addons/themes/package.json index 55aa7bead9bf..b70b09365b04 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-themes", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Themes addon: Switch between themes from the toolbar", "keywords": [ "css", diff --git a/code/addons/vitest/package.json b/code/addons/vitest/package.json index 8da5d09d5ac7..4f7c20148b00 100644 --- a/code/addons/vitest/package.json +++ b/code/addons/vitest/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-vitest", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Vitest addon: Blazing fast component testing using stories", "keywords": [ "storybook", diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json index 46e7121bc2b4..72fe5453d036 100644 --- a/code/builders/builder-vite/package.json +++ b/code/builders/builder-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-vite", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "A Storybook builder to dev and build with Vite", "keywords": [ "storybook", diff --git a/code/builders/builder-webpack5/package.json b/code/builders/builder-webpack5/package.json index 9dd1e2a94830..f5aa238d3694 100644 --- a/code/builders/builder-webpack5/package.json +++ b/code/builders/builder-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/builder-webpack5", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "A Storybook builder to dev and build with Webpack", "keywords": [ "storybook", diff --git a/code/core/package.json b/code/core/package.json index 2956c06590ca..e9124f9a7b0d 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/core/src/common/versions.ts b/code/core/src/common/versions.ts index 9b89d803329b..45bc692645fb 100644 --- a/code/core/src/common/versions.ts +++ b/code/core/src/common/versions.ts @@ -1,45 +1,45 @@ // auto generated file, do not edit export default { - '@storybook/addon-a11y': '10.3.0-beta.1', - '@storybook/addon-docs': '10.3.0-beta.1', - '@storybook/addon-links': '10.3.0-beta.1', - '@storybook/addon-onboarding': '10.3.0-beta.1', - 'storybook-addon-pseudo-states': '10.3.0-beta.1', - '@storybook/addon-themes': '10.3.0-beta.1', - '@storybook/addon-vitest': '10.3.0-beta.1', - '@storybook/builder-vite': '10.3.0-beta.1', - '@storybook/builder-webpack5': '10.3.0-beta.1', - storybook: '10.3.0-beta.1', - '@storybook/angular': '10.3.0-beta.1', - '@storybook/ember': '10.3.0-beta.1', - '@storybook/html-vite': '10.3.0-beta.1', - '@storybook/nextjs': '10.3.0-beta.1', - '@storybook/nextjs-vite': '10.3.0-beta.1', - '@storybook/preact-vite': '10.3.0-beta.1', - '@storybook/react-native-web-vite': '10.3.0-beta.1', - '@storybook/react-vite': '10.3.0-beta.1', - '@storybook/react-webpack5': '10.3.0-beta.1', - '@storybook/server-webpack5': '10.3.0-beta.1', - '@storybook/svelte-vite': '10.3.0-beta.1', - '@storybook/sveltekit': '10.3.0-beta.1', - '@storybook/vue3-vite': '10.3.0-beta.1', - '@storybook/web-components-vite': '10.3.0-beta.1', - sb: '10.3.0-beta.1', - '@storybook/cli': '10.3.0-beta.1', - '@storybook/codemod': '10.3.0-beta.1', - '@storybook/core-webpack': '10.3.0-beta.1', - 'create-storybook': '10.3.0-beta.1', - '@storybook/csf-plugin': '10.3.0-beta.1', - 'eslint-plugin-storybook': '10.3.0-beta.1', - '@storybook/react-dom-shim': '10.3.0-beta.1', - '@storybook/preset-create-react-app': '10.3.0-beta.1', - '@storybook/preset-react-webpack': '10.3.0-beta.1', - '@storybook/preset-server-webpack': '10.3.0-beta.1', - '@storybook/html': '10.3.0-beta.1', - '@storybook/preact': '10.3.0-beta.1', - '@storybook/react': '10.3.0-beta.1', - '@storybook/server': '10.3.0-beta.1', - '@storybook/svelte': '10.3.0-beta.1', - '@storybook/vue3': '10.3.0-beta.1', - '@storybook/web-components': '10.3.0-beta.1', + '@storybook/addon-a11y': '10.3.0-beta.2', + '@storybook/addon-docs': '10.3.0-beta.2', + '@storybook/addon-links': '10.3.0-beta.2', + '@storybook/addon-onboarding': '10.3.0-beta.2', + 'storybook-addon-pseudo-states': '10.3.0-beta.2', + '@storybook/addon-themes': '10.3.0-beta.2', + '@storybook/addon-vitest': '10.3.0-beta.2', + '@storybook/builder-vite': '10.3.0-beta.2', + '@storybook/builder-webpack5': '10.3.0-beta.2', + storybook: '10.3.0-beta.2', + '@storybook/angular': '10.3.0-beta.2', + '@storybook/ember': '10.3.0-beta.2', + '@storybook/html-vite': '10.3.0-beta.2', + '@storybook/nextjs': '10.3.0-beta.2', + '@storybook/nextjs-vite': '10.3.0-beta.2', + '@storybook/preact-vite': '10.3.0-beta.2', + '@storybook/react-native-web-vite': '10.3.0-beta.2', + '@storybook/react-vite': '10.3.0-beta.2', + '@storybook/react-webpack5': '10.3.0-beta.2', + '@storybook/server-webpack5': '10.3.0-beta.2', + '@storybook/svelte-vite': '10.3.0-beta.2', + '@storybook/sveltekit': '10.3.0-beta.2', + '@storybook/vue3-vite': '10.3.0-beta.2', + '@storybook/web-components-vite': '10.3.0-beta.2', + sb: '10.3.0-beta.2', + '@storybook/cli': '10.3.0-beta.2', + '@storybook/codemod': '10.3.0-beta.2', + '@storybook/core-webpack': '10.3.0-beta.2', + 'create-storybook': '10.3.0-beta.2', + '@storybook/csf-plugin': '10.3.0-beta.2', + 'eslint-plugin-storybook': '10.3.0-beta.2', + '@storybook/react-dom-shim': '10.3.0-beta.2', + '@storybook/preset-create-react-app': '10.3.0-beta.2', + '@storybook/preset-react-webpack': '10.3.0-beta.2', + '@storybook/preset-server-webpack': '10.3.0-beta.2', + '@storybook/html': '10.3.0-beta.2', + '@storybook/preact': '10.3.0-beta.2', + '@storybook/react': '10.3.0-beta.2', + '@storybook/server': '10.3.0-beta.2', + '@storybook/svelte': '10.3.0-beta.2', + '@storybook/vue3': '10.3.0-beta.2', + '@storybook/web-components': '10.3.0-beta.2', }; diff --git a/code/core/src/manager-api/version.ts b/code/core/src/manager-api/version.ts index a98b928e9511..66f11a256def 100644 --- a/code/core/src/manager-api/version.ts +++ b/code/core/src/manager-api/version.ts @@ -1 +1 @@ -export const version = '10.3.0-beta.1'; +export const version = '10.3.0-beta.2'; diff --git a/code/frameworks/angular/package.json b/code/frameworks/angular/package.json index b328d0404069..122541a8fce3 100644 --- a/code/frameworks/angular/package.json +++ b/code/frameworks/angular/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/angular", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for Angular: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/frameworks/ember/package.json b/code/frameworks/ember/package.json index 3dccd218d5bb..f555c1796679 100644 --- a/code/frameworks/ember/package.json +++ b/code/frameworks/ember/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/ember", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for Ember: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/frameworks/html-vite/package.json b/code/frameworks/html-vite/package.json index 5d50fa1913cd..4dbfcb3a728f 100644 --- a/code/frameworks/html-vite/package.json +++ b/code/frameworks/html-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html-vite", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for HTML and Vite: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/frameworks/nextjs-vite/package.json b/code/frameworks/nextjs-vite/package.json index 9dc52eaf04e6..3cfa847a20f7 100644 --- a/code/frameworks/nextjs-vite/package.json +++ b/code/frameworks/nextjs-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/nextjs-vite", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for Next.js and Vite: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index 71315ad41a36..3fa3d4814091 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/nextjs", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for Next.js: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/frameworks/preact-vite/package.json b/code/frameworks/preact-vite/package.json index 0f9793e904d7..d4228939268b 100644 --- a/code/frameworks/preact-vite/package.json +++ b/code/frameworks/preact-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact-vite", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for Preact and Vite: Develop, document, and test UI components 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 e9b7818c2add..3733ae36dbec 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": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for React Native Web and Vite: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json index 69fa10365445..74c5f8a7f1b2 100644 --- a/code/frameworks/react-vite/package.json +++ b/code/frameworks/react-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-vite", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for React and Vite: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/frameworks/react-webpack5/package.json b/code/frameworks/react-webpack5/package.json index 155333dc1c34..22741dfb860a 100644 --- a/code/frameworks/react-webpack5/package.json +++ b/code/frameworks/react-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-webpack5", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for React and Webpack: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/frameworks/server-webpack5/package.json b/code/frameworks/server-webpack5/package.json index 004421483b70..48565ad717da 100644 --- a/code/frameworks/server-webpack5/package.json +++ b/code/frameworks/server-webpack5/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/server-webpack5", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "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 1de92680eab3..164e37c042c1 100644 --- a/code/frameworks/svelte-vite/package.json +++ b/code/frameworks/svelte-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/svelte-vite", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for Svelte and Vite: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index 2f76e258ce9c..58c7f81bd1a4 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/sveltekit", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for SvelteKit: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/frameworks/vue3-vite/package.json b/code/frameworks/vue3-vite/package.json index 53e51c1c993c..2f0f62dc6f49 100644 --- a/code/frameworks/vue3-vite/package.json +++ b/code/frameworks/vue3-vite/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue3-vite", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for Vue3 and Vite: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/frameworks/web-components-vite/package.json b/code/frameworks/web-components-vite/package.json index c1da46f0ecf4..b032ea34d703 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": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for Web Components and Vite: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/lib/cli-sb/package.json b/code/lib/cli-sb/package.json index 013c03eaa7bc..f20e21705a55 100644 --- a/code/lib/cli-sb/package.json +++ b/code/lib/cli-sb/package.json @@ -1,6 +1,6 @@ { "name": "sb", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook CLI: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/lib/cli-storybook/package.json b/code/lib/cli-storybook/package.json index ec1e4bacec89..5c626a19e05e 100644 --- a/code/lib/cli-storybook/package.json +++ b/code/lib/cli-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/cli", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook CLI: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index 70efb8ac4dbb..2096c61c3867 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/codemod", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "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 d3d901646f76..3133c2fe4fb5 100644 --- a/code/lib/core-webpack/package.json +++ b/code/lib/core-webpack/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/core-webpack", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook framework-agnostic API", "keywords": [ "storybook" diff --git a/code/lib/create-storybook/package.json b/code/lib/create-storybook/package.json index 9c4438f74adf..baee343d9bac 100644 --- a/code/lib/create-storybook/package.json +++ b/code/lib/create-storybook/package.json @@ -1,6 +1,6 @@ { "name": "create-storybook", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook installer: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/lib/csf-plugin/package.json b/code/lib/csf-plugin/package.json index bede0792fce9..9274198e3359 100644 --- a/code/lib/csf-plugin/package.json +++ b/code/lib/csf-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/csf-plugin", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Enrich CSF files via static analysis", "keywords": [ "storybook" diff --git a/code/lib/eslint-plugin/package.json b/code/lib/eslint-plugin/package.json index acec622cd26f..3a6bf8573f81 100644 --- a/code/lib/eslint-plugin/package.json +++ b/code/lib/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-storybook", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook ESLint Plugin: Best practice rules for writing stories", "keywords": [ "eslint", diff --git a/code/lib/react-dom-shim/package.json b/code/lib/react-dom-shim/package.json index f485171f4967..fc555fb3347a 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": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "", "keywords": [ "storybook" diff --git a/code/package.json b/code/package.json index cccfff08f4b9..5dd7b74d414e 100644 --- a/code/package.json +++ b/code/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/code", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "private": true, "description": "Storybook root", "homepage": "https://storybook.js.org/", @@ -214,6 +214,5 @@ "Dependency Upgrades" ] ] - }, - "deferredNextVersion": "10.3.0-beta.2" + } } diff --git a/code/presets/create-react-app/package.json b/code/presets/create-react-app/package.json index fd61d9120d3f..336299aaf27b 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": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for Create React App preset", "keywords": [ "storybook" diff --git a/code/presets/react-webpack/package.json b/code/presets/react-webpack/package.json index 1048b0b4e2b6..c5b43cd46ddc 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": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "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 333b72f18eba..e48ddfd13e55 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": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook for Server: View HTML snippets from a server in isolation with Hot Reloading.", "keywords": [ "storybook" diff --git a/code/renderers/html/package.json b/code/renderers/html/package.json index 2ca19ef9b144..104105cd5b61 100644 --- a/code/renderers/html/package.json +++ b/code/renderers/html/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/html", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook HTML renderer: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/renderers/preact/package.json b/code/renderers/preact/package.json index 53bdf7a41b1c..2f2fbf5e1f85 100644 --- a/code/renderers/preact/package.json +++ b/code/renderers/preact/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/preact", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Preact renderer: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json index 549506d071db..77feb03ccbd0 100644 --- a/code/renderers/react/package.json +++ b/code/renderers/react/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook React renderer", "keywords": [ "storybook" diff --git a/code/renderers/server/package.json b/code/renderers/server/package.json index 671a36f145f2..45d5400b2b5a 100644 --- a/code/renderers/server/package.json +++ b/code/renderers/server/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/server", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Server renderer: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/renderers/svelte/package.json b/code/renderers/svelte/package.json index 92ffa38e6ccd..24ad267acaac 100644 --- a/code/renderers/svelte/package.json +++ b/code/renderers/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/svelte", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Svelte renderer: Develop, document, and test UI components in isolation.", "keywords": [ "storybook", diff --git a/code/renderers/vue3/package.json b/code/renderers/vue3/package.json index e132d50f633b..718623906d34 100644 --- a/code/renderers/vue3/package.json +++ b/code/renderers/vue3/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/vue3", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Vue 3 renderer: Develop, document, and test UI components in isolation", "keywords": [ "storybook", diff --git a/code/renderers/web-components/package.json b/code/renderers/web-components/package.json index 6bacf574ff7b..c694ba209e85 100644 --- a/code/renderers/web-components/package.json +++ b/code/renderers/web-components/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/web-components", - "version": "10.3.0-beta.1", + "version": "10.3.0-beta.2", "description": "Storybook Web Components renderer: Develop, document, and test UI components in isolation", "keywords": [ "storybook",