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/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/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts
index d627fa416497..f11c43eeff99 100644
--- a/code/addons/vitest/src/postinstall.ts
+++ b/code/addons/vitest/src/postinstall.ts
@@ -199,10 +199,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';
@@ -264,7 +270,7 @@ export default async function postInstall(options: PostinstallOptions) {
/\/\/\/\s*/
);
- const templateName = getTemplateName();
+ const templateName = getTemplateName(configFile);
const alreadyConfigured = isConfigAlreadySetup(rootConfig, configFile);
@@ -446,3 +452,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..477c8c0ddcba
--- /dev/null
+++ b/code/addons/vitest/src/updateVitestFile.config.3.2.test.ts
@@ -0,0 +1,1897 @@
+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
+ -
+ + 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('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
+ -
+ + 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('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']
+ -
+ + 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 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);"
+ `);
+ });
+
+ 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
new file mode 100644
index 000000000000..4cf0e1f576f4
--- /dev/null
+++ b/code/addons/vitest/src/updateVitestFile.config.4.test.ts
@@ -0,0 +1,2216 @@
+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
+ -
+ + 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('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
+ -
+ + 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('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']
+ -
+ + 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 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', {
+ 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);"
+ `);
+ });
+
+ 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', {
+ 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
new file mode 100644
index 000000000000..adfb71f7e876
--- /dev/null
+++ b/code/addons/vitest/src/updateVitestFile.config.test.ts
@@ -0,0 +1,2060 @@
+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
+ -
+ + 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('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
+ -
+ + 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
+ + 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']
+ -
+ + 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 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', {
+ 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);"
+ `);
+ });
+
+ 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.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 9ff9385b80ab..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,1315 +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'
- + }]
- + }
- + }
- + }]
- +
- }
- }));"
- `);
- });
-});
-
-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)
diff --git a/code/addons/vitest/src/updateVitestFile.ts b/code/addons/vitest/src/updateVitestFile.ts
index ca9a01bffd9a..8aa792b52a1e 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';
@@ -69,56 +70,364 @@ const mergeProperties = (
};
/**
- * 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 {}).
+ * Returns true if the identifier is a local alias for `defineConfig`/`defineProject` imported from
+ * either `vitest/config` or `vite`.
*/
-const getTargetConfigObject = (
- target: BabelFile['ast'],
- exportDefault: t.ExportDefaultDeclaration
+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' ||
+ 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 decl = exportDefault.declaration;
- if (decl.type === 'ObjectExpression') {
- return decl;
+ const resolved = resolveExpression(arg, ast);
+ if (!resolved) {
+ return null;
}
- if (
- decl.type === 'CallExpression' &&
- decl.callee.type === 'Identifier' &&
- decl.callee.name === 'defineConfig' &&
- decl.arguments[0]?.type === 'ObjectExpression'
- ) {
- return decl.arguments[0] as t.ObjectExpression;
+
+ 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
+ * 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 (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)
+ const resolved = resolveExpression(testProp.value as t.Expression, ast);
+ 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 `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 === 'workspace' || p.key.name === 'projects') &&
+ p.value.type === 'ArrayExpression';
+
+/**
+ * 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 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 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 (!varDecl) {
- return null;
+ if (templateProjectRefsProp && templateProjectRefsProp.value.type === 'ArrayExpression') {
+ (existingProjectRefsProp.value as t.ArrayExpression).elements.push(
+ ...(templateProjectRefsProp.value as t.ArrayExpression).elements
+ );
}
- const declarator = varDecl.declarations.find(
- (d) => d.id.type === 'Identifier' && d.id.name === varName
+
+ // 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)
);
- if (!declarator?.init) {
- return null;
+ for (const templateProp of templateTestProp.value.properties) {
+ if (
+ 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);
+ }
}
- const init = declarator.init;
+ }
+
+ // 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, hoisting shared properties (coverage, env, pool, maxWorkers) 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;
+ }
+
+ // 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) as t.ObjectProperty[];
+
+ const topLevelPropSet = new Set(topLevelProps);
+ const projectTestProps = resolvedTestValue.properties.filter(
+ (p) => !topLevelPropSet.has(p as any)
+ );
+
+ // Create the existing test project: { extends: true, test: { ...projectTestProps } }
+ 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: projectTestProps,
+ } 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 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);
+};
+
+/**
+ * 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:
+ *
+ * - 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, ast) && resolved.arguments.length > 0) {
+ const innerArg = resolveExpression(resolved.arguments[0] as t.Expression, ast);
if (
- init.type === 'CallExpression' &&
- init.callee.type === 'Identifier' &&
- init.callee.name === 'defineConfig' &&
- init.arguments[0]?.type === 'ObjectExpression'
+ innerArg?.type === 'CallExpression' &&
+ innerArg.callee.type === 'Identifier' &&
+ innerArg.callee.name === 'mergeConfig'
) {
- return init.arguments[0] as t.ObjectExpression;
- }
- if (init.type === 'ObjectExpression') {
- return init;
+ 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 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.type === 'ObjectExpression') {
+ return resolved;
+ }
+ if (
+ resolved.type === 'CallExpression' &&
+ isDefineConfigLike(resolved, target) &&
+ resolved.arguments[0]?.type === 'ObjectExpression'
+ ) {
+ return resolved.arguments[0] as t.ObjectExpression;
+ }
return null;
};
@@ -165,50 +474,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, target) &&
+ 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;
}
@@ -258,196 +541,42 @@ 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 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);
- }
- }
-
- // 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'
- )
- );
-
- const targetConfigObject = configObjectWithTest || configObjectNodes[0];
-
- if (!targetConfigObject) {
- return false;
- }
+ } 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 a plain object, a wrapper call with object argument,
+ // an Identifier (variable reference), or wrapped in a TS type annotation.
+ const configObjectNodes: t.ObjectExpression[] = [];
- // 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 (existingTestProp && existingTestProp.value.type === 'ObjectExpression') {
- // 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 = 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);
+ for (const arg of mergeConfigCall.arguments) {
+ const configObject = getConfigObjectFromMergeArg(arg as t.Expression, target);
+ if (configObject) {
+ configObjectNodes.push(configObject);
}
- } 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(
+ }
+
+ // Prefer the config object that already has an immediate `test` property.
+ const configObjectWithTest = configObjectNodes.find((obj) =>
+ obj.properties.some(
(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 = 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',
- 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 doesn't have expected structure
- mergeProperties(properties, targetConfigObject.properties);
+ p.key.name === 'test'
+ )
+ );
+
+ const targetConfigObject = configObjectWithTest || configObjectNodes[0];
+
+ if (!targetConfigObject) {
+ return false;
}
- } else {
- // No existing test config, just merge normally
- mergeProperties(properties, targetConfigObject.properties);
+
+ mergeTemplateIntoConfigObject(targetConfigObject, properties, target);
+ updated = true;
}
- updated = true;
}
}
}
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-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),
],
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/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
new file mode 100644
index 000000000000..3b2ab1d4ca80
--- /dev/null
+++ b/code/core/src/babel/expression-resolver.ts
@@ -0,0 +1,60 @@
+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;
+ 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;
+ }
+ 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;
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 01f43cf9050d..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/",
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",
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
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',
];