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;
}