From 4ef6304458270b5c2d4995cbfad6037e7fe45f11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:42:15 +0000 Subject: [PATCH 1/3] Fix: Don't add triple slash reference to vitest.config files Only add `/// ` to vite.config files, not vitest.config files, because vitest.config files already have the vitest/config types available by default. Co-authored-by: yannbf <1671563+yannbf@users.noreply.github.com> --- code/addons/vitest/src/postinstall.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/code/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts index 82bff176da2c..6bea9c1a0322 100644 --- a/code/addons/vitest/src/postinstall.ts +++ b/code/addons/vitest/src/postinstall.ts @@ -468,11 +468,14 @@ export default async function postInstall(options: PostinstallOptions) { logger.plain(` ${rootConfig}`); const formattedContent = await formatFileContent(rootConfig, generate(target).code); + // Only add triple slash reference to vite.config files, not vitest.config files + // vitest.config files already have the vitest/config types available + const shouldAddReference = !configFileHasTypeReference && !vitestConfigFile; await writeFile( rootConfig, - configFileHasTypeReference - ? formattedContent - : '/// \n' + formattedContent + shouldAddReference + ? '/// \n' + formattedContent + : formattedContent ); } else { logErrors( From 7ad957f2dba9e57ef5a6c6be7ab9c4e2a61b8126 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:26:04 +0000 Subject: [PATCH 2/3] Fix vitest addon to extract coverage config to top-level test object When transforming vitest configs with coverage settings, the addon now correctly: - Extracts the coverage property from the existing test config - Keeps it at the top-level test object (where it's global) - Moves other test properties to workspace/projects array items - Adds test cases to verify the fix works for both workspace and projects modes Co-authored-by: yannbf <1671563+yannbf@users.noreply.github.com> --- .../vitest/src/updateVitestFile.test.ts | 188 ++++++++++++++++++ code/addons/vitest/src/updateVitestFile.ts | 26 ++- 2 files changed, 213 insertions(+), 1 deletion(-) diff --git a/code/addons/vitest/src/updateVitestFile.test.ts b/code/addons/vitest/src/updateVitestFile.test.ts index a244b23f02ee..04bd87cd375d 100644 --- a/code/addons/vitest/src/updateVitestFile.test.ts +++ b/code/addons/vitest/src/updateVitestFile.test.ts @@ -771,6 +771,194 @@ describe('updateConfigFile', () => { }));" `); }); + + it('extracts coverage config and keeps it at top level when using workspace', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template.ts', { + 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: { + + provider: 'playwright' + + }, + + setupFiles: ['../.storybook/vitest.setup.ts'] + + } + + }] + + + } + }));" + `); + }); + + 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.ts', { + 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: { + + provider: 'playwright' + + }, + + setupFiles: ['../.storybook/vitest.setup.ts'] + + } + + }] + + + } + }));" + `); + }); }); describe('updateWorkspaceFile', () => { diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts index 04af8916ac28..1dd2cd1a404b 100644 --- a/code/addons/vitest/src/updateVitestFile.ts +++ b/code/addons/vitest/src/updateVitestFile.ts @@ -249,6 +249,24 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as workspaceOrProjectsProp && workspaceOrProjectsProp.value.type === 'ArrayExpression' ) { + // Extract coverage config before creating the test project + const coverageProp = existingTestProp.value.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 = existingTestProp.value.properties.filter( + (p) => p !== coverageProp + ); + + const testConfigForProject: t.ObjectExpression = { + type: 'ObjectExpression', + properties: testPropsWithoutCoverage, + }; + // Create the existing test project const existingTestProject: t.ObjectExpression = { type: 'ObjectExpression', @@ -263,7 +281,7 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as { type: 'ObjectProperty', key: { type: 'Identifier', name: 'test' }, - value: existingTestProp.value, + value: testConfigForProject, computed: false, shorthand: false, }, @@ -278,6 +296,12 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as (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, defineConfigProps.properties); } else { From 0384713710fd1914c57d29bbd49a23fa5395ee99 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Mon, 27 Oct 2025 13:32:06 +0100 Subject: [PATCH 3/3] support non defineConfig calls in mergeConfig --- .../vitest/src/updateVitestFile.test.ts | 96 ++++++++++++++++++- code/addons/vitest/src/updateVitestFile.ts | 64 ++++++------- 2 files changed, 124 insertions(+), 36 deletions(-) diff --git a/code/addons/vitest/src/updateVitestFile.test.ts b/code/addons/vitest/src/updateVitestFile.test.ts index 04bd87cd375d..6e08aa611eef 100644 --- a/code/addons/vitest/src/updateVitestFile.test.ts +++ b/code/addons/vitest/src/updateVitestFile.test.ts @@ -611,6 +611,88 @@ describe('updateConfigFile', () => { }));" `); }); + it('supports mergeConfig without defineConfig calls', async () => { + const source = babel.babelParse( + await loadTemplate('vitest.config.template.ts', { + 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' + + }] + + }, + + setupFiles: ['../.storybook/vitest.setup.ts'] + + } + + }] + + + } + });" + `); + }); it('supports mergeConfig without config containing test property', async () => { const source = babel.babelParse( @@ -855,7 +937,12 @@ describe('updateConfigFile', () => { + test: { + name: 'storybook', + browser: { - + provider: 'playwright' + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + }, + setupFiles: ['../.storybook/vitest.setup.ts'] + } @@ -949,7 +1036,12 @@ describe('updateConfigFile', () => { + test: { + name: 'storybook', + browser: { - + provider: 'playwright' + + enabled: true, + + headless: true, + + provider: 'playwright', + + instances: [{ + + browser: 'chromium' + + }] + }, + setupFiles: ['../.storybook/vitest.setup.ts'] + } diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts index 1dd2cd1a404b..0f544dde3482 100644 --- a/code/addons/vitest/src/updateVitestFile.ts +++ b/code/addons/vitest/src/updateVitestFile.ts @@ -123,14 +123,7 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as targetExportDefault.declaration.callee.name === 'mergeConfig' && targetExportDefault.declaration.arguments.length >= 2 ) { - const defineConfigNodes = targetExportDefault.declaration.arguments.filter( - (arg): arg is t.CallExpression => - arg?.type === 'CallExpression' && - arg.callee.type === 'Identifier' && - arg.callee.name === 'defineConfig' && - arg.arguments[0]?.type === 'ObjectExpression' - ); - canHandleConfig = defineConfigNodes.length > 0; + canHandleConfig = true; } if (!canHandleConfig) { @@ -194,37 +187,40 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as exportDefault.declaration.callee.name === 'mergeConfig' && exportDefault.declaration.arguments.length >= 2 ) { - const defineConfigNodes = exportDefault.declaration.arguments.filter( - (arg): arg is t.CallExpression => + // 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); + } + } - const defineConfigNodeWithTest = defineConfigNodes.find( - (node) => - node.arguments[0].type === 'ObjectExpression' && - node.arguments[0].properties.some( - (p) => - p.type === 'ObjectProperty' && - p.key.type === 'Identifier' && - p.key.name === 'test' - ) + // 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' + ) ); - // Give precedence for the defineConfig expression which contains a test config property - // As with mergeConfig you never know where the test could be e.g. mergeConfig(viteConfig, defineConfig({}), defineConfig({ test: {...} })) - const defineConfigNode = defineConfigNodeWithTest || defineConfigNodes[0]; + const targetConfigObject = configObjectWithTest || configObjectNodes[0]; - if (!defineConfigNode) { + if (!targetConfigObject) { return false; } - const defineConfigProps = defineConfigNode.arguments[0] as t.ObjectExpression; - - // Check if there's already a test property in the defineConfig - const existingTestProp = defineConfigProps.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; @@ -291,8 +287,8 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as // Add the existing test project to the template's array workspaceOrProjectsProp.value.elements.unshift(existingTestProject); - // Remove the existing test property from defineConfig since we're moving it to the array - defineConfigProps.properties = defineConfigProps.properties.filter( + // 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 ); @@ -303,18 +299,18 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as } // Merge the template properties (which now include our existing test project in the array) - mergeProperties(properties, defineConfigProps.properties); + mergeProperties(properties, targetConfigObject.properties); } else { // Fallback to original behavior if template structure is unexpected - mergeProperties(properties, defineConfigProps.properties); + mergeProperties(properties, targetConfigObject.properties); } } else { // Fallback to original behavior if template doesn't have expected structure - mergeProperties(properties, defineConfigProps.properties); + mergeProperties(properties, targetConfigObject.properties); } } else { // No existing test config, just merge normally - mergeProperties(properties, defineConfigProps.properties); + mergeProperties(properties, targetConfigObject.properties); } updated = true; }