diff --git a/AGENTS.md b/AGENTS.md index 6b556d268f68..e94316c2e490 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`) diff --git a/code/addons/a11y/src/components/A11YPanel.test.tsx b/code/addons/a11y/src/components/A11YPanel.test.tsx index 12040e9eb902..623c2d17e2b7 100644 --- a/code/addons/a11y/src/components/A11YPanel.test.tsx +++ b/code/addons/a11y/src/components/A11YPanel.test.tsx @@ -1,5 +1,5 @@ // @vitest-environment happy-dom -/// +import '@testing-library/jest-dom/vitest'; import { fireEvent, render } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; diff --git a/code/addons/a11y/src/components/A11yContext.test.tsx b/code/addons/a11y/src/components/A11yContext.test.tsx index f6ee6c5a969e..9eeb1763611e 100644 --- a/code/addons/a11y/src/components/A11yContext.test.tsx +++ b/code/addons/a11y/src/components/A11yContext.test.tsx @@ -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'; diff --git a/code/addons/links/src/react/components/link.test.tsx b/code/addons/links/src/react/components/link.test.tsx index 8bd2e3bbcd01..e343dae703f4 100644 --- a/code/addons/links/src/react/components/link.test.tsx +++ b/code/addons/links/src/react/components/link.test.tsx @@ -1,5 +1,5 @@ // @vitest-environment happy-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'; diff --git a/code/addons/vitest/package.json b/code/addons/vitest/package.json index 758738a67d9a..94841810d613 100644 --- a/code/addons/vitest/package.json +++ b/code/addons/vitest/package.json @@ -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", @@ -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": { @@ -120,9 +120,6 @@ "@vitest/browser-playwright": { "optional": true }, - "@vitest/runner": { - "optional": true - }, "vitest": { "optional": true } diff --git a/code/addons/vitest/src/node/coverage-reporter.test.ts b/code/addons/vitest/src/node/coverage-reporter.test.ts new file mode 100644 index 000000000000..2aa17e5b4086 --- /dev/null +++ b/code/addons/vitest/src/node/coverage-reporter.test.ts @@ -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: '' }); +}); + +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(); +}); diff --git a/code/addons/vitest/src/node/coverage-reporter.ts b/code/addons/vitest/src/node/coverage-reporter.ts index 118d1730c59f..efe2ea43b3ca 100644 --- a/code/addons/vitest/src/node/coverage-reporter.ts +++ b/code/addons/vitest/src/node/coverage-reporter.ts @@ -1,5 +1,3 @@ -import type { ResolvedCoverageOptions } from 'vitest/node'; - import type { ReportNode, Visitor } from 'istanbul-lib-report'; import { ReportBase } from 'istanbul-lib-report'; @@ -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 { #testManager: StorybookCoverageReporterOptions['testManager']; - #coverageOptions: StorybookCoverageReporterOptions['coverageOptions']; - constructor(opts: StorybookCoverageReporterOptions) { super(); this.#testManager = opts.testManager; - this.#coverageOptions = opts.coverageOptions; } onSummary(node: ReportNode) { @@ -32,7 +26,7 @@ class StorybookCoverageReporter extends ReportBase implements Partial { // 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, @@ -47,10 +41,7 @@ class StorybookCoverageReporter extends ReportBase implements Partial { } } -/** - * 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' }; diff --git a/code/addons/vitest/src/node/reporter.ts b/code/addons/vitest/src/node/reporter.ts index 493541170323..d729d987e289 100644 --- a/code/addons/vitest/src/node/reporter.ts +++ b/code/addons/vitest/src/node/reporter.ts @@ -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'; diff --git a/code/addons/vitest/src/node/test-manager.test.ts b/code/addons/vitest/src/node/test-manager.test.ts index 7c2a61fcd4a7..8915d7d3dcfd 100644 --- a/code/addons/vitest/src/node/test-manager.test.ts +++ b/code/addons/vitest/src/node/test-manager.test.ts @@ -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(), @@ -52,10 +53,14 @@ 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 @@ -63,6 +68,7 @@ const createVitest = mockCreateVitest; beforeEach(() => { vi.clearAllMocks(); + mockVitestVersion.value = '4.1.6'; mockStore.setState(() => storeOptions.initialState); vitest.projects = [{}]; vitest.config.coverage.enabled = false; @@ -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(() => { @@ -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(); @@ -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$`) ); }); diff --git a/code/addons/vitest/src/node/vitest-manager.ts b/code/addons/vitest/src/node/vitest-manager.ts index c58b0ce2023d..95411ddbe37b 100644 --- a/code/addons/vitest/src/node/vitest-manager.ts +++ b/code/addons/vitest/src/node/vitest-manager.ts @@ -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'; @@ -44,6 +38,8 @@ const getTestName = (name: string) => `${name}${DOUBLE_SPACES}`; export class VitestManager { vitest: Vitest | null = null; + private testNameSeparator = ' '; + vitestStartupCounter = 0; vitestRestartPromise: Promise | null = null; @@ -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 = ( @@ -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'); @@ -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)}$`; diff --git a/code/addons/vitest/src/updateVitestFile.config.4.test.ts b/code/addons/vitest/src/updateVitestFile.config.4.test.ts index 81b391e725f9..0bb9e516f876 100644 --- a/code/addons/vitest/src/updateVitestFile.config.4.test.ts +++ b/code/addons/vitest/src/updateVitestFile.config.4.test.ts @@ -1,6 +1,9 @@ -import { join } from 'node:path'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; +import { createVitest, type Vitest } from 'vitest/node'; import * as babel from 'storybook/internal/babel'; @@ -20,6 +23,77 @@ vi.mock('../../../core/src/shared/utils/module', () => ({ })); describe('updateConfigFile', () => { + it.each(['existing projects', 'single project'])( + 'preserves shared Vite configuration and isolates unit setup when adding to %s', + async (mode) => { + const setupFile = resolve('unit-only-setup.js'); + const source = babel.babelParse( + await loadTemplate('vitest.config.4.template', { CONFIG_DIR: '.storybook' }) + ); + const target = babel.babelParse(` + export default defineConfig({ + plugins: [{ name: 'shared-plugin' }], + resolve: { alias: { 'shared-alias': '/shared' } }, + test: { + ${ + mode === 'existing projects' + ? `projects: [{ extends: true, test: { name: 'unit', setupFiles: [${JSON.stringify(setupFile)}] } }]` + : `name: 'unit', setupFiles: [${JSON.stringify(setupFile)}]` + } + } + }); + `); + expect(updateConfigFile(source, target)).toBe(true); + const declaration = target.program.body.find( + (node) => node.type === 'ExportDefaultDeclaration' + )!; + const directory = await mkdtemp(join(tmpdir(), 'storybook-vitest-config-')); + let vitest: Vitest | undefined; + try { + const configFile = join(directory, 'vitest.config.mjs'); + await writeFile( + configFile, + ` + import path from 'node:path'; + import { playwright } from ${JSON.stringify(import.meta.resolve('@vitest/browser-playwright'))}; + const dirname = ${JSON.stringify(process.cwd())}; + const defineConfig = (value) => value; + const storybookTest = () => ({ name: 'storybook-test-probe' }); + export default ${babel.generate(declaration.declaration).code}; + ` + ); + vitest = await createVitest('test', { + config: configFile, + configLoader: 'native', + watch: false, + reporters: [], + }); + const unit = vitest.projects.find((project) => project.name === 'unit')!; + const storybook = vitest.projects.find((project) => project.config.browser.enabled)!; + expect(unit.config.setupFiles).toEqual([setupFile]); + expect(unit.vite.config.plugins.map((plugin) => plugin.name)).toContain('shared-plugin'); + expect(unit.vite.config.resolve.alias).toContainEqual({ + find: 'shared-alias', + replacement: '/shared', + }); + expect(storybook.config.setupFiles).toEqual([]); + expect(storybook.vite.config.plugins.map((plugin) => plugin.name)).toContain( + 'shared-plugin' + ); + expect(storybook.vite.config.resolve.alias).toContainEqual({ + find: 'shared-alias', + replacement: '/shared', + }); + expect(storybook.vite.config.plugins.map((plugin) => plugin.name)).toContain( + 'storybook-test-probe' + ); + } finally { + await vitest?.close(); + await rm(directory, { recursive: true, force: true }); + } + } + ); + it('updates vite config file with existing projects', async () => { const source = babel.babelParse( await loadTemplate('vitest.config.4.template', { diff --git a/code/addons/vitest/src/vitest-plugin/agent-telemetry-reporter.ts b/code/addons/vitest/src/vitest-plugin/agent-telemetry-reporter.ts index b8092e16ff91..c0196d95d8b9 100644 --- a/code/addons/vitest/src/vitest-plugin/agent-telemetry-reporter.ts +++ b/code/addons/vitest/src/vitest-plugin/agent-telemetry-reporter.ts @@ -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 { analyzeTestResults, toStoryTestResult } from 'storybook/internal/core-server'; import type { StoryTestResult } from 'storybook/internal/core-server'; diff --git a/code/core/src/actions/models/HandlerFunction.test-d.ts b/code/core/src/actions/models/HandlerFunction.test-d.ts index 455eb8a43e3c..e535d81e8478 100644 --- a/code/core/src/actions/models/HandlerFunction.test-d.ts +++ b/code/core/src/actions/models/HandlerFunction.test-d.ts @@ -1,9 +1,11 @@ -import { expectTypeOf } from 'vitest'; +import { expectTypeOf, test } from 'vitest'; import type { HandlerFunction } from './HandlerFunction.ts'; -// Should be assignable to async callback props (the fixed case) -expectTypeOf().toExtend<() => Promise>(); +test('is assignable to async callbacks', () => { + expectTypeOf().toExtend<() => Promise>(); +}); -// Should remain assignable to plain void callbacks -expectTypeOf().toExtend<() => void>(); +test('is assignable to void callbacks', () => { + expectTypeOf().toExtend<() => void>(); +}); diff --git a/code/core/src/backgrounds/index.test-d.ts b/code/core/src/backgrounds/index.test-d.ts index 48a291580cde..52f1170348bd 100644 --- a/code/core/src/backgrounds/index.test-d.ts +++ b/code/core/src/backgrounds/index.test-d.ts @@ -1,15 +1,17 @@ -import { expectTypeOf } from 'vitest'; +import { expectTypeOf, test } from 'vitest'; import { DEFAULT_BACKGROUNDS } from 'storybook/backgrounds'; import type { Background, BackgroundMap } from 'storybook/backgrounds'; -expectTypeOf().toEqualTypeOf>(); +test('accepts built-in and custom background maps', () => { + expectTypeOf().toEqualTypeOf>(); -expectTypeOf(DEFAULT_BACKGROUNDS).toEqualTypeOf(); + expectTypeOf(DEFAULT_BACKGROUNDS).toEqualTypeOf(); -const customBackgrounds = { - light: { name: 'Light', value: '#ffffff' }, - dark: { name: 'Dark', value: '#1a1a1a' }, -} satisfies BackgroundMap; + const customBackgrounds = { + light: { name: 'Light', value: '#ffffff' }, + dark: { name: 'Dark', value: '#1a1a1a' }, + } satisfies BackgroundMap; -expectTypeOf(customBackgrounds.light).toExtend(); + expectTypeOf(customBackgrounds.light).toExtend(); +}); diff --git a/code/core/src/cli/angular/helpers.ts b/code/core/src/cli/angular/helpers.ts index 9a00c6be5b3c..3ef49d5513a0 100644 --- a/code/core/src/cli/angular/helpers.ts +++ b/code/core/src/cli/angular/helpers.ts @@ -16,8 +16,7 @@ import semver from 'semver'; export const ANGULAR_JSON_PATH = 'angular.json'; -/** Must stay inside the `>=2.7.3` peer range that `@storybook/angular-vite` declares. */ -export const ANALOG_VITE_PLUGIN_ANGULAR_VERSION = '^2.7.3'; +export const ANALOG_VITE_PLUGIN_ANGULAR_VERSION = '^2.7.5'; export const toDevkitVersion = (ngRange?: string | null): string | undefined => { if (!ngRange) { diff --git a/code/core/src/common/js-package-manager/PNPMProxy.catalog.test.ts b/code/core/src/common/js-package-manager/PNPMProxy.catalog.test.ts index a3c50c8bfdb0..439f0bfd00d8 100644 --- a/code/core/src/common/js-package-manager/PNPMProxy.catalog.test.ts +++ b/code/core/src/common/js-package-manager/PNPMProxy.catalog.test.ts @@ -25,7 +25,8 @@ describe('PNPMProxy catalogs', () => { beforeEach(() => { // Restore the real fs so the proxy can be constructed, then redirect fs to memfs. - vi.resetAllMocks(); + vi.mocked(readFileSync).mockReset(); + vi.mocked(writeFileSync).mockReset(); pnpmProxy = new PNPMProxy(); vol.reset(); vi.mocked(readFileSync).mockImplementation(memfs.readFileSync as typeof readFileSync); diff --git a/code/core/src/common/utils/__snapshots__/formatter.test.ts.snap b/code/core/src/common/utils/__snapshots__/formatter.test.ts.snap index 784281ec3075..bd0a9ddf1a0c 100644 --- a/code/core/src/common/utils/__snapshots__/formatter.test.ts.snap +++ b/code/core/src/common/utils/__snapshots__/formatter.test.ts.snap @@ -1,14 +1,13 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`formatter > withPrettierConfig > prettier > formats content with prettier 1`] = ` -" -import type { Meta, StoryObj } from '@storybook/nextjs' +"import type { Meta, StoryObj } from '@storybook/nextjs'; import Component from './foo'; - const meta = { - component: Component - } satisfies Meta; +const meta = { + component: Component, +} satisfies Meta; export default meta; @@ -17,14 +16,13 @@ type Story = StoryObj; `; exports[`formatter > withoutPrettierConfigAndWithEditorConfig > prettier-v3 > formats content with prettier 1`] = ` -" -import type { Meta, StoryObj } from '@storybook/nextjs' +"import type { Meta, StoryObj } from "@storybook/nextjs"; -import Component from './foo'; +import Component from "./foo"; - const meta = { - component: Component - } satisfies Meta; +const meta = { + component: Component, +} satisfies Meta; export default meta; diff --git a/code/core/src/common/utils/formatter.test.ts b/code/core/src/common/utils/formatter.test.ts index 4d03f2800631..a5e3c0eb7e6b 100644 --- a/code/core/src/common/utils/formatter.test.ts +++ b/code/core/src/common/utils/formatter.test.ts @@ -37,7 +37,7 @@ describe('formatter', () => { const testPath = resolve(__dirname, '__tests-formatter__', 'withPrettierConfig'); describe('prettier', async () => { - const prettierV3 = await import('prettier'); + const prettierV3 = await vi.importActual('prettier'); it('formats content with prettier', async () => { mockPrettier.format.mockImplementation(prettierV3.format); @@ -57,7 +57,7 @@ describe('formatter', () => { const testPath = resolve(__dirname, '__tests-formatter__', 'withoutPrettierConfig'); describe('prettier-v3', async () => { - const prettierV3 = await import('prettier'); + const prettierV3 = await vi.importActual('prettier'); it('formats content with prettier', async () => { mockPrettier.format.mockImplementation(prettierV3.format); @@ -77,7 +77,7 @@ describe('formatter', () => { const testPath = resolve(__dirname, '__tests-formatter__', 'withoutEditorConfig'); describe('prettier-v3', async () => { - const prettierV3 = await import('prettier'); + const prettierV3 = await vi.importActual('prettier'); it('formats content with prettier', async () => { mockPrettier.format.mockImplementation(prettierV3.format); diff --git a/code/core/src/manager-api/tests/stories.test.ts b/code/core/src/manager-api/tests/stories.test.ts index 56d8651a9cac..4c6d139c0701 100644 --- a/code/core/src/manager-api/tests/stories.test.ts +++ b/code/core/src/manager-api/tests/stories.test.ts @@ -44,6 +44,8 @@ const getEventMetadata = vi.mocked(getEventMetadataOriginal); const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +vi.mock('../stores/status.ts'); + vi.mock('../lib/events.ts', () => ({ getEventMetadata: vi.fn(() => ({ sourceType: 'local' })), })); @@ -1655,7 +1657,6 @@ describe('stories API', () => { }); it('can filter on status', async () => { - vi.mock('../stores/status'); const moduleArgs = createMockModuleArgs({}); const { api } = initStories(moduleArgs as unknown as ModuleArgs); const { store } = moduleArgs; @@ -2015,7 +2016,6 @@ describe('stories API', () => { describe('computeStatusFilterFn (via experimental_setFilter)', () => { it('passes through all stories when both included and excluded are empty', async () => { - vi.mock('../stores/status'); const moduleArgs = createMockModuleArgs({}); const { api } = initStories(moduleArgs as unknown as ModuleArgs); const { store } = moduleArgs; @@ -2028,7 +2028,6 @@ describe('stories API', () => { }); it('applies OR logic within included status filters', async () => { - vi.mock('../stores/status'); const moduleArgs = createMockModuleArgs({}); const { api } = initStories(moduleArgs as unknown as ModuleArgs); const { store } = moduleArgs; @@ -2063,7 +2062,6 @@ describe('stories API', () => { }); it('re-applies active filters on status changes without re-registering the status filter', async () => { - vi.mock('../stores/status'); fullStatusStore.unset(); const moduleArgs = createMockModuleArgs({}); const { api } = initStories(moduleArgs as unknown as ModuleArgs); @@ -2097,7 +2095,6 @@ describe('stories API', () => { }); it('a stream of status updates triggers a bounded number of index rebuilds', async () => { - vi.mock('../stores/status'); vi.useFakeTimers(); try { fullStatusStore.unset(); @@ -2135,7 +2132,6 @@ describe('stories API', () => { }); it('applies the last status update after a burst (trailing edge of the throttle)', async () => { - vi.mock('../stores/status'); vi.useFakeTimers(); try { fullStatusStore.unset(); @@ -2175,7 +2171,6 @@ describe('stories API', () => { }); it('does not overlap status-driven index rebuilds when a rebuild outlasts the throttle', async () => { - vi.mock('../stores/status'); vi.useFakeTimers(); try { fullStatusStore.unset(); @@ -2235,7 +2230,6 @@ describe('stories API', () => { }); it('keeps rebuilding the status-filtered index after a rebuild rejection', async () => { - vi.mock('../stores/status'); vi.useFakeTimers(); const warn = vi.spyOn(logger, 'warn').mockImplementation(() => {}); try { @@ -2287,7 +2281,6 @@ describe('stories API', () => { }); it('applies exclude logic: story with excluded status is hidden', async () => { - vi.mock('../stores/status'); const moduleArgs = createMockModuleArgs({}); const { api } = initStories(moduleArgs as unknown as ModuleArgs); const { store } = moduleArgs; @@ -2322,7 +2315,6 @@ describe('stories API', () => { }); it('story with no statuses is hidden when included filters are active', async () => { - vi.mock('../stores/status'); const moduleArgs = createMockModuleArgs({}); const { api } = initStories(moduleArgs as unknown as ModuleArgs); const { store } = moduleArgs; diff --git a/code/core/src/preview-api/modules/preview-web/WebView.test.ts b/code/core/src/preview-api/modules/preview-web/WebView.test.ts index 28393168f11f..9f0503e9ce3a 100644 --- a/code/core/src/preview-api/modules/preview-web/WebView.test.ts +++ b/code/core/src/preview-api/modules/preview-web/WebView.test.ts @@ -1,4 +1,5 @@ // @vitest-environment happy-dom +import '@testing-library/jest-dom/vitest'; import { beforeEach, describe, expect, it } from 'vitest'; import { WebView } from './WebView.ts'; diff --git a/code/e2e-sandbox/addon-onboarding.spec.ts b/code/e2e-sandbox/addon-onboarding.spec.ts index 3b7351b57dae..df7874261750 100644 --- a/code/e2e-sandbox/addon-onboarding.spec.ts +++ b/code/e2e-sandbox/addon-onboarding.spec.ts @@ -1,8 +1,4 @@ -import { readdir, rm } from 'node:fs/promises'; -import { homedir } from 'node:os'; - import { expect, test } from '@playwright/test'; -import { join } from 'pathe'; import process from 'process'; import { SbPage, hasOnboardingFeature } from './util.ts'; @@ -11,30 +7,6 @@ const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001'; const templateName = process.env.STORYBOOK_TEMPLATE_NAME || ''; const type = process.env.STORYBOOK_TYPE || 'dev'; -async function clearChecklistCache() { - const storybookCacheDir = join( - process.env.STORYBOOK_SANDBOX_DIR!, - 'node_modules', - '.cache', - 'storybook' - ); - const storybookCacheEntries = await readdir(storybookCacheDir, { withFileTypes: true }).catch( - () => [] - ); - - // Storybook scopes cache entries by version, so remove the checklist for any installed version. - await Promise.all( - storybookCacheEntries - .filter((entry) => entry.isDirectory()) - .map((entry) => - rm(join(storybookCacheDir, entry.name, 'default', 'checklist'), { - recursive: true, - force: true, - }) - ) - ); -} - test.describe('addon-onboarding', () => { test.skip(type === 'build', `Skipping addon tests for production Storybooks`); test.skip( @@ -46,14 +18,22 @@ test.describe('addon-onboarding', () => { `Skipping ${templateName}, whose onboarding coverage is carried by vue3-vite/default-ts.` ); test('the onboarding flow', async ({ page }) => { - // eslint-disable-next-line playwright/no-conditional-in-test - if (process.env.CI) { - await rm(join(homedir(), '.storybook', 'settings.json'), { force: true }); - await clearChecklistCache(); - } + test.setTimeout(60_000); - await page.goto(`${storybookUrl}/?path=/onboarding`); const sbPage = new SbPage(page, expect); + await page.goto(`${storybookUrl}/?path=/story/example-button--primary`); + await sbPage.waitUntilLoaded(); + await page.evaluate(async () => { + const { internal_checklistStore, internal_universalChecklistStore } = ( + window as unknown as { + __STORYBOOK_API__: typeof import('storybook/manager-api'); + } + ).__STORYBOOK_API__; + await internal_universalChecklistStore.readyPromise; + internal_checklistStore.reset('onboardingSurvey'); + }); + + await page.goto(`${storybookUrl}/?path=/onboarding`); await sbPage.waitUntilLoaded(); await expect(page.getByRole('heading', { name: 'Meet your new frontend' })).toBeVisible(); diff --git a/code/e2e-sandbox/util.ts b/code/e2e-sandbox/util.ts index 281220858e05..6cf471b56d3b 100644 --- a/code/e2e-sandbox/util.ts +++ b/code/e2e-sandbox/util.ts @@ -159,9 +159,8 @@ export class SbPage { async closeAnyPendingModal() { const popover = this.page.locator('[role="dialog"]'); if (await popover.isVisible()) { - await this.page.keyboard.press('Escape'); - await this.page.keyboard.press('Escape'); - await popover.waitFor({ state: 'hidden', timeout: 1000 }); + await popover.press('Escape'); + await this.expect(popover).toBeHidden(); } } diff --git a/code/frameworks/angular-vite/package.json b/code/frameworks/angular-vite/package.json index 1166ac9f5fe2..70114368460b 100644 --- a/code/frameworks/angular-vite/package.json +++ b/code/frameworks/angular-vite/package.json @@ -70,7 +70,7 @@ "vite": ">=8.0.0" }, "devDependencies": { - "@analogjs/vite-plugin-angular": "^2.5.2", + "@analogjs/vite-plugin-angular": "^2.7.5", "@angular-devkit/architect": "^0.2102.12", "@angular-devkit/build-angular": "^21.2.12", "@angular-devkit/core": "^21.2.12", @@ -92,7 +92,7 @@ "zone.js": "^0.16.2" }, "peerDependencies": { - "@analogjs/vite-plugin-angular": ">=2.7.3", + "@analogjs/vite-plugin-angular": ">=2.7.5", "@angular-devkit/architect": ">=0.2100.0 < 0.2300.0", "@angular-devkit/core": ">=21.0.0 < 23.0.0", "@angular/build": ">=21.0.0 < 23.0.0", diff --git a/code/frameworks/angular-vite/src/preset.test.ts b/code/frameworks/angular-vite/src/preset.test.ts index f06f5969e2b7..b432a16bfc90 100644 --- a/code/frameworks/angular-vite/src/preset.test.ts +++ b/code/frameworks/angular-vite/src/preset.test.ts @@ -91,6 +91,29 @@ const optionsWith = ( }, }) as unknown as Parameters[1]; +describe('Angular hash initialization', () => { + afterEach(() => { + vi.mocked(createRequire).mockRestore(); + }); + + it.each(['21.2.22', '22.1.0', '22.2.0'])( + 'initializes hashing when required by Angular %s', + async (version) => { + const initializeHash = vi.fn().mockResolvedValue(undefined); + const angularBuildPath = resolve('/angular/build/package.json'); + const angularRequire = vi.fn((path: string) => + path === angularBuildPath ? { version } : { initializeHash } + ); + Object.assign(angularRequire, { resolve: () => angularBuildPath }); + vi.mocked(createRequire).mockReturnValue(angularRequire as unknown as NodeJS.Require); + + await viteFinal({}, optionsWith({}, { experimentalDocgenServer: true })); + + expect(initializeHash).toHaveBeenCalledTimes(version === '22.2.0' ? 1 : 0); + } + ); +}); + function runConfig(stylePreprocessorOptions: Record | undefined) { const options = { configDir: resolve(WORKSPACE_ROOT, '.storybook'), diff --git a/code/frameworks/angular-vite/src/preset.ts b/code/frameworks/angular-vite/src/preset.ts index 271bb15b00fe..685f505b8014 100644 --- a/code/frameworks/angular-vite/src/preset.ts +++ b/code/frameworks/angular-vite/src/preset.ts @@ -106,6 +106,15 @@ export const viteFinal = async (config: UserConfig, options: Options & Standalon const { mergeConfig, normalizePath } = await import('vite'); const { default: angular } = await import('@analogjs/vite-plugin-angular'); + const angularRequire = createRequire(import.meta.resolve('@analogjs/vite-plugin-angular')); + const angularBuildPath = angularRequire.resolve('@angular/build/package.json'); + const { version } = angularRequire(angularBuildPath) as { version: string }; + const [major, minor = 0] = version.split('.').map(Number); + // Angular 22.2 requires hash initialization before Analog's JavaScript transformer runs. + if (major === 22 && minor >= 2) { + await angularRequire(join(dirname(angularBuildPath), 'src/utils/hash.js')).initializeHash(); + } + const framework = await options.presets.apply('framework'); const frameworkOptions: FrameworkOptions | undefined = typeof framework === 'string' ? undefined : framework.options; diff --git a/code/lib/create-storybook/src/services/ProjectTypeService.test.ts b/code/lib/create-storybook/src/services/ProjectTypeService.test.ts index 299a04db1203..eef4a418f58e 100644 --- a/code/lib/create-storybook/src/services/ProjectTypeService.test.ts +++ b/code/lib/create-storybook/src/services/ProjectTypeService.test.ts @@ -29,7 +29,6 @@ describe('ProjectTypeService', () => { it('logs a helpful message when framework cannot be detected', async () => { const service = new ProjectTypeService(pm); const options = { html: false } as unknown as CommandOptions; - // @ts-expect-error accessing private for test vi.spyOn(service, 'detectProjectType').mockResolvedValue(ProjectType.UNDETECTED); await expect(service.autoDetectProjectType(options)).rejects.toThrowError( diff --git a/code/package.json b/code/package.json index b7d25b631c0b..b57ca8d9b322 100644 --- a/code/package.json +++ b/code/package.json @@ -93,10 +93,10 @@ "@types/react-dom": "^18.0.11", "@vitejs/plugin-react": "^4.3.2", "@vitejs/plugin-vue": "^4.4.0", - "@vitest/browser": "^4.1.6", - "@vitest/browser-playwright": "^4.1.6", - "@vitest/coverage-istanbul": "^4.1.6", - "@vitest/coverage-v8": "^4.1.6", + "@vitest/browser": "^5.0.0", + "@vitest/browser-playwright": "^5.0.0", + "@vitest/coverage-istanbul": "^5.0.0", + "@vitest/coverage-v8": "^5.0.0", "chromatic": "^18.0.1", "create-storybook": "workspace:*", "danger": "^13.0.4", @@ -123,7 +123,7 @@ "uuid": "^11.1.0", "vite": "^7.0.4", "vite-plugin-inspect": "^11.0.0", - "vitest": "^4.1.6", + "vitest": "^5.0.0", "wait-on": "^8.0.3" }, "devDependencies": { diff --git a/code/renderers/react/src/componentManifest/generator.react-docgen-typescript.test.ts b/code/renderers/react/src/componentManifest/generator.react-docgen-typescript.test.ts index fa05cfec2701..14d8bbe41814 100644 --- a/code/renderers/react/src/componentManifest/generator.react-docgen-typescript.test.ts +++ b/code/renderers/react/src/componentManifest/generator.react-docgen-typescript.test.ts @@ -18,34 +18,38 @@ type ManifestComponentWithReactDocgenTypescript = { reactDocgenTypescript?: ComponentDocWithExportName; }; -test.sequential('manifests uses the referenced app tsconfig for react-docgen-typescript in Vite-style projects', async () => { - invalidateCache(); - invalidateParser(); - - const tempDir = createTempProject(); - const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tempDir); - - try { - const result = await manifests(undefined, createManifestOptions()); - const button = result?.components?.components?.['example-button'] as - | ManifestComponentWithReactDocgenTypescript - | undefined; - - expect(button?.error).toBeUndefined(); - expect(button?.reactDocgenTypescript?.displayName).toBe('Button'); - expect(button?.reactDocgenTypescript?.props.label).toMatchObject({ - required: true, - }); - expect(button?.reactDocgenTypescript?.props.primary).toMatchObject({ - required: false, - }); - } finally { - cwdSpy.mockRestore(); +test( + 'manifests uses the referenced app tsconfig for react-docgen-typescript in Vite-style projects', + { concurrent: false }, + async () => { invalidateCache(); invalidateParser(); - rmSync(tempDir, { recursive: true, force: true }); + + const tempDir = createTempProject(); + const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tempDir); + + try { + const result = await manifests(undefined, createManifestOptions()); + const button = result?.components?.components?.['example-button'] as + | ManifestComponentWithReactDocgenTypescript + | undefined; + + expect(button?.error).toBeUndefined(); + expect(button?.reactDocgenTypescript?.displayName).toBe('Button'); + expect(button?.reactDocgenTypescript?.props.label).toMatchObject({ + required: true, + }); + expect(button?.reactDocgenTypescript?.props.primary).toMatchObject({ + required: false, + }); + } finally { + cwdSpy.mockRestore(); + invalidateCache(); + invalidateParser(); + rmSync(tempDir, { recursive: true, force: true }); + } } -}); +); function createManifestOptions() { const manifestEntries: IndexEntry[] = [ diff --git a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts index d6752601ba64..0957f7ccdf24 100644 --- a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts +++ b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts @@ -1,5 +1,5 @@ // @vitest-environment happy-dom -/// +import '@testing-library/jest-dom/vitest'; import { cleanup, render, screen } from '@testing-library/vue'; import { afterEach, describe, expect, it, vi } from 'vitest'; diff --git a/docs/writing-tests/integrations/vitest-addon/index.mdx b/docs/writing-tests/integrations/vitest-addon/index.mdx index 3c8341325a90..42fd584290b8 100644 --- a/docs/writing-tests/integrations/vitest-addon/index.mdx +++ b/docs/writing-tests/integrations/vitest-addon/index.mdx @@ -282,11 +282,17 @@ If your stories use assets in the public directory and you're not using the defa ### How do I isolate Storybook tests from others? -Some projects might contain a `test` property in their Vite configuration. Because the Vitest configuration used by this plugin extends that Vite config, the `test` properties are merged. This lack of isolation can cause issues with your Storybook tests. +The generated Storybook test project uses `extends: true` to share your root Vite configuration, including framework plugins and aliases. It also inherits root-level test settings, so unit-only setup files at the root can interfere with your stories. -To isolate your Storybook tests from other tests, you need to move the `test` property from your Vite configuration to the Vitest configuration. The Vitest config used by the plugin can then safely extend your Vite config without merging the `test` property. +Move unit-only settings, such as `setupFiles` and `environment`, into the unit-test project inside `test.projects`. Keep shared Vite plugins and aliases at the root. When the addon converts a single test configuration into projects, it moves project-specific test settings into the unit project automatically. -Additionally, we recommend using a [test project](#example-configuration-files) if you're using Vitest ≥ 4.0, or a workspace for previous versions to define separate configurations for your Storybook tests and other tests. This ensures each can be run either in isolation or together, depending on your needs. +If you need to isolate inherited project configuration, set `extends: false` on the Storybook project and explicitly configure its required framework plugins and aliases, either on that project or through Storybook's [`viteFinal`](../../../api/main-config/main-config-vite-final.mdx). + +Root `globalSetup` and root plugin hooks such as `config` still run even with `extends: false`. Move unit-only global setup into the unit-test project, and keep root plugin hooks safe for all projects. + +Coverage is a [global Vitest option](https://vitest.dev/guide/projects), so `extends: false` does not isolate it. When you run `vitest --project=storybook --coverage`, Vitest still uses the root `test.coverage` configuration. + +Use a [test project](#example-configuration-files) if you're using Vitest ≥ 4.0, or a workspace for previous versions, to define separate configurations for Storybook tests and other tests. This allows each suite to run independently or together. ### Why do we recommend browser mode? diff --git a/package.json b/package.json index aeaf54fdffa5..2c1479d026f4 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "oxfmt": "^0.41.0", "std-env": "^4.0.0", "vite": "^7.0.4", - "vitest": "^4.1.6" + "vitest": "^5.0.0" }, "packageManager": "yarn@4.18.0" } diff --git a/scripts/package.json b/scripts/package.json index 630297178197..1920b03513f4 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -76,7 +76,7 @@ "@types/uuid": "^9.0.8", "@types/wait-on": "^5.3.4", "@types/window-size": "^1.1.4", - "@vitest/coverage-v8": "^4.1.6", + "@vitest/coverage-v8": "^5.0.0", "chromatic": "^18.0.1", "citty": "^0.2.1", "codecov": "^3.8.1", @@ -151,7 +151,7 @@ "typescript": "^6.0.3", "typescript-native": "npm:typescript@^7.0.2", "uuid": "^9.0.1", - "vitest": "^4.1.6", + "vitest": "^5.0.0", "wait-on": "^8.0.3", "window-size": "^1.1.1", "yaml": "^2.8.1", diff --git a/scripts/vitest-compatibility/README.md b/scripts/vitest-compatibility/README.md new file mode 100644 index 000000000000..df0482ff31ba --- /dev/null +++ b/scripts/vitest-compatibility/README.md @@ -0,0 +1,112 @@ +# Vitest addon runtime compatibility + +Run with the Node version in `.nvmrc`, npm, and the repository's Yarn installation. +Compile the addon, framework, renderer, builder, and core before preparing a fixture: + +```sh +yarn nx run-many -t compile --projects=addon-vitest,react-vite +node scripts/vitest-compatibility/prepare.ts /tmp/storybook-vitest-5 5.0.1 react +cd /tmp/storybook-vitest-5 +npx playwright install chromium +node run.ts +``` + +Repeat with separate directories and versions `4.0.0` and `5.0.0`. Preparation packages +local build output into tarballs and installs real, independent dependencies. It does +not link the monorepo's node_modules. `--legacy-peer-deps` prevents npm from installing +optional, unrelated Storybook peers; the required runtime peers are pinned explicitly. +Keep the generated package-lock.json with the evidence when reproducing an installation. + +The harness starts the published addon child-process entry point, bridges real +UniversalStores, and runs actual Chromium stories. It checks startup without a run, +all stories, exact child selection containing regex punctuation, parent selection, +coverage off/on/off, and automatic watch failure/recovery after component edits. +Native V8 execution counts verify three `init()` calls on Vitest 4 or three +`standalone()` calls on Vitest 5, including both coverage restarts. A separate CLI +`vitest run --project=storybook --coverage` invocation verifies coverage output. + +The intentionally failing watch run must fail before recovery passes. Its diagnostic +output is expected. Harness assertion failures produce a nonzero exit status. + +Artifacts stay in the fixture: environment.json, package-lock.json, results.json, +lifecycle.json, native-coverage/, and coverage-cli/. Run browser checks outside an OS +sandbox that prevents Chromium from launching. Do not treat a blocked launch as a +product failure or a passing test. + +## Framework configuration migration + +Compile `vue3-vite` and `svelte-vite` before preparing the corresponding fixtures. +From the repository root: + +```sh +node scripts/vitest-compatibility/prepare.ts /tmp/storybook-vitest-vue 5.0.1 vue3 +node scripts/vitest-compatibility/framework-config.ts /tmp/storybook-vitest-vue vue3 existing +cd /tmp/storybook-vitest-vue +node node_modules/vitest/vitest.mjs run --project=storybook +``` + +Repeat with mode `single`, and with framework `svelte` in another directory. The +configuration comes from the actual updater and template. Real Vue/Svelte compiler +plugins and an alias live in shared configuration. Unit setup deliberately throws +if it leaks into the Storybook project. Explicit dependency prebundling avoids +cold-cache reloads interrupting the first browser run. + +## Storybook UI + +After a successful React `run.ts`, add `@storybook/addon-vitest` to the fixture's +`.storybook/main.js` addons array. Start the UI from the fixture directory: + +```sh +node node_modules/storybook/dist/bin/dispatcher.js dev --ci --port 6026 +``` + +In another terminal in that directory, run `node ui.ts http://localhost:6026`. +It selects a nested story, starts a test run through the UI, enables watch mode, +edits the component to fail, restores it, and verifies displayed failure/recovery. +It saves a Playwright trace and screenshots. Exact child/parent filtering is asserted +by `run.ts` against the real backend's completed run results. + +## Coverage and mutation measurements + +Collect source coverage from the repository root: + +```sh +yarn --cwd code/addons/vitest vitest run --coverage --coverage.provider=v8 \ + --coverage.include=src/node/vitest-manager.ts \ + --coverage.include=src/node/coverage-reporter.ts \ + --coverage.include=src/vitest-plugin/index.ts \ + --coverage.reporter=json --coverage.reportsDirectory=/tmp/vitest-source-coverage +node scripts/vitest-compatibility/quality.ts /tmp/vitest-source-coverage/coverage-final.json /tmp/vitest-quality.json +``` + +The report states its complexity and coverage conventions. It measures complete +changed methods, including their pre-existing branches. Source coverage here is +from unit tests; native/browser evidence is reported separately, not silently merged. +The score formula is `complexity² × (1 − coverage)³ + complexity`. + +Install pinned mutation tooling outside the repository, then run from the repository: + +```sh +npm install --prefix /tmp/storybook-mutation-tools --no-audit --no-fund @stryker-mutator/core@10.0.0 +node scripts/vitest-compatibility/mutation.ts /tmp/storybook-mutation-tools /tmp/storybook-mutation-results +``` + +This uses the command runner with coverage analysis disabled. Stryker's Vitest +runner currently constructs space-separated test-name filters, which do not select +Vitest 5 nested tests correctly. The command runner executes a fresh focused suite +for every mutant. The sandbox build compiles the mutated coverage reporter, so the +native loader tests also exercise mutations. Sandboxes stay outside the repository +to avoid NX discovering duplicate projects. Keep timeouts separate from killed +mutants when interpreting the JSON report; survivors are not waived or hidden. + +After successful unmodified compatibility runs, execute explicit built-artifact +regression probes: + +```sh +node scripts/vitest-compatibility/regressions.ts /tmp/storybook-vitest-4 /tmp/storybook-vitest-5 /tmp/storybook-regressions +``` + +These revert individual compatibility changes in disposable fixture packages and +require the expected failure. They restore each package in `finally`. They supplement +the generated mutation report; they do not establish a zero-survivor score for whole +functions or substitute for a complete base-branch run. diff --git a/scripts/vitest-compatibility/framework-config.ts b/scripts/vitest-compatibility/framework-config.ts new file mode 100644 index 000000000000..e06363e8b421 --- /dev/null +++ b/scripts/vitest-compatibility/framework-config.ts @@ -0,0 +1,54 @@ +import assert from 'node:assert/strict'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; + +import { babelParse, generate } from 'storybook/internal/babel'; + +import { updateConfigFile } from '../../code/addons/vitest/src/updateVitestFile.ts'; + +const [directory, framework, mode = 'single'] = process.argv.slice(2); +assert(directory && ['vue3', 'svelte'].includes(framework)); +const plugin = framework === 'vue3' ? '@vitejs/plugin-vue' : '@sveltejs/vite-plugin-svelte'; +const extension = framework === 'vue3' ? 'vue' : 'svelte'; +await mkdir(join(directory, '.storybook'), { recursive: true }); +await writeFile( + join(directory, '.storybook/main.js'), + `export default { stories: ['../*.stories.js'], addons: [], framework: '@storybook/${framework}-vite', core: { disableTelemetry: true } };` +); +await writeFile(join(directory, '.storybook/preview.js'), 'export default {};'); +await writeFile(join(directory, 'label.js'), "export default 'Ready';"); +await writeFile( + join(directory, `Button.${extension}`), + framework === 'vue3' + ? `` + : `` +); +await writeFile( + join(directory, 'Button.stories.js'), + `import Button from './Button.${extension}'; +import { expect } from 'storybook/test'; +export default { title: 'Framework', component: Button }; +export const Primary = { play: async ({ canvas }) => { await expect(canvas.getByRole('button')).toHaveTextContent('Ready'); } };` +); +await writeFile( + join(directory, 'unit-only-setup.js'), + `throw new Error('Unit setup leaked into the Storybook project');` +); +const target = babelParse(`import { defineConfig } from 'vitest/config'; +import ${framework === 'vue3' ? 'frameworkPlugin' : '{ svelte as frameworkPlugin }'} from '${plugin}'; +export default defineConfig({ + plugins: [frameworkPlugin()], + optimizeDeps: { include: ${JSON.stringify(framework === 'vue3' ? ['vue'] : ['svelte', 'svelte/internal/client', 'svelte/internal/flags/legacy'])} }, + resolve: { alias: { 'fixture-label': ${JSON.stringify(resolve(directory, 'label.js'))} } }, + test: { ${mode === 'existing' ? "projects: [{ extends: true, test: { name: 'unit', setupFiles: ['./unit-only-setup.js'] } }]" : "name: 'unit', setupFiles: ['./unit-only-setup.js']"} } +});`); +const source = babelParse( + ( + await readFile( + new URL('../../code/addons/vitest/templates/vitest.config.4.template.ts', import.meta.url), + 'utf8' + ) + ).replace('CONFIG_DIR', '.storybook') +); +assert(updateConfigFile(source, target)); +await writeFile(join(directory, 'vitest.config.mjs'), generate(target).code); diff --git a/scripts/vitest-compatibility/mutation-build.ts b/scripts/vitest-compatibility/mutation-build.ts new file mode 100644 index 000000000000..ea3b958790df --- /dev/null +++ b/scripts/vitest-compatibility/mutation-build.ts @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import { symlink } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +assert(process.env.STORYBOOK_MUTATION_ROOT); +await symlink( + resolve(process.env.STORYBOOK_MUTATION_ROOT, 'node_modules'), + resolve('node_modules'), + 'junction' +); +const { build } = await import('esbuild'); +await symlink( + resolve(process.env.STORYBOOK_MUTATION_ROOT, 'code/core/dist'), + resolve('code/core/dist'), + 'dir' +); +await build({ + entryPoints: ['code/addons/vitest/src/node/coverage-reporter.ts'], + outfile: 'code/addons/vitest/dist/node/coverage-reporter.js', + bundle: true, + packages: 'external', + format: 'esm', + platform: 'node', + target: 'node22', +}); diff --git a/scripts/vitest-compatibility/mutation.ts b/scripts/vitest-compatibility/mutation.ts new file mode 100644 index 000000000000..78c49d93a288 --- /dev/null +++ b/scripts/vitest-compatibility/mutation.ts @@ -0,0 +1,91 @@ +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { once } from 'node:events'; +import { createWriteStream } from 'node:fs'; +import { finished } from 'node:stream/promises'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { resolve, join } from 'node:path'; + +import ts from 'typescript'; + +const [toolsDirectory, outputDirectory] = process.argv.slice(2); +assert( + toolsDirectory && outputDirectory, + 'Usage: node scripts/vitest-compatibility/mutation.ts TOOLS_DIRECTORY OUTPUT_DIRECTORY' +); +const root = resolve(import.meta.dirname, '../..'); +const output = resolve(outputDirectory); +await mkdir(output, { recursive: true }); +const targets = [ + ['code/addons/vitest/src/node/vitest-manager.ts', ['startVitest', 'buildStoryTestNamePattern']], + ['code/addons/vitest/src/vitest-plugin/index.ts', ['config']], + ['code/addons/vitest/src/node/coverage-reporter.ts', ['constructor', 'onSummary']], +] as const; +const mutate: string[] = []; +for (const [file, names] of targets) { + const source = ts.createSourceFile( + file, + await readFile(join(root, file), 'utf8'), + ts.ScriptTarget.Latest, + true + ); + function visit(node: ts.Node) { + const name = ts.isConstructorDeclaration(node) + ? 'constructor' + : ts.isMethodDeclaration(node) + ? node.name.getText(source) + : undefined; + if (name && (names as readonly string[]).includes(name)) { + const start = source.getLineAndCharacterOfPosition(node.getStart()); + const end = source.getLineAndCharacterOfPosition(node.end); + mutate.push(`${file}:${start.line + 1}:${start.character}-${end.line + 1}:${end.character}`); + } + ts.forEachChild(node, visit); + } + visit(source); +} +const configuration = { + testRunner: 'command', + plugins: [] as string[], + coverageAnalysis: 'off', + concurrency: 2, + timeoutMS: 10000, + commandRunner: { + command: + 'node node_modules/vitest/vitest.mjs run --config scripts/vitest-compatibility/vitest.mutation.config.ts', + }, + buildCommand: 'node scripts/vitest-compatibility/mutation-build.ts', + mutate, + reporters: ['clear-text', 'json'], + jsonReporter: { fileName: join(output, 'mutation.json') }, + tempDirName: join(output, 'sandboxes'), + ignorePatterns: [ + '**/dist/**', + '**/node_modules/**', + '.git/**', + '.yarn/**', + '.nx/**', + 'docs/**', + 'test-storybooks/**', + ], +}; +const configFile = join(output, 'stryker.config.json'); +await writeFile(configFile, JSON.stringify(configuration, null, 2)); +const log = createWriteStream(join(output, 'mutation.log')); +const child = spawn( + process.execPath, + [resolve(toolsDirectory, 'node_modules/@stryker-mutator/core/bin/stryker.js'), 'run', configFile], + { + cwd: root, + env: { ...process.env, STORYBOOK_MUTATION_ROOT: root }, + stdio: ['ignore', 'pipe', 'pipe'], + timeout: 30 * 60 * 1000, + } +); +child.stdout.pipe(log, { end: false }); +child.stderr.pipe(log, { end: false }); +const [code] = await once(child, 'close'); +log.end(); +await finished(log); +assert.equal(code, 0, `Mutation run failed; see ${join(output, 'mutation.log')}`); +process.stdout.write(`Mutation report: ${join(output, 'mutation.json')}\n`); diff --git a/scripts/vitest-compatibility/prepare.ts b/scripts/vitest-compatibility/prepare.ts new file mode 100644 index 000000000000..49028708cd4c --- /dev/null +++ b/scripts/vitest-compatibility/prepare.ts @@ -0,0 +1,118 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { cp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; +import { resolve, join } from 'node:path'; + +const root = resolve(import.meta.dirname, '../..'); +assert.equal(process.versions.node, (await readFile(join(root, '.nvmrc'), 'utf8')).trim()); +const [destination, version = '5.0.1', framework = 'react'] = process.argv.slice(2); +assert( + destination, + 'Usage: node scripts/vitest-compatibility/prepare.ts DIRECTORY VERSION FRAMEWORK' +); +assert(['react', 'vue3', 'svelte'].includes(framework)); +assert(/^[45]\./.test(version), 'Supported compatibility versions are Vitest 4 and 5'); +const directory = resolve(destination); +type Manifest = { + name: string; + version: string; + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; + scripts?: Record; +}; +const packages = new Map(); +for (const category of ['.', 'addons', 'builders', 'frameworks', 'renderers', 'lib']) { + const parent = join(root, 'code', category); + for (const entry of await readdir(parent, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const path = join(parent, entry.name); + try { + const manifest = JSON.parse(await readFile(join(path, 'package.json'), 'utf8')); + packages.set(manifest.name, { path, manifest }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + } +} +await mkdir(directory, { recursive: true }); +const dependencies: Record = {}; +async function pack(name: string) { + if (dependencies[name]) return; + const pkg = packages.get(name); + assert(pkg, `Missing local package ${name}`); + const staging = join(directory, '.packages', name.replaceAll('/', '-')); + await mkdir(staging, { recursive: true }); + dependencies[name] = ''; + const manifest = structuredClone(pkg.manifest); + delete manifest.devDependencies; + delete manifest.scripts; + for (const field of ['dependencies', 'peerDependencies', 'optionalDependencies'] as const) { + const packageDependencies = manifest[field]; + if (!packageDependencies) continue; + for (const [dependency, range] of Object.entries(packageDependencies)) { + if (String(range).startsWith('workspace:')) { + packageDependencies[dependency] = packages.get(dependency)!.manifest.version; + await pack(dependency); + } + } + } + await writeFile(join(staging, 'package.json'), JSON.stringify(manifest, null, 2)); + await cp(join(pkg.path, 'dist'), join(staging, 'dist'), { recursive: true }); + for (const entry of await readdir(pkg.path)) { + if ( + ['templates', 'assets', 'static', 'input'].includes(entry) || + /\.(mjs|cjs|js|ts)$/.test(entry) + ) { + await cp(join(pkg.path, entry), join(staging, entry), { recursive: true }); + } + } + const result = JSON.parse( + execFileSync('npm', ['pack', '--ignore-scripts', '--json'], { cwd: staging, encoding: 'utf8' }) + ); + dependencies[name] = `file:${join(staging, result[0].filename)}`; +} +await pack('@storybook/addon-vitest'); +await pack(`@storybook/${framework}-vite`); +Object.assign(dependencies, { + vitest: version, + '@vitest/coverage-v8': version, + '@vitest/browser-playwright': version, + playwright: '1.58.2', + vite: version.startsWith('5.') ? '7.3.1' : '6.3.6', + typescript: '6.0.3', + ...(framework === 'react' + ? { react: '18.3.1', 'react-dom': '18.3.1', '@vitejs/plugin-react': '4.7.0' } + : {}), + ...(framework === 'vue3' ? { vue: '3.5.30', '@vitejs/plugin-vue': '6.0.5' } : {}), + ...(framework === 'svelte' ? { svelte: '5.53.12', '@sveltejs/vite-plugin-svelte': '6.2.4' } : {}), +}); +await writeFile( + join(directory, 'package.json'), + JSON.stringify( + { name: 'storybook-vitest-compatibility', private: true, type: 'module', dependencies }, + null, + 2 + ) +); +execFileSync('npm', ['install', '--legacy-peer-deps', '--no-audit', '--no-fund'], { + cwd: directory, + stdio: 'inherit', +}); +await cp(join(import.meta.dirname, 'run.ts'), join(directory, 'run.ts')); +await cp(join(import.meta.dirname, 'ui.ts'), join(directory, 'ui.ts')); +await writeFile( + join(directory, 'environment.json'), + JSON.stringify( + { + sha: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(), + node: process.versions.node, + version, + framework, + dependencies, + }, + null, + 2 + ) +); diff --git a/scripts/vitest-compatibility/quality.ts b/scripts/vitest-compatibility/quality.ts new file mode 100644 index 000000000000..aeb185ca076f --- /dev/null +++ b/scripts/vitest-compatibility/quality.ts @@ -0,0 +1,118 @@ +import assert from 'node:assert/strict'; +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +import ts from 'typescript'; + +const [coverageFile, output = 'vitest-quality.json'] = process.argv.slice(2); +assert( + coverageFile, + 'Usage: node scripts/vitest-compatibility/quality.ts COVERAGE_JSON OUTPUT_JSON' +); +const coverage: Record< + string, + { + statementMap: Record; + s: Record; + } +> = JSON.parse(await readFile(coverageFile, 'utf8')); +const targets = [ + ['code/addons/vitest/src/node/vitest-manager.ts', 'startVitest'], + ['code/addons/vitest/src/node/vitest-manager.ts', 'buildStoryTestNamePattern'], + ['code/addons/vitest/src/vitest-plugin/index.ts', 'config'], + ['code/addons/vitest/src/node/coverage-reporter.ts', 'constructor'], + ['code/addons/vitest/src/node/coverage-reporter.ts', 'onSummary'], +]; +const report = []; +for (const [file, name] of targets) { + const source = ts.createSourceFile( + file, + await readFile(file, 'utf8'), + ts.ScriptTarget.Latest, + true + ); + const matches: (ts.MethodDeclaration | ts.ConstructorDeclaration)[] = []; + function find(node: ts.Node) { + if ( + (ts.isMethodDeclaration(node) && node.name.getText(source) === name) || + (ts.isConstructorDeclaration(node) && name === 'constructor') + ) + matches.push(node); + ts.forEachChild(node, find); + } + find(source); + assert.equal(matches.length, 1, `${file}:${name}`); + const method = matches[0]; + let complexity = 1; + function count(node: ts.Node) { + if (node !== method && ts.isFunctionLike(node)) return; + if ( + ts.isIfStatement(node) || + ts.isConditionalExpression(node) || + ts.isForStatement(node) || + ts.isForOfStatement(node) || + ts.isForInStatement(node) || + ts.isWhileStatement(node) || + ts.isDoStatement(node) || + ts.isCatchClause(node) || + ts.isCaseClause(node) + ) + complexity++; + if ( + ts.isBinaryExpression(node) && + [ + ts.SyntaxKind.AmpersandAmpersandToken, + ts.SyntaxKind.BarBarToken, + ts.SyntaxKind.QuestionQuestionToken, + ts.SyntaxKind.AmpersandAmpersandEqualsToken, + ts.SyntaxKind.BarBarEqualsToken, + ts.SyntaxKind.QuestionQuestionEqualsToken, + ].includes(node.operatorToken.kind) + ) + complexity++; + if ( + (ts.isPropertyAccessExpression(node) || + ts.isElementAccessExpression(node) || + ts.isCallExpression(node)) && + node.questionDotToken + ) + complexity++; + ts.forEachChild(node, count); + } + count(method); + const start = source.getLineAndCharacterOfPosition(method.getStart()).line + 1; + const end = source.getLineAndCharacterOfPosition(method.end).line + 1; + const data = coverage[resolve(file)]; + const statements = + data && + Object.entries(data.statementMap).filter( + ([, location]) => location.start.line >= start && location.end.line <= end + ); + const covered = statements?.filter(([id]) => data.s[id] > 0).length; + const ratio = statements?.length ? covered / statements.length : undefined; + report.push({ + file, + name, + start, + end, + complexity, + coveredStatements: covered, + totalStatements: statements?.length, + coverage: ratio, + crap: ratio === undefined ? null : complexity ** 2 * (1 - ratio) ** 3 + complexity, + complexityFloorPasses: complexity < 25, + }); +} +await writeFile( + output, + JSON.stringify( + { + convention: + 'Cyclomatic decisions including short-circuit and optional-chain branches, excluding nested functions; Istanbul statement coverage over the complete method; CRAP = complexity^2 * (1 - coverage)^3 + complexity.', + report, + }, + null, + 2 + ) +); +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); diff --git a/scripts/vitest-compatibility/regressions.ts b/scripts/vitest-compatibility/regressions.ts new file mode 100644 index 000000000000..b9b40eddcef8 --- /dev/null +++ b/scripts/vitest-compatibility/regressions.ts @@ -0,0 +1,77 @@ +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { promisify } from 'node:util'; + +const [vitest4, vitest5, outputDirectory] = process.argv.slice(2); +assert( + vitest4 && vitest5 && outputDirectory, + 'Usage: node scripts/vitest-compatibility/regressions.ts VITEST4_DIRECTORY VITEST5_DIRECTORY OUTPUT_DIRECTORY' +); +const output = resolve(outputDirectory); +await mkdir(output, { recursive: true }); +const probes = [ + { + name: 'vitest5-init', + directory: vitest5, + file: 'node/vitest.js', + pattern: + /typeof this\.vitest\.standalone == "function" \? await this\.vitest\.standalone\(\) : await this\.vitest\.init\(\)/, + replacement: 'await this.vitest.init()', + expected: /Deprecated Vitest.init/, + }, + { + name: 'vitest5-separator', + directory: vitest5, + file: 'node/vitest.js', + pattern: /this\.testNameSeparator = Number\.parseInt\(version, 10\) >= 5 \? " > " : " "/, + replacement: 'this.testNameSeparator = " "', + expected: /0 !== 1/, + }, + { + name: 'vitest5-default-export', + directory: vitest5, + file: 'node/coverage-reporter.js', + pattern: / coverage_reporter_default as default,\n/, + replacement: '', + expected: /not a constructor|default export|Could not load|Failed to run tests/, + }, + { + name: 'vitest4-commonjs-export', + directory: vitest4, + file: 'node/coverage-reporter.js', + pattern: / StorybookCoverageReporter as "module.exports"/, + replacement: '', + expected: /not a constructor|Failed to run tests/, + }, +]; +const results = []; +for (const probe of probes) { + const file = join(probe.directory, 'node_modules/@storybook/addon-vitest/dist', probe.file); + const original = await readFile(file, 'utf8'); + assert(probe.pattern.test(original), `Build pattern missing: ${probe.name}`); + try { + await writeFile(file, original.replace(probe.pattern, probe.replacement)); + let failure: { code?: string | number; stdout?: string; stderr?: string } | undefined; + try { + await promisify(execFile)(process.execPath, ['run.ts'], { + cwd: probe.directory, + timeout: 120000, + maxBuffer: 10 * 1024 * 1024, + }); + } catch (error) { + failure = error as typeof failure; + } + assert(failure, `Regression survived: ${probe.name}`); + assert.equal(failure.code, 1, `Unexpected process failure: ${probe.name}`); + const log = (failure.stdout ?? '') + (failure.stderr ?? ''); + await writeFile(join(output, `${probe.name}.log`), log); + assert.match(log, probe.expected, `Unexpected failure: ${probe.name}`); + results.push({ name: probe.name, status: 'caught', exitCode: failure.code }); + process.stdout.write(`PASS regression probe: ${probe.name}\n`); + } finally { + await writeFile(file, original); + } +} +await writeFile(join(output, 'regressions.json'), JSON.stringify(results, null, 2)); diff --git a/scripts/vitest-compatibility/run.ts b/scripts/vitest-compatibility/run.ts new file mode 100644 index 000000000000..3db508fc39a3 --- /dev/null +++ b/scripts/vitest-compatibility/run.ts @@ -0,0 +1,284 @@ +import assert from 'node:assert/strict'; +import { once } from 'node:events'; +import { promisify } from 'node:util'; +import type { CurrentRun, StoreState, StoreEvent } from '../../code/addons/vitest/src/types.ts'; +import { fork, execFile, type ChildProcess } from 'node:child_process'; +import { mkdir, readFile, writeFile, readdir, rm } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { ChannelEvent } from 'storybook/internal/channels'; +import { + internal_UniversalStore as UniversalStore, + prepareHeadlessUniversalStores, + internal_universalStatusStore, + internal_universalTestProviderStore, +} from 'storybook/internal/core-server'; +import { loadCsf } from 'storybook/internal/csf-tools'; +import { storeOptions, STORY_INDEX_CHANNEL_EVENT_NAME } from '@storybook/addon-vitest/constants'; + +const environment = JSON.parse(await readFile('environment.json', 'utf8')); +const version = JSON.parse(await readFile('node_modules/vitest/package.json', 'utf8')).version; +assert.equal(version, environment.version); +assert(/^[45]\./.test(version), 'Supported compatibility versions are Vitest 4 and 5'); +assert.equal(process.versions.node, '22.22.3'); +const configDir = resolve('.storybook'); +await mkdir(configDir, { recursive: true }); +await writeFile( + '.storybook/main.js', + `export default { stories: ['../*.stories.jsx'], addons: [], features: { experimentalTestSyntax: true }, framework: '@storybook/react-vite', core: { disableTelemetry: true } };` +); +await writeFile( + '.storybook/preview.js', + `import { __definePreview } from '@storybook/react'; export default __definePreview({});` +); +await writeFile( + 'Button.jsx', + `import React from 'react'; +export const unused = () => { + return 'Uncovered'; +}; +export const Button = () => ;` +); +const stories = `import { Button } from './Button.jsx'; +import preview from './.storybook/preview.js'; +import { expect } from 'storybook/test'; +const meta = preview.meta({ title: 'Compatibility', component: Button }); +export const Primary = meta.story({ name: 'Primary (button)' }); +Primary.test('renders [ready]', async ({ canvas }) => { await expect(canvas.getByRole('button')).toHaveTextContent('Ready'); }); +Primary.test('renders [ready] again', async ({ canvas }) => { await expect(canvas.getByRole('button')).toHaveTextContent('Ready'); }); +export const PrimaryMobile = meta.story({ name: 'Primary (button) Mobile', play: async ({ canvas }) => { await expect(canvas.getByRole('button')).toHaveTextContent('Ready'); } }); +`; +await writeFile('Button.stories.jsx', stories); +await writeFile( + 'vitest.config.mjs', + `import { defineConfig } from 'vitest/config'; +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; +import { playwright } from '@vitest/browser-playwright'; +export default defineConfig({ test: { coverage: { provider: 'v8', include: ['Button.jsx'], watermarks: { statements: [90, 100] }, reporter: ['json-summary'], reportsDirectory: './coverage-cli' }, projects: [{ extends: true, optimizeDeps: { include: ['@storybook/react'] }, plugins: [storybookTest({ configDir: ${JSON.stringify(configDir)} })], test: { name: 'storybook', browser: { enabled: true, headless: true, provider: playwright(), instances: [{ browser: 'chromium' }] } } }] } });` +); +let child: ChildProcess | undefined; +const channel = prepareHeadlessUniversalStores(); +const inputs = loadCsf(stories, { + fileName: resolve('Button.stories.jsx'), + makeTitle: (title) => title, +}).parse().indexInputs; +const entries = Object.fromEntries( + inputs.map((input) => { + assert(input.type === 'story' && input.__id && input.name && input.title); + return [ + input.__id, + { + id: input.__id, + name: input.name, + title: input.title, + type: 'story' as const, + subtype: input.subtype, + parent: input.parent, + importPath: './Button.stories.jsx', + tags: ['test'], + }, + ]; + }) +); +const selectedInput = inputs.find((input) => input.name === 'renders [ready]'); +assert(selectedInput?.__id); +const childId = selectedInput.__id; +const store = UniversalStore.create({ + ...storeOptions, + leader: true, + initialState: storeOptions.initialState, +}); +function bridge( + source: UniversalStore, + id: string +) { + source.subscribe((event, eventInfo) => + child?.send({ type: `UNIVERSAL_STORE:${id}`, args: [{ event, eventInfo }], from: 'server' }) + ); +} +bridge(store, 'storybook/test'); +bridge(internal_universalStatusStore, 'storybook/status'); +bridge(internal_universalTestProviderStore, 'storybook/test-provider'); +const events: StoreEvent[] = []; +store.subscribe((event) => { + if (event.type === 'FATAL_ERROR' || event.type === 'TEST_RUN_COMPLETED') events.push(event); +}); +function waitFor(predicate: () => boolean, label: string) { + return new Promise((accept, reject) => { + const timer = setTimeout(() => { + clearInterval(poll); + reject(new Error(`Timeout: ${label}\n${JSON.stringify(events.slice(-5))}`)); + }, 90000); + const poll = setInterval(() => { + const fatal = events.find((event) => event.type === 'FATAL_ERROR'); + if (fatal || predicate()) { + clearTimeout(timer); + clearInterval(poll); + if (fatal) reject(new Error(JSON.stringify(fatal))); + else accept(); + } + }, 50); + }); +} +let ready = false; +await rm('native-coverage', { recursive: true, force: true }); +try { + child = fork(fileURLToPath(import.meta.resolve('@storybook/addon-vitest/vitest')), [], { + env: { + ...process.env, + VITEST: 'true', + TEST: 'true', + VITEST_CHILD_PROCESS: 'true', + STORYBOOK_DISABLE_TELEMETRY: '1', + STORYBOOK_CONFIG_DIR: configDir, + NODE_V8_COVERAGE: resolve('native-coverage'), + }, + stdio: ['ignore', 'inherit', 'inherit', 'ipc'], + }); + child.on('message', (message) => { + const event = message as ChannelEvent & { + payload: Extract['payload']; + }; + if (event.type === 'ready') ready = true; + else if (event.type === 'uncaught-error') + events.push({ type: 'FATAL_ERROR', payload: event.payload }); + else channel.receive(event); + }); + await waitFor(() => ready, 'backend startup'); + child.send({ type: STORY_INDEX_CHANNEL_EVENT_NAME, args: [{ v: 5, entries }] }); + assert.equal(events.filter((event) => event.type === 'TEST_RUN_COMPLETED').length, 0); + const results: CurrentRun[] = []; + async function run(storyIds?: string[], coverage = false) { + const before = events.length; + store.send({ + type: 'TRIGGER_RUN', + payload: { storyIds, triggeredBy: 'run-all', configOverride: { coverage, a11y: false } }, + }); + await waitFor( + () => events.slice(before).some((event) => event.type === 'TEST_RUN_COMPLETED'), + 'test completion' + ); + const result = events + .slice(before) + .find((event) => event.type === 'TEST_RUN_COMPLETED')!.payload; + assert.deepEqual(result.unhandledErrors, []); + assert.equal(result.componentTestCount.error, 0); + results.push(result); + return result; + } + assert.equal((await run()).componentTestCount.success, 4); + const selected = await run([childId]); + assert.equal(selected.componentTestCount.success, 1); + assert.equal(selected.totalTestCount, 1); + assert.equal((await run(['compatibility--primary'])).componentTestCount.success, 3); + const covered = await run(undefined, true); + assert(covered.coverageSummary); + assert(covered.coverageSummary.percentage > 0 && covered.coverageSummary.percentage < 90); + assert.equal(covered.coverageSummary.status, 'negative'); + assert.equal((await run()).coverageSummary, undefined); + store.setState((state) => ({ ...state, watching: true })); + const before = events.length; + await writeFile( + 'Button.jsx', + `import React from 'react'; +export const unused = () => { + return 'Uncovered'; +}; +export const Button = () => ;` + ); + await waitFor( + () => + events + .slice(before) + .some( + (event) => + event.type === 'TEST_RUN_COMPLETED' && event.payload.componentTestCount.error === 3 + ), + 'watch failure' + ); + const failed = events.length; + await writeFile( + 'Button.jsx', + `import React from 'react'; +export const unused = () => { + return 'Uncovered'; +}; +export const Button = () => ;` + ); + await waitFor( + () => + events + .slice(failed) + .some( + (event) => + event.type === 'TEST_RUN_COMPLETED' && event.payload.componentTestCount.success === 4 + ), + 'watch recovery' + ); + await writeFile('results.json', JSON.stringify({ environment, results, events }, null, 2)); + process.stdout.write( + `PASS Vitest ${version}: startup, filtering, coverage off/on/off, watch pass/fail/pass\n` + ); +} finally { + if (child) { + const exited = once(child, 'exit'); + child.kill('SIGTERM'); + await exited; + } + channel.removeAllListeners(); +} + +type ScriptCoverage = { + url: string; + functions: { + functionName: string; + ranges: { startOffset: number; endOffset: number; count: number }[]; + }[]; +}; +const nativeCoverage: { result: ScriptCoverage[] }[] = await Promise.all( + (await readdir('native-coverage')).map(async (file) => + JSON.parse(await readFile(resolve('native-coverage', file), 'utf8')) + ) +); +const api = version.startsWith('5.') ? 'standalone' : 'init'; +if (version.startsWith('5.')) { + for (const script of nativeCoverage + .flatMap((coverage) => coverage.result) + .filter((script) => script.url.includes('/vitest/dist/'))) { + const source = await readFile(fileURLToPath(script.url), 'utf8'); + for (const fn of script.functions.filter((fn) => fn.functionName === 'init')) { + const range = fn.ranges[0]; + if ( + source.slice(range.startOffset, range.endOffset).includes('`vitest.init()` is deprecated.') + ) { + assert.equal(range.count, 0, 'Deprecated Vitest.init() was called'); + } + } + } +} + +const executions = nativeCoverage + .flatMap((coverage) => coverage.result) + .filter((script) => script.url.includes('/vitest/dist/')) + .flatMap((script) => + script.functions + .filter((fn) => fn.functionName === api && fn.ranges[0].count === 3) + .map((fn) => ({ url: script.url, ...fn })) + ); +assert.equal( + executions.length, + 1, + `Expected three real ${api} calls: initial startup and two coverage restarts` +); +await writeFile('lifecycle.json', JSON.stringify(executions, null, 2)); +await promisify(execFile)( + process.execPath, + ['node_modules/vitest/vitest.mjs', 'run', '--project=storybook', '--coverage'], + { timeout: 90000, maxBuffer: 10 * 1024 * 1024 } +); +const summary = JSON.parse(await readFile('coverage-cli/coverage-summary.json', 'utf8')); +assert(summary.total.statements.total > 0); +const results = JSON.parse(await readFile('results.json', 'utf8')); +const coveredRun = results.results.find((run: CurrentRun) => run.coverageSummary); +assert.equal(Math.round(summary.total.statements.pct), coveredRun.coverageSummary.percentage); +process.stdout.write(`PASS Vitest ${version}: CLI coverage and real ${api} execution evidence\n`); diff --git a/scripts/vitest-compatibility/ui.ts b/scripts/vitest-compatibility/ui.ts new file mode 100644 index 000000000000..bd9a1c2e433d --- /dev/null +++ b/scripts/vitest-compatibility/ui.ts @@ -0,0 +1,55 @@ +import assert from 'node:assert/strict'; +import { readFile, writeFile } from 'node:fs/promises'; + +import { chromium } from 'playwright'; + +const url = process.argv[2] ?? 'http://localhost:6026'; +const component = await readFile('Button.jsx', 'utf8'); +const browser = await chromium.launch(); +const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); +await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); +const page = await context.newPage(); +page.setDefaultTimeout(60000); +try { + await page.goto(`${url}/?path=/story/compatibility--primary:renders-ready`); + await page.getByRole('button', { name: 'Expand testing module' }).click(); + const watch = page.getByRole('switch', { name: 'Watch mode', exact: true }); + await watch.waitFor(); + if ((await watch.getAttribute('aria-checked')) === 'true') await watch.click(); + const start = page.getByRole('button', { name: 'Start test run', exact: true }); + await start.click(); + await page.getByRole('button', { name: 'Stop test run', exact: true }).waitFor(); + await start.waitFor(); + await page.getByRole('button', { name: 'Component tests passed', exact: true }).waitFor(); + assert.match(await page.locator('body').innerText(), /Ran 4 tests/); + await watch.click(); + await page.getByRole('switch', { name: 'Watch mode', exact: true, checked: true }).waitFor(); + await writeFile('Button.jsx', component.replace('Ready', 'Changed')); + await page.getByRole('button', { name: /^Component tests failed/ }).waitFor(); + await start.waitFor(); + await page.screenshot({ path: 'ui-watch-failure.png' }); + await writeFile('Button.jsx', component); + await page.getByRole('button', { name: 'Component tests passed', exact: true }).waitFor(); + await start.waitFor(); + await watch.click(); + await page.getByRole('switch', { name: 'Watch mode', exact: true, checked: false }).waitFor(); + await page.screenshot({ path: 'ui-watch-recovery.png' }); + process.stdout.write('PASS UI: story selection, run, watch failure and recovery\n'); +} catch (error) { + await writeFile( + 'ui-failure.txt', + (await page.locator('body').innerText()) + + '\n' + + JSON.stringify( + await page + .getByRole('button') + .evaluateAll((elements) => elements.map((element) => element.getAttribute('aria-label'))) + ) + ); + await page.screenshot({ path: 'ui-failure.png' }); + throw error; +} finally { + await writeFile('Button.jsx', component); + await context.tracing.stop({ path: 'ui-trace.zip' }); + await browser.close(); +} diff --git a/scripts/vitest-compatibility/vitest.mutation.config.ts b/scripts/vitest-compatibility/vitest.mutation.config.ts new file mode 100644 index 000000000000..724212cba5cd --- /dev/null +++ b/scripts/vitest-compatibility/vitest.mutation.config.ts @@ -0,0 +1,20 @@ +import { resolve } from 'node:path'; + +import { defineConfig, mergeConfig } from 'vitest/config'; + +import { vitestCommonConfig } from '../../code/vitest.shared.ts'; + +export default mergeConfig( + vitestCommonConfig, + defineConfig({ + root: resolve(import.meta.dirname, '../../code/addons/vitest'), + test: { + passWithNoTests: false, + include: [ + 'src/node/test-manager.test.ts', + 'src/node/coverage-reporter.test.ts', + 'src/vitest-plugin/vitest-root.test.ts', + ], + }, + }) +); diff --git a/test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts b/test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts index d734f4b8c022..7b5bfdbf700f 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts +++ b/test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { expect, test, type Page } from "@playwright/test"; -import { SbPage } from "../../../../code/e2e-sandbox/util"; +import { SbPage } from "../../../../code/e2e-sandbox/util.ts"; const STORYBOOK_URL = "http://localhost:6006"; const TEST_STORY_PATH = path.resolve( @@ -119,13 +119,12 @@ test.describe("component testing", () => { await clearStatusesButton.click({ force: true }); } - const watchModeToggle = page.getByRole("switch", { name: "Watch mode" }); - if ( - (await watchModeToggle.isVisible()) && - (await watchModeToggle.getAttribute("aria-checked")) === "true" - ) { - await watchModeToggle.click({ force: true }); + const watchModeToggle = page.getByRole("switch", { name: /^Watch mode/ }); + await expect(watchModeToggle).toBeEnabled({ timeout: 30000 }); + if (await watchModeToggle.isChecked()) { + await watchModeToggle.click(); } + await expect(watchModeToggle).not.toBeChecked(); const configs = [ page.getByRole("checkbox", { name: "Coverage" }), diff --git a/yarn.lock b/yarn.lock index ca8d1bad6387..bae4b1764251 100644 --- a/yarn.lock +++ b/yarn.lock @@ -119,15 +119,14 @@ __metadata: languageName: node linkType: hard -"@analogjs/vite-plugin-angular@npm:^2.5.2": - version: 2.6.1 - resolution: "@analogjs/vite-plugin-angular@npm:2.6.1" +"@analogjs/vite-plugin-angular@npm:^2.5.2, @analogjs/vite-plugin-angular@npm:^2.7.5": + version: 2.7.5 + resolution: "@analogjs/vite-plugin-angular@npm:2.7.5" dependencies: magic-string: "npm:^0.30.21" obug: "npm:^2.1.1" oxc-parser: "npm:^0.121.0" tinyglobby: "npm:^0.2.14" - ts-morph: "npm:^21.0.0" peerDependencies: "@angular-devkit/build-angular": ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0 "@angular/build": ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0 @@ -139,7 +138,7 @@ __metadata: optional: true vite: optional: true - checksum: 10c0/cc4f509feb48fa68a52eff4313cade2e091662705146807feca4a2b114d6452f0f59854eb18d0c4cd6ae398df7aaad3f05cf90e98678be00fa6cb7a4e40b21d3 + checksum: 10c0/530622ca948fba34b1862429b25a06a326f2d492818ebb91f297165cdc9b14454aa34247732a7890cff3a2c03e58080f296a84637aa04451909f93bcbede518d languageName: node linkType: hard @@ -687,6 +686,16 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^8.0.0, @babel/code-frame@npm:^8.0.6": + version: 8.0.6 + resolution: "@babel/code-frame@npm:8.0.6" + dependencies: + "@babel/helper-validator-identifier": "npm:^8.0.6" + js-tokens: "npm:^10.0.0" + checksum: 10c0/ae3022ce1e41a48e367712f32c51736624992c1b87be17714ce0970063935c511bccbea4e005db02cc4da87370b608b5861378fcab86a78c9764fa3bd2b6959a + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.28.6, @babel/compat-data@npm:^7.29.0, @babel/compat-data@npm:^7.29.7": version: 7.29.7 resolution: "@babel/compat-data@npm:7.29.7" @@ -694,6 +703,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^8.0.5": + version: 8.0.5 + resolution: "@babel/compat-data@npm:8.0.5" + checksum: 10c0/53931a4e15538655b2e720098afcc18405abfbc6b8f8df7d85320f3a6193145a3e5b1c87c61218f58fc4bd6c51cea6fa78092ee723e3fb12e089d89802a8a05a + languageName: node + linkType: hard + "@babel/core@npm:7.29.7, @babel/core@npm:^7.12.0, @babel/core@npm:^7.23.7, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.7, @babel/core@npm:^7.28.0, @babel/core@npm:^7.28.5, @babel/core@npm:^7.29.0, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5": version: 7.29.7 resolution: "@babel/core@npm:7.29.7" @@ -717,6 +733,30 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^8.0.1": + version: 8.0.6 + resolution: "@babel/core@npm:8.0.6" + dependencies: + "@babel/code-frame": "npm:^8.0.6" + "@babel/generator": "npm:^8.0.6" + "@babel/helper-compilation-targets": "npm:^8.0.6" + "@babel/helpers": "npm:^8.0.5" + "@babel/parser": "npm:^8.0.6" + "@babel/template": "npm:^8.0.0" + "@babel/traverse": "npm:^8.0.6" + "@babel/types": "npm:^8.0.6" + "@types/gensync": "npm:^1.0.5" + convert-source-map: "npm:^2.0.0" + empathic: "npm:^2.0.1" + gensync: "npm:^1.0.0-beta.2" + import-meta-resolve: "npm:^4.2.0" + json5: "npm:^2.2.3" + obug: "npm:^2.1.1" + verkit: "npm:^0.3.2" + checksum: 10c0/8198276c898f37d4a1a4e68ea79dc66948638a91a5a4f6b6b23b4e077d96ed80eb79df2810f23ec55f4223738993f8ddabc3bc90076eb63946e76580671ecc0b + languageName: node + linkType: hard + "@babel/generator@npm:7.29.1": version: 7.29.1 resolution: "@babel/generator@npm:7.29.1" @@ -743,17 +783,17 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^8.0.0": - version: 8.0.0 - resolution: "@babel/generator@npm:8.0.0" +"@babel/generator@npm:^8.0.0, @babel/generator@npm:^8.0.6": + version: 8.0.6 + resolution: "@babel/generator@npm:8.0.6" dependencies: - "@babel/parser": "npm:^8.0.0" - "@babel/types": "npm:^8.0.0" - "@jridgewell/gen-mapping": "npm:^0.3.12" - "@jridgewell/trace-mapping": "npm:^0.3.28" + "@babel/parser": "npm:^8.0.6" + "@babel/types": "npm:^8.0.6" + "@jridgewell/gen-mapping": "npm:0.4.0-beta.0" + "@jridgewell/trace-mapping": "npm:^0.3.31" "@types/jsesc": "npm:^2.5.0" jsesc: "npm:^3.0.2" - checksum: 10c0/2b2d171beaedcacca62164f0f39bc8962df5f9700a54c5515174276ea20b3a53933e9804e0b35c2be1059108a764447d99d0c5beb63ecbbf8c66035ca6c003c4 + checksum: 10c0/ea3b078547815e7ee2a865c9618bbdeaca83961690f036791c05fbdd9a5dbfa0e7bd1dc36434cffba6c8be508297bad798ae47096551a6c38a7ff2e4d139729d languageName: node linkType: hard @@ -788,6 +828,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^8.0.6": + version: 8.0.6 + resolution: "@babel/helper-compilation-targets@npm:8.0.6" + dependencies: + "@babel/compat-data": "npm:^8.0.5" + "@babel/helper-validator-option": "npm:^8.0.0" + browserslist: "npm:^4.24.0" + flru: "npm:1.0.2" + verkit: "npm:^0.3.2" + checksum: 10c0/959e4f528544aa16c3b490b9e20e89cc2f3ce641e88b818e00f509bab3fe11afb9a365e4ebf120590519bcef303386155f02d888b48eb031de71b19d0fdfef73 + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.27.1, @babel/helper-create-class-features-plugin@npm:^7.29.7": version: 7.29.7 resolution: "@babel/helper-create-class-features-plugin@npm:7.29.7" @@ -948,10 +1001,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^8.0.0": - version: 8.0.4 - resolution: "@babel/helper-validator-identifier@npm:8.0.4" - checksum: 10c0/9d3c639a42b70613016fda026487a3b8056df918cb40221a48ef666a3bd2574475e315ee509c552baef4bef55a627ee8ea530e9af5b376512ab248c37aeab336 +"@babel/helper-validator-identifier@npm:^8.0.0, @babel/helper-validator-identifier@npm:^8.0.6": + version: 8.0.6 + resolution: "@babel/helper-validator-identifier@npm:8.0.6" + checksum: 10c0/9170fe52f3c330945b259cfb5786fb0e1fb4bdafd4deedcb0d3cbe300e993c8df9ca2307734bd3009220799f3053d206557f78d942df0f41e565304e19d2595c languageName: node linkType: hard @@ -962,6 +1015,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^8.0.0": + version: 8.0.0 + resolution: "@babel/helper-validator-option@npm:8.0.0" + checksum: 10c0/eb466b464479f75c15ba06f6c53862f10d90740350c9203b3d81c6f33d4193bf0a7b7b89de901f256fb639a1346475b48616734af89906d6b4fcf9a856d9f1f1 + languageName: node + linkType: hard + "@babel/helper-wrap-function@npm:^7.29.7": version: 7.29.7 resolution: "@babel/helper-wrap-function@npm:7.29.7" @@ -983,6 +1043,16 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^8.0.5": + version: 8.0.5 + resolution: "@babel/helpers@npm:8.0.5" + dependencies: + "@babel/template": "npm:^8.0.0" + "@babel/types": "npm:^8.0.5" + checksum: 10c0/eb215dd47757fffebc5f690c721e77e541e2131ffa1e06b5f026a75a8b7726b30cd24e6d97fe0a30cbc7865417169f563e0f6c0af45597172fa05e44b2cee3d0 + languageName: node + linkType: hard + "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.6, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.28.5, @babel/parser@npm:^7.29.0, @babel/parser@npm:^7.29.7, @babel/parser@npm:^7.29.8, @babel/parser@npm:^7.4.5, @babel/parser@npm:^7.6.0, @babel/parser@npm:^7.9.6": version: 7.29.8 resolution: "@babel/parser@npm:7.29.8" @@ -994,14 +1064,14 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^8.0.0": - version: 8.0.4 - resolution: "@babel/parser@npm:8.0.4" +"@babel/parser@npm:^8.0.0, @babel/parser@npm:^8.0.4, @babel/parser@npm:^8.0.6": + version: 8.0.6 + resolution: "@babel/parser@npm:8.0.6" dependencies: - "@babel/types": "npm:^8.0.4" + "@babel/types": "npm:^8.0.6" bin: parser: ./bin/babel-parser.js - checksum: 10c0/b98ebc350e6583ccec0c59ab653e6c5967935826b8e03210a96e638a98b0c552587d4ca9cc44502964bac42463e2bab93ca1a6ac519279a25e190bd78f3ce513 + checksum: 10c0/af600f314542538a9dacf08e51cdce1eb947a6008e54539833987bdda98ef8632fd7178985411315431240e3d3d969788a216e534d6301eecbabed6d4a88a766 languageName: node linkType: hard @@ -2167,6 +2237,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^8.0.0": + version: 8.0.0 + resolution: "@babel/template@npm:8.0.0" + dependencies: + "@babel/code-frame": "npm:^8.0.0" + "@babel/parser": "npm:^8.0.0" + "@babel/types": "npm:^8.0.0" + checksum: 10c0/8a5e5687299517cc6b0efff3a6342cf4c4256aeeffa14753cc8d08106aaf0ba1916707c0e1f385b511a7b42c6c87806fecaecd4fe06316207475e0d4d1148e2c + languageName: node + linkType: hard + "@babel/traverse@npm:latest": version: 7.28.5 resolution: "@babel/traverse@npm:7.28.5" @@ -2265,10 +2346,10 @@ __metadata: languageName: node linkType: hard -"@blazediff/core@npm:1.9.1": - version: 1.9.1 - resolution: "@blazediff/core@npm:1.9.1" - checksum: 10c0/fd45cdd0544002341d74831a179ef693a81414abd348c1ff0c01086c0ea03f5e5ee284c4e16c2e6fb3670c265f90a3d85752b9360320efa9a835928e604dae77 +"@blazediff/core@npm:1.10.0": + version: 1.10.0 + resolution: "@blazediff/core@npm:1.10.0" + checksum: 10c0/4b6095d39338fccd09964fff06f6c5a75628e6296cb2d598149376a876b5c390e083a3769e6ebdaeaa53550f29f1ed5744688df66e4f10b0be0bbc41e9393c8c languageName: node linkType: hard @@ -3820,6 +3901,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:0.4.0-beta.0": + version: 0.4.0-beta.0 + resolution: "@jridgewell/gen-mapping@npm:0.4.0-beta.0" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.6.0-beta.0" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/3c8e3d13318cac007834b2ebc065e99574f5d12c1059e7fb58ad50e3b1cc6e422f610e29c2b90afeaad967a0e69503352b8b29b25954985e6bc8dfdb0031d53b + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.13, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.13 resolution: "@jridgewell/gen-mapping@npm:0.3.13" @@ -3857,10 +3948,10 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": - version: 1.5.5 - resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" - checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 +"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5, @jridgewell/sourcemap-codec@npm:^1.6.0, @jridgewell/sourcemap-codec@npm:^1.6.0-beta.0": + version: 1.6.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.6.0" + checksum: 10c0/b5be700e45a775f218589c3466c4ffea630582b4988657652da464e5ab8a7d18bf928ee4fd2363fb346ede4bea9c6ff0bae05a538358c4565ece6199531b5f72 languageName: node linkType: hard @@ -10011,11 +10102,12 @@ __metadata: "@types/micromatch": "npm:^4.0.0" "@types/node": "npm:^22.19.1" "@types/semver": "npm:^7.7.1" - "@vitest/browser-playwright": "npm:^4.1.6" - "@vitest/runner": "npm:^4.1.6" + "@vitest/browser-playwright": "npm:^5.0.0" + "@vitest/istanbul-lib-report": "npm:^1.0.1" empathic: "npm:^2.0.0" es-toolkit: "npm:^1.43.0" istanbul-lib-report: "npm:^3.0.1" + istanbul-reports: "npm:^3.2.0" micromatch: "npm:^4.0.8" pathe: "npm:^1.1.2" picocolors: "npm:^1.1.0" @@ -10028,20 +10120,17 @@ __metadata: tree-kill: "npm:^1.2.2" ts-dedent: "npm:^2.2.0" typescript: "npm:^6.0.3" - vitest: "npm:^4.1.6" + vitest: "npm:^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": optional: true "@vitest/browser-playwright": optional: true - "@vitest/runner": - optional: true vitest: optional: true languageName: unknown @@ -10072,7 +10161,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/angular-vite@workspace:code/frameworks/angular-vite" dependencies: - "@analogjs/vite-plugin-angular": "npm:^2.5.2" + "@analogjs/vite-plugin-angular": "npm:^2.7.5" "@angular-devkit/architect": "npm:^0.2102.12" "@angular-devkit/build-angular": "npm:^21.2.12" "@angular-devkit/core": "npm:^21.2.12" @@ -10098,7 +10187,7 @@ __metadata: vite: "npm:>=8.0.0" zone.js: "npm:^0.16.2" peerDependencies: - "@analogjs/vite-plugin-angular": ">=2.7.3" + "@analogjs/vite-plugin-angular": ">=2.7.5" "@angular-devkit/architect": ">=0.2100.0 < 0.2300.0" "@angular-devkit/core": ">=21.0.0 < 23.0.0" "@angular/build": ">=21.0.0 < 23.0.0" @@ -10332,10 +10421,10 @@ __metadata: "@types/react-dom": "npm:^18.0.11" "@vitejs/plugin-react": "npm:^4.3.2" "@vitejs/plugin-vue": "npm:^4.4.0" - "@vitest/browser": "npm:^4.1.6" - "@vitest/browser-playwright": "npm:^4.1.6" - "@vitest/coverage-istanbul": "npm:^4.1.6" - "@vitest/coverage-v8": "npm:^4.1.6" + "@vitest/browser": "npm:^5.0.0" + "@vitest/browser-playwright": "npm:^5.0.0" + "@vitest/coverage-istanbul": "npm:^5.0.0" + "@vitest/coverage-v8": "npm:^5.0.0" chromatic: "npm:^18.0.1" create-storybook: "workspace:*" danger: "npm:^13.0.4" @@ -10363,7 +10452,7 @@ __metadata: uuid: "npm:^11.1.0" vite: "npm:^7.0.4" vite-plugin-inspect: "npm:^11.0.0" - vitest: "npm:^4.1.6" + vitest: "npm:^5.0.0" wait-on: "npm:^8.0.3" dependenciesMeta: ejs: @@ -10883,7 +10972,7 @@ __metadata: oxfmt: "npm:^0.41.0" std-env: "npm:^4.0.0" vite: "npm:^7.0.4" - vitest: "npm:^4.1.6" + vitest: "npm:^5.0.0" languageName: unknown linkType: soft @@ -10921,7 +11010,7 @@ __metadata: "@types/wait-on": "npm:^5.3.4" "@types/window-size": "npm:^1.1.4" "@verdaccio/types": "npm:^10.8.0" - "@vitest/coverage-v8": "npm:^4.1.6" + "@vitest/coverage-v8": "npm:^5.0.0" chromatic: "npm:^18.0.1" citty: "npm:^0.2.1" codecov: "npm:^3.8.1" @@ -10999,7 +11088,7 @@ __metadata: uuid: "npm:^9.0.1" verdaccio: "npm:^5.33.0" verdaccio-auth-memory: "npm:^10.3.1" - vitest: "npm:^4.1.6" + vitest: "npm:^5.0.0" wait-on: "npm:^8.0.3" window-size: "npm:^1.1.1" yaml: "npm:^2.8.1" @@ -12123,18 +12212,6 @@ __metadata: languageName: node linkType: hard -"@ts-morph/common@npm:~0.22.0": - version: 0.22.0 - resolution: "@ts-morph/common@npm:0.22.0" - dependencies: - fast-glob: "npm:^3.3.2" - minimatch: "npm:^9.0.3" - mkdirp: "npm:^3.0.1" - path-browserify: "npm:^1.0.1" - checksum: 10c0/1f1ff7fee54414e09803b1ba559ff51881b821c19bc97cb19d06fc46c58957dea4a58e6a83e9f2e30e08c8179d6d142e45be329d9242f410323795b5c1e04806 - languageName: node - linkType: hard - "@ts-morph/common@npm:~0.29.0": version: 0.29.0 resolution: "@ts-morph/common@npm:0.29.0" @@ -12490,6 +12567,13 @@ __metadata: languageName: node linkType: hard +"@types/gensync@npm:^1.0.5": + version: 1.0.5 + resolution: "@types/gensync@npm:1.0.5" + checksum: 10c0/7c37c8fc3b1a49efd8889f635a6204008f1c1cc86a09ac6a96e219cf3fc52cf1ff091c4dddc3160e93261d6e599fbba39f5fb698fbf936518d1c410289cdd640 + languageName: node + linkType: hard + "@types/glob@npm:*": version: 8.1.0 resolution: "@types/glob@npm:8.1.0" @@ -14238,76 +14322,58 @@ __metadata: languageName: node linkType: hard -"@vitest/browser-playwright@npm:^4.1.6": - version: 4.1.10 - resolution: "@vitest/browser-playwright@npm:4.1.10" +"@vitest/browser-playwright@npm:^5.0.0": + version: 5.0.1 + resolution: "@vitest/browser-playwright@npm:5.0.1" dependencies: - "@vitest/browser": "npm:4.1.10" - "@vitest/mocker": "npm:4.1.10" - tinyrainbow: "npm:^3.1.0" + "@vitest/browser": "npm:5.0.1" + "@vitest/mocker": "npm:5.0.1" + tinyrainbow: "npm:^3.1.1" peerDependencies: playwright: "*" - vitest: 4.1.10 + vitest: 5.0.1 peerDependenciesMeta: playwright: optional: false - checksum: 10c0/161530da4da9c061875c0e80c2c94e93658bf92514f2e5173c04299e17a6021feb209ef8fb5e14e880c8c8b2b9e7fc4f3ec7a346c8c4a77831864597331257fc + checksum: 10c0/152fd11e78a8fdea46a5e6537c5ce42ecd95b8744a7fe1cc59e4117b8e8220d1edf9bc55d28bc3c1cc51e7b758cbc0fd08b7d07794638c44dfbe5f546d84535f languageName: node linkType: hard -"@vitest/browser@npm:4.1.10": - version: 4.1.10 - resolution: "@vitest/browser@npm:4.1.10" - dependencies: - "@blazediff/core": "npm:1.9.1" - "@vitest/mocker": "npm:4.1.10" - "@vitest/utils": "npm:4.1.10" - magic-string: "npm:^0.30.21" - pngjs: "npm:^7.0.0" - sirv: "npm:^3.0.2" - tinyrainbow: "npm:^3.1.0" - ws: "npm:^8.19.0" - peerDependencies: - vitest: 4.1.10 - checksum: 10c0/61c86b8c0fcc78bd01de559914525e768dc16d565ee8a40a41d51ef6d99595c3c5e976defe32d090b3dda5f77a980caa11a2072e9c4d279cfb8d55d31c565fc7 - languageName: node - linkType: hard - -"@vitest/browser@npm:^4.1.6": - version: 4.1.11 - resolution: "@vitest/browser@npm:4.1.11" +"@vitest/browser@npm:5.0.1, @vitest/browser@npm:^5.0.0": + version: 5.0.1 + resolution: "@vitest/browser@npm:5.0.1" dependencies: - "@blazediff/core": "npm:1.9.1" - "@vitest/mocker": "npm:4.1.11" - "@vitest/utils": "npm:4.1.11" - magic-string: "npm:^0.30.21" + "@blazediff/core": "npm:1.10.0" + "@vitest/mocker": "npm:5.0.1" + "@vitest/ui": "npm:5.0.1" + "@vitest/utils": "npm:5.0.1" + magic-string: "npm:^1.2.3" pngjs: "npm:^7.0.0" sirv: "npm:^3.0.2" - tinyrainbow: "npm:^3.1.0" - ws: "npm:^8.19.0" + tinyrainbow: "npm:^3.1.1" + ws: "npm:^8.21.3" peerDependencies: - vitest: 4.1.11 - checksum: 10c0/a16b95d043650e82e0706131454d259e660e11fca01d2ce9e336320a482626b2175051e12a93ea18efaf9a794f6cce221919fe59fe27fba04bd9f0e253e6957e + vitest: 5.0.1 + checksum: 10c0/1fb73d5140aebbf9bbc93b4d09077051689afa7a8bbb45ae22f2136f3eddd1bcb6f44a63569b5fb09dded1846e79e9f3dd6e05a055d05af75de780ef6056a3e9 languageName: node linkType: hard -"@vitest/coverage-istanbul@npm:^4.1.6": - version: 4.1.10 - resolution: "@vitest/coverage-istanbul@npm:4.1.10" +"@vitest/coverage-istanbul@npm:^5.0.0": + version: 5.0.1 + resolution: "@vitest/coverage-istanbul@npm:5.0.1" dependencies: - "@babel/core": "npm:^7.29.0" - "@istanbuljs/schema": "npm:^0.1.3" "@jridgewell/gen-mapping": "npm:^0.3.13" "@jridgewell/trace-mapping": "npm:0.3.31" - istanbul-lib-coverage: "npm:^3.2.2" - istanbul-lib-report: "npm:^3.0.1" - istanbul-reports: "npm:^3.2.0" - magicast: "npm:^0.5.2" - obug: "npm:^2.1.1" - tinyrainbow: "npm:^3.1.0" + "@vitest/istanbul-lib-coverage": "npm:^1.0.0" + "@vitest/istanbul-lib-instrument": "npm:^1.0.0" + "@vitest/istanbul-lib-report": "npm:^1.0.0" + "@vitest/istanbul-lib-source-maps": "npm:^1.0.0" + magicast: "npm:^0.5.4" + obug: "npm:^2.1.4" + tinyrainbow: "npm:^3.1.1" peerDependencies: - vitest: 4.1.10 - checksum: 10c0/a426b0aff94482587a100db7e8db4e238fabf9e5bc4d33a319bd9cd4261b8972a7b3d78f2bc7cbc3d11b0b58b5d7cc487f96ab35a96c1c766c54f9b6d5eb4f78 + vitest: 5.0.1 + checksum: 10c0/f7049c8a849f4f0ea48c9cbb3cf59cf7bf106ff52cf7a3e138dccf209d510f2616d2fe49adbc8c380cd30487d238059022fd7c8a0af0d192c57848c608d795f1 languageName: node linkType: hard @@ -14362,6 +14428,28 @@ __metadata: languageName: node linkType: hard +"@vitest/coverage-v8@npm:^5.0.0": + version: 5.0.1 + resolution: "@vitest/coverage-v8@npm:5.0.1" + dependencies: + "@bcoe/v8-coverage": "npm:^1.0.2" + "@vitest/istanbul-lib-coverage": "npm:^1.0.0" + "@vitest/istanbul-lib-report": "npm:^1.0.0" + ast-v8-to-istanbul: "npm:^1.0.5" + magicast: "npm:^0.5.4" + obug: "npm:^2.1.4" + std-env: "npm:^4.2.0" + tinyrainbow: "npm:^3.1.1" + peerDependencies: + "@vitest/browser": 5.0.1 + vitest: 5.0.1 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10c0/7f6dba018b7cd3303c625ff0c906bab971eef00214fac5d7edfd7ab3991bccbe7edf47f89273f49f7d2161e16fbdcc2b9938efd16f2da3bab8c31bf83777349e + languageName: node + linkType: hard + "@vitest/expect@npm:3.2.4": version: 3.2.4 resolution: "@vitest/expect@npm:3.2.4" @@ -14402,6 +14490,46 @@ __metadata: languageName: node linkType: hard +"@vitest/istanbul-lib-coverage@npm:1.0.1, @vitest/istanbul-lib-coverage@npm:^1.0.0": + version: 1.0.1 + resolution: "@vitest/istanbul-lib-coverage@npm:1.0.1" + checksum: 10c0/5259767db6c748a018c39554951e19818268fb6f72f74b74d2370471c7d83df990c3371247caea7d38f8e315df6dd461427dc242a1e7f5071e4b28863524f354 + languageName: node + linkType: hard + +"@vitest/istanbul-lib-instrument@npm:^1.0.0": + version: 1.0.1 + resolution: "@vitest/istanbul-lib-instrument@npm:1.0.1" + dependencies: + "@babel/core": "npm:^8.0.1" + "@babel/parser": "npm:^8.0.4" + "@istanbuljs/schema": "npm:^0.1.3" + "@jridgewell/trace-mapping": "npm:^0.3.23" + "@vitest/istanbul-lib-coverage": "npm:1.0.1" + checksum: 10c0/627f2323f28ca8c4b7dd2c0842c1642f4a7b0058f7119eed9b7c909920a660cda73926f8aeda66b678947ac91ef0cfcaa0721e208879aaf4908170c8a5925319 + languageName: node + linkType: hard + +"@vitest/istanbul-lib-report@npm:^1.0.0, @vitest/istanbul-lib-report@npm:^1.0.1": + version: 1.0.1 + resolution: "@vitest/istanbul-lib-report@npm:1.0.1" + dependencies: + "@vitest/istanbul-lib-coverage": "npm:1.0.1" + checksum: 10c0/1a76d4cd4c6284da0b897230d672769a5191235b3a0df41a3c861e3e12659d1790012705df6bad0f540e3f14b7b017f3611f99c9b9eb96972f8b55a3fb9270fe + languageName: node + linkType: hard + +"@vitest/istanbul-lib-source-maps@npm:^1.0.0": + version: 1.0.1 + resolution: "@vitest/istanbul-lib-source-maps@npm:1.0.1" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.23" + "@vitest/istanbul-lib-coverage": "npm:1.0.1" + obug: "npm:^2.1.4" + checksum: 10c0/819e1f7b7d6b3cc2c34c91430fa966d3f0046842296ff8c2139d3e2237f97ca6fff9bfa285fccc073960e10ba6bea1bb09d115db2808acd0cff8dba4014b6e14 + languageName: node + linkType: hard + "@vitest/mocker@npm:3.2.4": version: 3.2.4 resolution: "@vitest/mocker@npm:3.2.4" @@ -14440,13 +14568,14 @@ __metadata: languageName: node linkType: hard -"@vitest/mocker@npm:4.1.11": - version: 4.1.11 - resolution: "@vitest/mocker@npm:4.1.11" +"@vitest/mocker@npm:5.0.1": + version: 5.0.1 + resolution: "@vitest/mocker@npm:5.0.1" dependencies: - "@vitest/spy": "npm:4.1.11" + "@jridgewell/trace-mapping": "npm:0.3.31" + "@vitest/spy": "npm:5.0.1" estree-walker: "npm:^3.0.3" - magic-string: "npm:^0.30.21" + magic-string: "npm:^1.2.3" peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -14455,7 +14584,7 @@ __metadata: optional: true vite: optional: true - checksum: 10c0/3111ea34bd5046f6c70bbd67cf8b89608ee8c41cb27ab2bd61d42f4b68810e9ea16a9a757a71bc254c105f73b407d00ebb6bab1ab0f7f5cdcc9d7d16602a3933 + checksum: 10c0/21f0456c7452d7f65449fc0f43f1ccbf222d353eb11fcf511b78b5b8c4ae38ac9ca286b56936d91856c483575cd86688f1b6dc00f02ffd85ed1108b12cf219cd languageName: node linkType: hard @@ -14477,12 +14606,12 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:4.1.11": - version: 4.1.11 - resolution: "@vitest/pretty-format@npm:4.1.11" +"@vitest/pretty-format@npm:5.0.1": + version: 5.0.1 + resolution: "@vitest/pretty-format@npm:5.0.1" dependencies: - tinyrainbow: "npm:^3.1.0" - checksum: 10c0/ad32525c73807c0b72f38dc29bc51fd5a17879dc650f37995a9c5adbb8526e15f787691f76aa8768448ec7ed5bf5ba2b12329eac8fda1428b4d6b8c036390f71 + tinyrainbow: "npm:^3.1.1" + checksum: 10c0/1fb025ae15b02b683b8ef8bd00babb585c6462bf9bc19985f39a8124f9d46489da943484a4c058b49123165489be5cb9bedac4ea41bf7c525380fff5e2182a0e languageName: node linkType: hard @@ -14496,16 +14625,6 @@ __metadata: languageName: node linkType: hard -"@vitest/runner@npm:^4.1.6": - version: 4.1.11 - resolution: "@vitest/runner@npm:4.1.11" - dependencies: - "@vitest/utils": "npm:4.1.11" - pathe: "npm:^2.0.3" - checksum: 10c0/3c782b055e9e688e1785f7c8937bd1669bad1b0e5758cb8b844f918f30a324b1d21e174d46c941ce80ebf37f2b89b55b6d619a675025914a1ece6377b95909e2 - languageName: node - linkType: hard - "@vitest/snapshot@npm:4.1.10": version: 4.1.10 resolution: "@vitest/snapshot@npm:4.1.10" @@ -14534,10 +14653,26 @@ __metadata: languageName: node linkType: hard -"@vitest/spy@npm:4.1.11": - version: 4.1.11 - resolution: "@vitest/spy@npm:4.1.11" - checksum: 10c0/06c68247a8efd21006abe7532fee17f30ba83c8cda3e0b952ef89e278d58058f4b1de69b6bbaa2ed614c568bf4f5769fcc76be42802dedf2bcdd9c0aae601740 +"@vitest/spy@npm:5.0.1": + version: 5.0.1 + resolution: "@vitest/spy@npm:5.0.1" + checksum: 10c0/88838f6dda5237f0a40c365598ce7112922611e9157b390ca90c11760ae8697c37214f621263bfbe12e2bd27234df577cbbecc93de003ced366d23be24383c37 + languageName: node + linkType: hard + +"@vitest/ui@npm:5.0.1": + version: 5.0.1 + resolution: "@vitest/ui@npm:5.0.1" + dependencies: + "@vitest/utils": "npm:5.0.1" + fflate: "npm:^0.8.3" + flatted: "npm:^3.4.4" + pathe: "npm:^2.0.3" + sirv: "npm:^3.0.2" + tinyrainbow: "npm:^3.1.1" + peerDependencies: + vitest: 5.0.1 + checksum: 10c0/24db3f5c27080c54841cd32ba0c82f9df59066923f860a7a26ff9c33c17f3246e5f6d0dc86ea214b5d06d986d7f54a23e5ee45a7013bc5dccccee48a67e44490 languageName: node linkType: hard @@ -14563,14 +14698,14 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:4.1.11": - version: 4.1.11 - resolution: "@vitest/utils@npm:4.1.11" +"@vitest/utils@npm:5.0.1": + version: 5.0.1 + resolution: "@vitest/utils@npm:5.0.1" dependencies: - "@vitest/pretty-format": "npm:4.1.11" + "@vitest/pretty-format": "npm:5.0.1" convert-source-map: "npm:^2.0.0" - tinyrainbow: "npm:^3.1.0" - checksum: 10c0/a2c1ddc64333458c3e031465c1ee0440a7660fd1b298c54ccbf865ef1e5cf3ebdd6c5c75a5ea235d8a672498b394e7121f992b096f021f45014ab25e9abd1b52 + tinyrainbow: "npm:^3.1.1" + checksum: 10c0/c16c5d0cf3795e5c3bc46212bc03a4192fb2e515cf51e1465188578d591a1feeacb911c0d554ebbb6e1dcf115e96a528b6175dedf0b2a4f8b4ce4a5ce61ab5bd languageName: node linkType: hard @@ -15813,14 +15948,14 @@ __metadata: languageName: node linkType: hard -"ast-v8-to-istanbul@npm:^1.0.0": - version: 1.0.0 - resolution: "ast-v8-to-istanbul@npm:1.0.0" +"ast-v8-to-istanbul@npm:^1.0.0, ast-v8-to-istanbul@npm:^1.0.5": + version: 1.0.7 + resolution: "ast-v8-to-istanbul@npm:1.0.7" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.31" estree-walker: "npm:^3.0.3" js-tokens: "npm:^10.0.0" - checksum: 10c0/35e57b754ba63287358094d4f7ae8de2de27286fb4e76a1fbf28b2e67e3b670b59c3f511882473d0fd2cdbaa260062e3cd4f216b724c70032e2b09e5cebbd618 + checksum: 10c0/c6bbfbf8b8adc8d793f8d10fbf9bcb25b0c65f2faa73ec1e41d2937a9c18327167b9395134943a4814dd3d095d2453b2f0f71a5d33aecd808ac4725c23e69918 languageName: node linkType: hard @@ -17574,13 +17709,6 @@ __metadata: languageName: node linkType: hard -"code-block-writer@npm:^12.0.0": - version: 12.0.0 - resolution: "code-block-writer@npm:12.0.0" - checksum: 10c0/ced73cdc466bff968bba9e8e32340d88420d25a229b9269f7425a10a7c2c9a12ca702dcb601b2462b96472d354f021cf66e552179fcbe30c8f7ecd0173c5fa07 - languageName: node - linkType: hard - "code-block-writer@npm:^13.0.3": version: 13.0.3 resolution: "code-block-writer@npm:13.0.3" @@ -19484,13 +19612,20 @@ __metadata: languageName: node linkType: hard -"empathic@npm:2.0.0, empathic@npm:^2.0.0": +"empathic@npm:2.0.0": version: 2.0.0 resolution: "empathic@npm:2.0.0" checksum: 10c0/7d3b14b04a93b35c47bcc950467ec914fd241cd9acc0269b0ea160f13026ec110f520c90fae64720fde72cc1757b57f3f292fb606617b7fccac1f4d008a76506 languageName: node linkType: hard +"empathic@npm:^2.0.0, empathic@npm:^2.0.1": + version: 2.1.0 + resolution: "empathic@npm:2.1.0" + checksum: 10c0/cf13e2919d680a22140d7cd1637a01f6cf9a727f21d3a54b23489ec01c80eab548b9d832d2e167d00e124a54377660c1d6fd2b5947d905366e4d22fde7b9e082 + languageName: node + linkType: hard + "encodeurl@npm:^2.0.0, encodeurl@npm:~2.0.0": version: 2.0.0 resolution: "encodeurl@npm:2.0.0" @@ -19856,10 +19991,10 @@ __metadata: languageName: node linkType: hard -"es-module-lexer@npm:^2.0.0": - version: 2.0.0 - resolution: "es-module-lexer@npm:2.0.0" - checksum: 10c0/ae78dbbd43035a4b972c46cfb6877e374ea290adfc62bc2f5a083fea242c0b2baaab25c5886af86be55f092f4a326741cb94334cd3c478c383fdc8a9ec5ff817 +"es-module-lexer@npm:^2.0.0, es-module-lexer@npm:^2.3.2": + version: 2.3.2 + resolution: "es-module-lexer@npm:2.3.2" + checksum: 10c0/5e7389424c43478439f12f9a6aca1750f6f99afa384fc3de329f4a45f152ab156671055008adaafc74960877abf8cc338aeebf6bed8c21297b146fd6eb7a22f8 languageName: node linkType: hard @@ -20795,7 +20930,7 @@ __metadata: languageName: node linkType: hard -"expect-type@npm:^1.3.0": +"expect-type@npm:^1.3.0, expect-type@npm:^1.4.0": version: 1.4.0 resolution: "expect-type@npm:1.4.0" checksum: 10c0/d40d76b8570695d36587beb3cc28494da2ca3ec8f04e67f5622ed2d372d850e401a9adef19c6835e1a8173903f157c79540b34c7b3fbd7cd8ce726cc903c57b7 @@ -21237,6 +21372,13 @@ __metadata: languageName: node linkType: hard +"fflate@npm:^0.8.3": + version: 0.8.3 + resolution: "fflate@npm:0.8.3" + checksum: 10c0/eab181ca37f5348ae76d4b6f840e0026e30220e33153289ac942222d8b9638237d486507dbcc09878d724095bd354993a2ee48bbee99c8f2c6440d4448719aa7 + languageName: node + linkType: hard + "figures@npm:3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -21508,10 +21650,10 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.2.9": - version: 3.2.9 - resolution: "flatted@npm:3.2.9" - checksum: 10c0/5c91c5a0a21bbc0b07b272231e5b4efe6b822bcb4ad317caf6bb06984be4042a9e9045026307da0fdb4583f1f545e317a67ef1231a59e71f7fced3cc429cfc53 +"flatted@npm:^3.2.9, flatted@npm:^3.4.4": + version: 3.4.4 + resolution: "flatted@npm:3.4.4" + checksum: 10c0/a3a52a88ea5a4c333e5a1f097dcd87a037fa31c236a77cf46222c2aa4036ef895ab9bc76138a66d9dc22bdb3652128f9598cd26e7439561bf19f67276e2bc37e languageName: node linkType: hard @@ -21536,6 +21678,13 @@ __metadata: languageName: node linkType: hard +"flru@npm:1.0.2": + version: 1.0.2 + resolution: "flru@npm:1.0.2" + checksum: 10c0/73b01b1df3f3f9d56a86c2102ac199ac72918ee7a00f8516be8dd4495da90ede9375873ad8902ca93a23c91bcc224aacf2dcf89bf7ce62efb9faf048b564b412 + languageName: node + linkType: hard + "flush-promises@npm:^1.0.2": version: 1.0.2 resolution: "flush-promises@npm:1.0.2" @@ -23401,6 +23550,13 @@ __metadata: languageName: node linkType: hard +"import-meta-resolve@npm:^4.2.0": + version: 4.2.0 + resolution: "import-meta-resolve@npm:4.2.0" + checksum: 10c0/3ee8aeecb61d19b49d2703987f977e9d1c7d4ba47db615a570eaa02fe414f40dfa63f7b953e842cbe8470d26df6371332bfcf21b2fd92b0112f9fea80dde2c4c + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -25815,12 +25971,12 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^1.0.0, magic-string@npm:^1.1.0": - version: 1.2.3 - resolution: "magic-string@npm:1.2.3" +"magic-string@npm:^1.0.0, magic-string@npm:^1.1.0, magic-string@npm:^1.2.3": + version: 1.4.2 + resolution: "magic-string@npm:1.4.2" dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.5.5" - checksum: 10c0/541ecb30ec96e4d9b76ca65aed00124592dca26739aeee86e01d7fbce227b3fce43c9d2f0b9495dc9077c30794c967b1e091445614e2e45722ea4c128be60a93 + "@jridgewell/sourcemap-codec": "npm:^1.6.0" + checksum: 10c0/d3e222c202f99ec4861e7316f473f9b1cfe335ebdf38d4f67335f842f3fe248f01ee00fd3e1a8206bcbe9855e0c41515d7c210300d8fcf6eefbb982d79e423c6 languageName: node linkType: hard @@ -25835,14 +25991,14 @@ __metadata: languageName: node linkType: hard -"magicast@npm:^0.5.2": - version: 0.5.2 - resolution: "magicast@npm:0.5.2" +"magicast@npm:^0.5.2, magicast@npm:^0.5.4": + version: 0.5.5 + resolution: "magicast@npm:0.5.5" dependencies: - "@babel/parser": "npm:^7.29.0" - "@babel/types": "npm:^7.29.0" + "@babel/parser": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" source-map-js: "npm:^1.2.1" - checksum: 10c0/924af677643c5a0a7d6cdb3247c0eb96fa7611b2ba6a5e720d35d81c503d3d9f5948eb5227f80f90f82ea3e7d38cffd10bb988f3fc09020db428e14f26e960d7 + checksum: 10c0/d27e94cd1e85f1736f97ab0c6e542170a818c7e573261722d056b3ec4a520e16d836ce4c6b0cd3dd215b1171d83592d48a3736f3da5997a55629a0de05f66e7d languageName: node linkType: hard @@ -26771,7 +26927,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.3, minimatch@npm:^9.0.4": +"minimatch@npm:^9.0.4": version: 9.0.5 resolution: "minimatch@npm:9.0.5" dependencies: @@ -26931,15 +27087,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^3.0.1": - version: 3.0.1 - resolution: "mkdirp@npm:3.0.1" - bin: - mkdirp: dist/cjs/src/bin.js - checksum: 10c0/9f2b975e9246351f5e3a40dcfac99fcd0baa31fbfab615fe059fb11e51f10e4803c63de1f384c54d656e4db31d000e4767e9ef076a22e12a641357602e31d57d - languageName: node - linkType: hard - "mktemp@npm:~0.4.0": version: 0.4.0 resolution: "mktemp@npm:0.4.0" @@ -27974,10 +28121,10 @@ __metadata: languageName: node linkType: hard -"obug@npm:^2.1.1, obug@npm:^2.1.3": - version: 2.1.3 - resolution: "obug@npm:2.1.3" - checksum: 10c0/cb8187fed0a5fc8445507c950e89f3c1bd43895658c398b5803f6b7804dfa0c562975ecce1e67f3d9247d521452a5bfade9e0e951cc0326b7444272f7c24d25f +"obug@npm:^2.1.1, obug@npm:^2.1.3, obug@npm:^2.1.4": + version: 2.2.1 + resolution: "obug@npm:2.2.1" + checksum: 10c0/8a7c001847d880fa6ba83c7bbee6a80fd271ee628439cd508c8647d53b217abb1759e2d5e4aaeb09a35d0a274bdb225a0a5a78eded3213d63e656153cf411b63 languageName: node linkType: hard @@ -33578,10 +33725,10 @@ __metadata: languageName: node linkType: hard -"std-env@npm:^4.0.0, std-env@npm:^4.0.0-rc.1": - version: 4.0.0 - resolution: "std-env@npm:4.0.0" - checksum: 10c0/63b1716eae27947adde49e21b7225a0f75fb2c3d410273ae9de8333c07c7d5fc7a0628ae4c8af6b4b49b4274ed46c2bf118ed69b64f1261c9d8213d76ed1c16c +"std-env@npm:^4.0.0, std-env@npm:^4.0.0-rc.1, std-env@npm:^4.2.0": + version: 4.2.0 + resolution: "std-env@npm:4.2.0" + checksum: 10c0/40ac525ce7b7c556abc332a7376f14356eeb1a7f17f6ff9a003eb9f52326ff1f3745d3e1b43452675b1ec6fcc319f1b1d6f3b0d386cf3f91058479ad883cff69 languageName: node linkType: hard @@ -34721,6 +34868,13 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:6.1.4": + version: 6.1.4 + resolution: "tinybench@npm:6.1.4" + checksum: 10c0/7a815318a02270a98247298ddd289b75c841725606c977f0b74749bc5ec811a44998267ccb60bd3deb23b551dbfaf4d979eb08e9b04fbc87ff571f9b39cc61c3 + languageName: node + linkType: hard + "tinybench@npm:^2.9.0": version: 2.9.0 resolution: "tinybench@npm:2.9.0" @@ -34742,10 +34896,10 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^1.0.2, tinyexec@npm:^1.0.4": - version: 1.0.4 - resolution: "tinyexec@npm:1.0.4" - checksum: 10c0/d4a5bbcf6bdb23527a4b74c4aa566f41432167112fe76f420ec7e3a90a3ecfd3a7d944383e2719fc3987b69400f7b928daf08700d145fb527c2e80ec01e198bd +"tinyexec@npm:1.3.0, tinyexec@npm:^1.0.2, tinyexec@npm:^1.0.4": + version: 1.3.0 + resolution: "tinyexec@npm:1.3.0" + checksum: 10c0/e9b89f97489d2aab2cef408da279e6b32547e738d1275032ccb8fd0028a006d93eb70fc51c6cffd9fc2f5aca6c2a273d8b6f73b52d46ee5116da6b94969ef958 languageName: node linkType: hard @@ -34783,10 +34937,10 @@ __metadata: languageName: node linkType: hard -"tinyrainbow@npm:^3.1.0": - version: 3.1.0 - resolution: "tinyrainbow@npm:3.1.0" - checksum: 10c0/f11cf387a26c5c9255bec141a90ac511b26172981b10c3e50053bc6700ea7d2336edcc4a3a21dbb8412fe7c013477d2ba4d7e4877800f3f8107be5105aad6511 +"tinyrainbow@npm:^3.1.0, tinyrainbow@npm:^3.1.1": + version: 3.1.1 + resolution: "tinyrainbow@npm:3.1.1" + checksum: 10c0/f9d2743832c6191f753408f36224fe817620b8abcef572b2e570204c673a901d753ff84ca8e7b88f9c79e934295b3ffc6fcbc56a06f126e24e1ec6186dcad40d languageName: node linkType: hard @@ -35094,16 +35248,6 @@ __metadata: languageName: node linkType: hard -"ts-morph@npm:^21.0.0": - version: 21.0.1 - resolution: "ts-morph@npm:21.0.1" - dependencies: - "@ts-morph/common": "npm:~0.22.0" - code-block-writer: "npm:^12.0.0" - checksum: 10c0/ed1d4ccdeba2300cfa236f2aaf64bb462aa386141e659a08d8a2eb96d3b4b274abc9d97b8dd06a0c13af79187b197f38f6a8baca709f99d11094a82c8abccf27 - languageName: node - linkType: hard - "ts-morph@npm:^28.0.0": version: 28.0.0 resolution: "ts-morph@npm:28.0.0" @@ -36267,6 +36411,13 @@ __metadata: languageName: node linkType: hard +"verkit@npm:^0.3.2": + version: 0.3.2 + resolution: "verkit@npm:0.3.2" + checksum: 10c0/2533d9cc4ee6514d7f0d07b035c56a1f3eedd3ec5c3a5b3df38af1f2e9412799476c250f51d034562ddeb36dea72d4530f840e7b80759bd9a527f31cc9aa54e7 + languageName: node + linkType: hard + "verror@npm:1.10.0": version: 1.10.0 resolution: "verror@npm:1.10.0" @@ -36660,6 +36811,67 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^5.0.0": + version: 5.0.1 + resolution: "vitest@npm:5.0.1" + dependencies: + "@types/chai": "npm:^5.2.2" + "@vitest/mocker": "npm:5.0.1" + chai: "npm:^6.2.2" + es-module-lexer: "npm:^2.3.2" + expect-type: "npm:^1.4.0" + magic-string: "npm:^1.2.3" + obug: "npm:^2.1.4" + picomatch: "npm:^4.0.7" + std-env: "npm:^4.2.0" + tinybench: "npm:6.1.4" + tinyexec: "npm:1.3.0" + tinyglobby: "npm:^0.2.17" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 5.0.1 + "@vitest/browser-preview": 5.0.1 + "@vitest/browser-webdriverio": ^5.0.0-beta.5 || >=5.0.0 + "@vitest/coverage-istanbul": 5.0.1 + "@vitest/coverage-v8": 5.0.1 + "@vitest/ui": 5.0.1 + happy-dom: "*" + jsdom: "*" + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/coverage-istanbul": + optional: true + "@vitest/coverage-v8": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vite: + optional: false + bin: + vitest: ./vitest.mjs + checksum: 10c0/1b49fe8eb7274ffd43ca5a29cccbcc942cf991488020a74114f3a725a1168c549d47820715b8ed3f521a0cd9ccc0013ab9511cc6e0f8e0a5bdfb751e3ddc4d2d + languageName: node + linkType: hard + "vlq@npm:^0.2.1": version: 0.2.3 resolution: "vlq@npm:0.2.3" @@ -37434,9 +37646,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.18.0, ws@npm:^8.19.0, ws@npm:^8.21.0, ws@npm:^8.21.1": - version: 8.21.1 - resolution: "ws@npm:8.21.1" +"ws@npm:^8.18.0, ws@npm:^8.21.0, ws@npm:^8.21.1, ws@npm:^8.21.3": + version: 8.21.3 + resolution: "ws@npm:8.21.3" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -37445,7 +37657,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10c0/c4c6f1d95f6d465262de2037c57c715725d67e078dd49420ede4e19115668aba159cb64d85c5e89c06eb2826be599e62e5095860ad6cc54ff42e8bd7684e1db8 + checksum: 10c0/7b28dc2863ea0e2cece68d142a3eee90361021b73f750431e6d8076bb7dede5fdfdb75b3d29534b62f411261147b76b1bc80fb5cc63ab0aabd467280e85b22e0 languageName: node linkType: hard