diff --git a/code/addons/vitest/src/updateVitestFile.test.ts b/code/addons/vitest/src/updateVitestFile.test.ts index 6e08aa611eef..c316e0f281e2 100644 --- a/code/addons/vitest/src/updateVitestFile.test.ts +++ b/code/addons/vitest/src/updateVitestFile.test.ts @@ -854,6 +854,95 @@ describe('updateConfigFile', () => { `); }); + it('appends storybook project to existing test.projects array (no double nesting)', 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: { + 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' + + }] + + }, + + setupFiles: ['../.storybook/vitest.setup.ts'] + + } + + + }] + } + }));" + `); + }); + it('extracts coverage config and keeps it at top level when using workspace', async () => { const source = babel.babelParse( await loadTemplate('vitest.config.template.ts', { diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts index 0f544dde3482..567df3de2114 100644 --- a/code/addons/vitest/src/updateVitestFile.ts +++ b/code/addons/vitest/src/updateVitestFile.ts @@ -232,8 +232,64 @@ export const updateConfigFile = (source: BabelFile['ast'], target: BabelFile['as p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'test' ) as t.ObjectProperty | undefined; - if (templateTestProp && templateTestProp.value.type === 'ObjectExpression') { - // Find the workspace/projects array in the template + 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 + ); + 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( (p) => p.type === 'ObjectProperty' &&