Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
db9f4af
Addon Vitest: support Vitest 5 initialization, reporters and test names
PaulMest Sep 8, 2026
1b3565a
Build: run local Storybook development and tests on Vitest 5
PaulMest Sep 8, 2026
414e293
Addon Vitest: preserve initialization on older supported versions
PaulMest Sep 8, 2026
1c66b5c
Addon Vitest: isolate generated Storybook projects from root test config
PaulMest Sep 8, 2026
0568fab
chore: remove expired Vitest release-age exceptions
PaulMest Sep 22, 2026
4982531
docs: clarify global coverage configuration for isolated projects
PaulMest Sep 22, 2026
e77f7f9
fix(addon-vitest): support Vitest 5 coverage reporter loading
PaulMest Sep 22, 2026
d936a5c
fix(addon-vitest): preserve shared Vite configuration
PaulMest Sep 24, 2026
b17ae39
test: fix core mocks for Vitest 5
PaulMest Sep 24, 2026
f73171b
chore: use caret ranges for Vitest 5 packages
PaulMest Sep 24, 2026
a16ead3
docs: clarify Vitest project isolation limits
PaulMest Sep 24, 2026
567b645
test(addon-vitest): verify built coverage reporter loaders
PaulMest Sep 24, 2026
0e516a3
test(vitest): exercise real cross-version browser lifecycles
PaulMest Sep 26, 2026
0dfef13
fix(vitest): resolve browser APIs on Vitest 3
PaulMest Sep 26, 2026
f5883c0
fix(vitest): use current coverage watermarks after runner restarts
PaulMest Sep 26, 2026
c2bdce5
test(vitest): verify framework config migration and UI watch runs
PaulMest Sep 26, 2026
5a5e7e9
test(vitest): add reproducible coverage and mutation measurements
PaulMest Sep 26, 2026
3ea7355
test(vitest): type internal store preparation in compatibility harness
PaulMest Sep 26, 2026
b7d31f1
test(vitest): wait for watch cleanup before coverage tests
PaulMest Sep 27, 2026
fcad0e5
fix(vitest): align compatibility with the Storybook 11 Vitest floor
PaulMest Sep 30, 2026
db78da2
test(vitest): adapt compatibility harness to the latest store API
PaulMest Sep 30, 2026
9e33b96
test(e2e): focus open dialogs before dismissing them
PaulMest Sep 30, 2026
2ac499f
test(core): name type assertions for Vitest 5 collection
PaulMest Oct 1, 2026
bd87999
fix(angular-vite): initialize Angular 22.2 optimizer hashing
PaulMest Oct 1, 2026
ad28d8c
fix(angular-vite): require Analog with Angular 22 cache support
PaulMest Oct 2, 2026
b4344f5
test(e2e): reset live onboarding survey state between attempts
PaulMest Oct 2, 2026
5682fed
Merge next and deduplicate Analog dependencies
PaulMest Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ Storybook is a large TypeScript monorepo. The git root is the repo root, the mai

- **Base branch**: `next` (all PRs should target `next`, not `main`)
- **Node.js**: `22.22.3` (see `.nvmrc`) — supports `.ts` natively via type stripping (no loader needed)
- **Package Manager**: Yarn Berry
- **Package Manager**: Yarn Berry (use `node .yarn/releases/yarn-4.18.0.cjs` if no global Yarn is configured)
- **Vitest**: 5.x for local development; addon compatibility tests also cover the Vitest 4 test-name separator.
- **Task orchestration**: NX plus the custom `yarn task` runner
- **Linting**: oxlint (root `.oxlintrc.json`, extended by `code/.oxlintrc.json` and `scripts/.oxlintrc.json`; custom rules load via `jsPlugins`). ESLint is no longer used for repo linting — `code/lib/eslint-plugin` remains as the published `eslint-plugin-storybook` package.
- **Formatting**: oxfmt (root `.oxfmtrc.json`)
Expand Down
2 changes: 1 addition & 1 deletion code/addons/a11y/src/components/A11YPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @vitest-environment happy-dom
/// <reference types="@testing-library/jest-dom" />
import '@testing-library/jest-dom/vitest';
import { fireEvent, render } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

Expand Down
1 change: 1 addition & 0 deletions code/addons/a11y/src/components/A11yContext.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest';
import { act, cleanup, render } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

Expand Down
2 changes: 1 addition & 1 deletion code/addons/links/src/react/components/link.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @vitest-environment happy-dom
/// <reference types="@testing-library/jest-dom" />
import '@testing-library/jest-dom/vitest';
import { act, cleanup, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
Expand Down
15 changes: 6 additions & 9 deletions code/addons/vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,12 @@
"@types/micromatch": "^4.0.0",
"@types/node": "^22.19.1",
"@types/semver": "^7.7.1",
"@vitest/browser-playwright": "^4.1.6",
"@vitest/runner": "^4.1.6",
"@vitest/browser-playwright": "^5.0.0",
"@vitest/istanbul-lib-report": "^1.0.1",
"empathic": "^2.0.0",
"es-toolkit": "^1.43.0",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.2.0",
"micromatch": "^4.0.8",
"pathe": "^1.1.2",
"picocolors": "^1.1.0",
Expand All @@ -104,14 +105,13 @@
"tree-kill": "^1.2.2",
"ts-dedent": "^2.2.0",
"typescript": "^6.0.3",
"vitest": "^4.1.6"
"vitest": "^5.0.0"
},
"peerDependencies": {
"@vitest/browser": "^3.0.0 || ^4.0.0 || ^5.0.0",
"@vitest/browser": "^4.0.0 || ^5.0.0",
"@vitest/browser-playwright": "^4.0.0 || ^5.0.0",
"@vitest/runner": "^3.0.0 || ^4.0.0 || ^5.0.0",
"storybook": "workspace:^",
"vitest": "^3.0.0 || ^4.0.0 || ^5.0.0"
"vitest": "^4.0.0 || ^5.0.0"
},
"peerDependenciesMeta": {
"@vitest/browser": {
Expand All @@ -120,9 +120,6 @@
"@vitest/browser-playwright": {
"optional": true
},
"@vitest/runner": {
"optional": true
},
"vitest": {
"optional": true
}
Expand Down
81 changes: 81 additions & 0 deletions code/addons/vitest/src/node/coverage-reporter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { execFile } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';

import { expect, it, vi } from 'vitest';

import type { ReportNode } from 'istanbul-lib-report';

import StorybookCoverageReporter from './coverage-reporter.ts';
import type { TestManager } from './test-manager.ts';

it.each([
['Vitest 3/4', "require('istanbul-reports').create"],
['Vitest 5', "(await import('@vitest/istanbul-lib-report')).createAsync"],
])('loads the built coverage reporter through the %s loader', async (_version, loader) => {
await expect(
promisify(execFile)(
process.execPath,
[
'--input-type=module',
'--eval',
`
import { strict as assert } from 'node:assert';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const summaries = [];
const reporter = await (${loader})(
'@storybook/addon-vitest/internal/coverage-reporter',
{
testManager: {
onCoverageCollected: (summary) => summaries.push(summary),
vitestManager: { vitest: { config: { coverage: { watermarks: { statements: [50, 90] } } } } },
},
}
);
reporter.onSummary({
isRoot: () => true,
getCoverageSummary: () => ({ data: { statements: { pct: 87.4 } } }),
});
assert.deepEqual(summaries, [{ percentage: 87, status: 'warning' }]);
`,
],
{ cwd: fileURLToPath(new URL('../../', import.meta.url)) }
)
).resolves.toMatchObject({ stdout: '' });
});
Comment thread
PaulMest marked this conversation as resolved.

it.each([
{ percentage: 49, watermarks: [50, 90], expected: { percentage: 49, status: 'negative' } },
{ percentage: 50, watermarks: [50, 90], expected: { percentage: 50, status: 'warning' } },
{ percentage: 89.5, watermarks: [50, 90], expected: { percentage: 90, status: 'positive' } },
{ percentage: 79.4, watermarks: undefined, expected: { percentage: 79, status: 'warning' } },
{ percentage: 80, watermarks: [], expected: { percentage: 80, status: 'positive' } },
{ percentage: 60, watermarks: [60], expected: { percentage: 60, status: 'warning' } },
])(
'reports $expected for $percentage with watermarks $watermarks',
({ percentage, watermarks, expected }) => {
const onCoverageCollected = vi.fn();
const testManager = {
onCoverageCollected,
vitestManager: {
vitest: { config: { coverage: { watermarks: { statements: watermarks } } } },
},
} as unknown as TestManager;
const reporter = new StorybookCoverageReporter({ testManager });
reporter.onSummary({
isRoot: () => true,
getCoverageSummary: () => ({ data: { statements: { pct: percentage } } }),
} as unknown as ReportNode);
expect(onCoverageCollected).toHaveBeenCalledExactlyOnceWith(expected);
}
);

it('ignores nested coverage summaries', () => {
const onCoverageCollected = vi.fn();
const reporter = new StorybookCoverageReporter({
testManager: { onCoverageCollected } as unknown as TestManager,
});
reporter.onSummary({ isRoot: () => false } as ReportNode);
expect(onCoverageCollected).not.toHaveBeenCalled();
});
17 changes: 4 additions & 13 deletions code/addons/vitest/src/node/coverage-reporter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { ResolvedCoverageOptions } from 'vitest/node';

import type { ReportNode, Visitor } from 'istanbul-lib-report';
import { ReportBase } from 'istanbul-lib-report';

Expand All @@ -8,18 +6,14 @@ import type { TestManager } from './test-manager.ts';

export type StorybookCoverageReporterOptions = {
testManager: TestManager;
coverageOptions: ResolvedCoverageOptions | undefined;
};

class StorybookCoverageReporter extends ReportBase implements Partial<Visitor> {
#testManager: StorybookCoverageReporterOptions['testManager'];

#coverageOptions: StorybookCoverageReporterOptions['coverageOptions'];

constructor(opts: StorybookCoverageReporterOptions) {
super();
this.#testManager = opts.testManager;
this.#coverageOptions = opts.coverageOptions;
}

onSummary(node: ReportNode) {
Expand All @@ -32,7 +26,7 @@ class StorybookCoverageReporter extends ReportBase implements Partial<Visitor> {

// Fallback to Vitest's default watermarks https://vitest.dev/config/#coverage-watermarks
const [lowWatermark = 50, highWatermark = 80] =
this.#coverageOptions?.watermarks?.statements ?? [];
this.#testManager.vitestManager.vitest?.config.coverage.watermarks?.statements ?? [];

const coverageSummary: StoreState['currentRun']['coverageSummary'] = {
percentage,
Expand All @@ -47,10 +41,7 @@ class StorybookCoverageReporter extends ReportBase implements Partial<Visitor> {
}
}

/**
* This is exported weirdly because the coverage tool loading this uses `Cont = require(name)`. So
* it doesn't support a default export, it has to be the "root" export as CJS
*
* @see https://nodejs.org/docs/latest-v20.x/api/modules.html#loading-ecmascript-modules-using-require
*/
export default StorybookCoverageReporter;

// Vitest 3 and 4 load custom coverage reporters with require(name).
export { StorybookCoverageReporter as 'module.exports' };
6 changes: 2 additions & 4 deletions code/addons/vitest/src/node/reporter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { SerializedError } from 'vitest';
import type { TestCase, TestModule, Vitest } from 'vitest/node';
import { type Reporter } from 'vitest/reporters';
import type { SerializedError, TaskMeta } from 'vitest';
import type { Reporter, TestCase, TestModule, Vitest } from 'vitest/node';

import type { TaskMeta } from '@vitest/runner';
import type { Report } from 'storybook/preview-api';

import type { VitestError } from '../types.ts';
Expand Down
36 changes: 34 additions & 2 deletions code/addons/vitest/src/node/test-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { DOUBLE_SPACES } from './vitest-manager.ts';
const setTestNamePattern = vi.hoisted(() => vi.fn());
const vitest = vi.hoisted(() => ({
projects: [{}],
standalone: vi.fn(),
init: vi.fn(),
close: vi.fn(),
onCancel: vi.fn(),
Expand Down Expand Up @@ -52,17 +53,22 @@ const vitest = vi.hoisted(() => ({
},
}));

const mockVitestVersion = vi.hoisted(() => ({ value: '4.1.6' }));
const mockCreateVitest = vi.fn();

vi.mock('vitest/node', () => ({
createVitest: mockCreateVitest,
get version() {
return mockVitestVersion.value;
},
}));

// Use the mock function directly
const createVitest = mockCreateVitest;

beforeEach(() => {
vi.clearAllMocks();
mockVitestVersion.value = '4.1.6';
mockStore.setState(() => storeOptions.initialState);
vitest.projects = [{}];
vitest.config.coverage.enabled = false;
Expand Down Expand Up @@ -194,6 +200,28 @@ const options: TestManagerOptions = {
};

describe('TestManager', () => {
it('initializes Vitest without running tests', async () => {
await TestManager.start(options);
expect(vitest.standalone).toHaveBeenCalledOnce();
expect(vitest.init).not.toHaveBeenCalled();
expect(vitest.runTestSpecifications).not.toHaveBeenCalled();
});

it.each(['3.2.4', '4.0.0'])(
'initializes Vitest %s without standalone or running tests',
async (version) => {
mockVitestVersion.value = version;
const { standalone, ...legacyVitest } = vitest;
createVitest.mockResolvedValue(legacyVitest);

await TestManager.start(options);

expect(legacyVitest.init).toHaveBeenCalledOnce();
expect(standalone).not.toHaveBeenCalled();
expect(legacyVitest.runTestSpecifications).not.toHaveBeenCalled();
}
);

it('should create a vitest instance', async () => {
new TestManager(options);
await vi.waitFor(() => {
Expand Down Expand Up @@ -490,7 +518,11 @@ describe('TestManager', () => {
expect(setTestNamePattern).toHaveBeenCalledWith(new RegExp(`^One$`));
});

it('should trigger a single story test', async () => {
it.each([
['4.1.6', ' '],
['5.0.0', ' > '],
])('should trigger a single story test with Vitest %s', async (version, separator) => {
mockVitestVersion.value = version;
vitest.globTestSpecifications.mockImplementation(() => tests);
const testManager = await startWithIndex();

Expand All @@ -503,7 +535,7 @@ describe('TestManager', () => {
});
// regex should be Parent Story Name + Test Name
expect(setTestNamePattern).toHaveBeenCalledWith(
new RegExp(`^Parent story${DOUBLE_SPACES} Test name$`)
new RegExp(`^Parent story${DOUBLE_SPACES}${separator}Test name$`)
);
});

Expand Down
22 changes: 11 additions & 11 deletions code/addons/vitest/src/node/vitest-manager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { existsSync, readFileSync } from 'node:fs';

import type {
CoverageOptions,
ResolvedCoverageOptions,
TestProject,
TestSpecification,
Vitest,
} from 'vitest/node';
import type { CoverageOptions, TestProject, TestSpecification, Vitest } from 'vitest/node';

import { getProjectRoot, resolvePathInStorybookCache } from 'storybook/internal/common';
import { Tag } from 'storybook/internal/core-server';
Expand Down Expand Up @@ -44,6 +38,8 @@ const getTestName = (name: string) => `${name}${DOUBLE_SPACES}`;
export class VitestManager {
vitest: Vitest | null = null;

private testNameSeparator = ' ';

vitestStartupCounter = 0;

vitestRestartPromise: Promise<void> | null = null;
Expand All @@ -53,13 +49,13 @@ export class VitestManager {
constructor(private testManager: TestManager) {}

async startVitest({ coverage }: { coverage: boolean }) {
const { createVitest } = await import('vitest/node');
const { createVitest, version } = await import('vitest/node');
this.testNameSeparator = Number.parseInt(version, 10) >= 5 ? ' > ' : ' ';

const storybookCoverageReporter: [string, StorybookCoverageReporterOptions] = [
'@storybook/addon-vitest/internal/coverage-reporter',
{
testManager: this.testManager,
coverageOptions: this.vitest?.config?.coverage as ResolvedCoverageOptions | undefined,
},
];
const coverageOptions = (
Expand Down Expand Up @@ -159,7 +155,11 @@ export class VitestManager {
}

try {
await this.vitest.init();
if (typeof this.vitest.standalone === 'function') {
await this.vitest.standalone();
} else {
await this.vitest.init();
}
} catch (e: any) {
let message = 'Failed to initialize Vitest';
const isV8 = e.message?.includes('@vitest/coverage-v8');
Expand Down Expand Up @@ -261,7 +261,7 @@ export class VitestManager {
throw new Error(`Parent story not found for story ${story.id}`);
}

return `^${escapeRegExp(getTestName(parentStory.name))} ${escapeRegExp(story.name)}$`;
return `^${escapeRegExp(getTestName(parentStory.name))}${this.testNameSeparator}${escapeRegExp(story.name)}$`;
}

return `^${escapeRegExp(story.name)}$`;
Expand Down
Loading
Loading