diff --git a/.external/addon-svelte-csf b/.external/addon-svelte-csf new file mode 160000 index 000000000000..0ff845efcd43 --- /dev/null +++ b/.external/addon-svelte-csf @@ -0,0 +1 @@ +Subproject commit 0ff845efcd43373e5418b70ee83fb7325d33e940 diff --git a/.external/addon-webpack5-compiler-babel b/.external/addon-webpack5-compiler-babel new file mode 160000 index 000000000000..ae95d5f16854 --- /dev/null +++ b/.external/addon-webpack5-compiler-babel @@ -0,0 +1 @@ +Subproject commit ae95d5f168548237319071fe67e6cf51bd3981cf diff --git a/.external/addon-webpack5-compiler-swc b/.external/addon-webpack5-compiler-swc new file mode 160000 index 000000000000..3d64e2bf92ea --- /dev/null +++ b/.external/addon-webpack5-compiler-swc @@ -0,0 +1 @@ +Subproject commit 3d64e2bf92eae064f528ce5e45bdd4ad159ee6a4 diff --git a/.rollout-repos/addon-coverage b/.rollout-repos/addon-coverage new file mode 160000 index 000000000000..0279cd63a671 --- /dev/null +++ b/.rollout-repos/addon-coverage @@ -0,0 +1 @@ +Subproject commit 0279cd63a671d1c35e3c45b4ae35d2f6e0a39ab6 diff --git a/.rollout-repos/addon-designs b/.rollout-repos/addon-designs new file mode 160000 index 000000000000..50824a3e12c5 --- /dev/null +++ b/.rollout-repos/addon-designs @@ -0,0 +1 @@ +Subproject commit 50824a3e12c5004443695f3d58955c884876458e diff --git a/.rollout-repos/addon-kit b/.rollout-repos/addon-kit new file mode 160000 index 000000000000..e17a0f15cab7 --- /dev/null +++ b/.rollout-repos/addon-kit @@ -0,0 +1 @@ +Subproject commit e17a0f15cab7c0ea6fd9e9c5bc0cd6a8f24941ca diff --git a/.rollout-repos/addon-styling-webpack b/.rollout-repos/addon-styling-webpack new file mode 160000 index 000000000000..7df09eb6d759 --- /dev/null +++ b/.rollout-repos/addon-styling-webpack @@ -0,0 +1 @@ +Subproject commit 7df09eb6d7596f03d07aefda1252c707ba488d65 diff --git a/.rollout-repos/addon-visual-tests b/.rollout-repos/addon-visual-tests new file mode 160000 index 000000000000..be1829a2403b --- /dev/null +++ b/.rollout-repos/addon-visual-tests @@ -0,0 +1 @@ +Subproject commit be1829a2403b2bd55ddc694db00fd0f6ade3007d diff --git a/.rollout-repos/addon-webpack5-compiler-babel b/.rollout-repos/addon-webpack5-compiler-babel new file mode 160000 index 000000000000..5d9ade95a91d --- /dev/null +++ b/.rollout-repos/addon-webpack5-compiler-babel @@ -0,0 +1 @@ +Subproject commit 5d9ade95a91dd90e023bc471ce82c1eba2c19988 diff --git a/.rollout-repos/addon-webpack5-compiler-swc b/.rollout-repos/addon-webpack5-compiler-swc new file mode 160000 index 000000000000..5af31673f84b --- /dev/null +++ b/.rollout-repos/addon-webpack5-compiler-swc @@ -0,0 +1 @@ +Subproject commit 5af31673f84b656f19de0ee799ce691ec1ee2d49 diff --git a/.rollout-repos/icons b/.rollout-repos/icons new file mode 160000 index 000000000000..70f13df022f8 --- /dev/null +++ b/.rollout-repos/icons @@ -0,0 +1 @@ +Subproject commit 70f13df022f8a0a8ccc15ad358df967ddc2c2e5d diff --git a/.rollout-repos/telejson b/.rollout-repos/telejson new file mode 160000 index 000000000000..78136d94add2 --- /dev/null +++ b/.rollout-repos/telejson @@ -0,0 +1 @@ +Subproject commit 78136d94add2f441c2321cebd95ff9a793893828 diff --git a/.rollout-repos/test-runner b/.rollout-repos/test-runner new file mode 160000 index 000000000000..c1be8e0ebcb2 --- /dev/null +++ b/.rollout-repos/test-runner @@ -0,0 +1 @@ +Subproject commit c1be8e0ebcb2ba1e3e7374f14a2e89c120339fb2 diff --git a/.rollout-repos/vite-plugin-storybook-nextjs b/.rollout-repos/vite-plugin-storybook-nextjs new file mode 160000 index 000000000000..bbbb87a985dd --- /dev/null +++ b/.rollout-repos/vite-plugin-storybook-nextjs @@ -0,0 +1 @@ +Subproject commit bbbb87a985dd72f5c1f5a18dc3b3e73f650e8b6a diff --git a/code/addons/vitest/src/constants.ts b/code/addons/vitest/src/constants.ts index 81113de40931..96e06983f104 100644 --- a/code/addons/vitest/src/constants.ts +++ b/code/addons/vitest/src/constants.ts @@ -39,6 +39,7 @@ export const storeOptions = { componentTestStatuses: [], a11yStatuses: [], a11yReports: {}, + reports: {}, componentTestCount: { success: 0, error: 0, @@ -66,6 +67,9 @@ export const TEST_PROVIDER_STORE_CHANNEL_EVENT_NAME = 'UNIVERSAL_STORE:storybook export const STATUS_TYPE_ID_COMPONENT_TEST = 'storybook/component-test'; export const STATUS_TYPE_ID_A11Y = 'storybook/a11y'; +export const STORYBOOK_TEST_PROVIDE_KEY = 'storybook/test-provided'; +export const STORYBOOK_CORE_GHOST_STORIES_PROVIDE_KEY = 'storybook/core-ghost-stories'; +export const STORYBOOK_CORE_RENDER_ANALYSIS_PROVIDE_KEY = 'storybook/core-render-analysis'; // Channel event names for programmatic test triggering export const TRIGGER_TEST_RUN_REQUEST = `${ADDON_ID}/trigger-test-run-request`; @@ -75,7 +79,7 @@ export type TriggerTestRunRequestPayload = { requestId: string; actor: string; storyIds?: string[]; - config?: Partial; + config?: Record; }; export type TestRunResult = CurrentRun; diff --git a/code/addons/vitest/src/node/test-manager.test.ts b/code/addons/vitest/src/node/test-manager.test.ts index 12e884fb000d..26ca2ecea936 100644 --- a/code/addons/vitest/src/node/test-manager.test.ts +++ b/code/addons/vitest/src/node/test-manager.test.ts @@ -10,8 +10,14 @@ import type { } from 'storybook/internal/types'; import path from 'pathe'; - -import { STATUS_TYPE_ID_A11Y, STATUS_TYPE_ID_COMPONENT_TEST, storeOptions } from '../constants.ts'; +import type { Report } from 'storybook/preview-api'; + +import { + STATUS_TYPE_ID_A11Y, + STATUS_TYPE_ID_COMPONENT_TEST, + STORYBOOK_TEST_PROVIDE_KEY, + storeOptions, +} from '../constants.ts'; import type { StoreEvent, StoreState } from '../types.ts'; import { TestManager, type TestManagerOptions } from './test-manager.ts'; import { DOUBLE_SPACES } from './vitest-manager.ts'; @@ -22,6 +28,10 @@ const vitest = vi.hoisted(() => ({ init: vi.fn(), close: vi.fn(), onCancel: vi.fn(), + logger: { + clearHighlightCache: vi.fn(), + }, + provide: vi.fn(), runTestSpecifications: vi.fn(), cancelCurrentRun: vi.fn(), globTestSpecifications: vi.fn(), @@ -52,6 +62,13 @@ vi.mock('vitest/node', () => ({ const createVitest = mockCreateVitest; beforeEach(() => { + vi.clearAllMocks(); + mockStore.setState(() => ({ + ...storeOptions.initialState, + index: mockIndex, + })); + vitest.projects = [{}]; + vitest.config.coverage.enabled = false; createVitest.mockResolvedValue(vitest); }); @@ -154,11 +171,15 @@ const mockTestProviderStore: TestProviderStoreById = { const tests = [ { - project: { config: { env: { __STORYBOOK_URL__: 'http://localhost:6006' } } }, + project: { + config: { env: { __STORYBOOK_URL__: 'http://localhost:6006' } }, + }, moduleId: path.join(process.cwd(), 'path/to/file'), }, { - project: { config: { env: { __STORYBOOK_URL__: 'http://localhost:6006' } } }, + project: { + config: { env: { __STORYBOOK_URL__: 'http://localhost:6006' } }, + }, moduleId: path.join(process.cwd(), 'path/to/another/file'), }, ]; @@ -211,9 +232,124 @@ describe('TestManager', () => { }, }); expect(createVitest).toHaveBeenCalledTimes(1); + expect(vitest.provide).toHaveBeenCalledWith(STORYBOOK_TEST_PROVIDE_KEY, { + coverage: false, + a11y: false, + }); expect(vitest.runTestSpecifications).toHaveBeenCalledWith(tests, true); }); + it('should provide merged config override before running tests', async () => { + vitest.globTestSpecifications.mockImplementation(() => tests); + const testManager = await TestManager.start(options); + + await testManager.handleTriggerRunEvent({ + type: 'TRIGGER_RUN', + payload: { + triggeredBy: 'external:actor', + configOverride: { + coverage: false, + a11y: true, + customFlag: 'custom-value', + }, + }, + }); + + expect(vitest.provide).toHaveBeenLastCalledWith(STORYBOOK_TEST_PROVIDE_KEY, { + coverage: false, + a11y: true, + customFlag: 'custom-value', + }); + }); + + it('should refresh provided config before watch-triggered reruns', async () => { + vitest.globTestSpecifications.mockImplementation(() => tests); + vitest.projects = [ + { + config: { + env: { __STORYBOOK_URL__: 'http://localhost:6006' }, + root: process.cwd(), + setupFiles: [], + }, + matchesTestGlob: vi.fn(), + vite: { + moduleGraph: { + getModuleById: vi.fn(), + getModulesByFile: vi.fn(() => []), + invalidateModule: vi.fn(), + }, + transformRequest: vi.fn(), + }, + }, + ] as any; + + const testManager = await TestManager.start(options); + + await testManager.handleTriggerRunEvent({ + type: 'TRIGGER_RUN', + payload: { + triggeredBy: 'global', + }, + }); + + vitest.provide.mockClear(); + mockStore.setState((s) => ({ + ...s, + watching: true, + config: { coverage: false, a11y: true }, + })); + + vi.spyOn(testManager.vitestManager as any, 'getTestDependencies').mockResolvedValue(new Set()); + + await testManager.vitestManager.runAffectedTestsAfterChange(tests[0].moduleId, 'change'); + + expect(vitest.provide).toHaveBeenCalledWith(STORYBOOK_TEST_PROVIDE_KEY, { + coverage: false, + a11y: true, + }); + expect(vitest.runTestSpecifications).toHaveBeenLastCalledWith(tests.slice(0, 1), false); + }); + + it('should persist all reports in currentRun', async () => { + const testManager = await TestManager.start(options); + const passedResult = { + state: 'passed', + errors: [], + } as unknown as TestResult; + + await testManager.runTestsWithState({ + storyIds: ['story--one'], + triggeredBy: 'global', + callback: async () => { + testManager.onTestCaseResult({ + storyId: 'story--one', + testResult: passedResult, + reports: [ + { + type: 'a11y', + status: 'passed', + result: { id: 'a11y-report' }, + } as Report, + { + type: 'custom', + status: 'passed', + result: { id: 'custom-report' }, + } as Report, + ], + }); + testManager.onTestRunEnd({ + totalTestCount: 1, + unhandledErrors: [], + }); + }, + }); + + expect(mockStore.getState().currentRun.reports['story--one']).toEqual([ + { type: 'a11y', status: 'passed', result: { id: 'a11y-report' } }, + { type: 'custom', status: 'passed', result: { id: 'custom-report' } }, + ]); + }); + it('should filter tests', async () => { vitest.globTestSpecifications.mockImplementation(() => tests); const testManager = await TestManager.start(options); @@ -325,7 +461,10 @@ describe('TestManager', () => { it('should ignore non-requested same-name story results after run', async () => { const testManager = await TestManager.start(options); - const passedResult = { state: 'passed', errors: [] } as unknown as TestResult; + const passedResult = { + state: 'passed', + errors: [], + } as unknown as TestResult; await testManager.runTestsWithState({ storyIds: ['story--one', 'another--two'], @@ -357,7 +496,10 @@ describe('TestManager', () => { it('should keep child test results when parent story is requested', async () => { const testManager = await TestManager.start(options); - const passedResult = { state: 'passed', errors: [] } as unknown as TestResult; + const passedResult = { + state: 'passed', + errors: [], + } as unknown as TestResult; await testManager.runTestsWithState({ storyIds: ['parent--story'], @@ -385,7 +527,10 @@ describe('TestManager', () => { expect(createVitest).toHaveBeenCalledTimes(1); createVitest.mockClear(); - mockStore.setState((s) => ({ ...s, config: { coverage: true, a11y: false } })); + mockStore.setState((s) => ({ + ...s, + config: { coverage: true, a11y: false }, + })); await testManager.handleTriggerRunEvent({ type: 'TRIGGER_RUN', @@ -408,7 +553,10 @@ describe('TestManager', () => { expect(createVitest).toHaveBeenCalledTimes(1); createVitest.mockClear(); - mockStore.setState((s) => ({ ...s, config: { coverage: true, a11y: false } })); + mockStore.setState((s) => ({ + ...s, + config: { coverage: true, a11y: false }, + })); await testManager.handleTriggerRunEvent({ type: 'TRIGGER_RUN', diff --git a/code/addons/vitest/src/node/test-manager.ts b/code/addons/vitest/src/node/test-manager.ts index d795ab1ecea6..83d945b780ec 100644 --- a/code/addons/vitest/src/node/test-manager.ts +++ b/code/addons/vitest/src/node/test-manager.ts @@ -8,14 +8,13 @@ import type { TestProviderStoreById, } from 'storybook/internal/types'; -import type { A11yReport } from '@storybook/addon-a11y'; - import { throttle } from 'es-toolkit/function'; import type { Report } from 'storybook/preview-api'; import { STATUS_TYPE_ID_A11Y, STATUS_TYPE_ID_COMPONENT_TEST, storeOptions } from '../constants.ts'; import type { CurrentRun, + RunConfig, RunTrigger, StoreEvent, StoreState, @@ -79,7 +78,9 @@ export class TestManager { this.store .untilReady() .then(() => { - return this.vitestManager.startVitest({ coverage: this.store.getState().config.coverage }); + return this.vitestManager.startVitest({ + coverage: this.store.getState().config.coverage, + }); }) .then(() => this.onReady?.()) .catch((e) => { @@ -129,7 +130,7 @@ export class TestManager { }: { storyIds?: string[]; triggeredBy: RunTrigger; - configOverride?: StoreState['config']; + configOverride?: RunConfig; callback: () => Promise; }) { this.componentTestStatusStore.unset(storyIds); @@ -147,10 +148,6 @@ export class TestManager { config: runConfig, }, })); - // set the config at the start of a test run, - // so that changing the config during the test run does not affect the currently running test run - process.env.VITEST_STORYBOOK_CONFIG = JSON.stringify(runConfig); - await this.testProviderStore.runWithState(async () => { await callback(); this.store.send({ @@ -229,14 +226,19 @@ export class TestManager { this.componentTestStatusStore.set(componentTestStatuses); const a11yReportsByStoryId: CurrentRun['a11yReports'] = {}; + const reportsByStoryId: CurrentRun['reports'] = {}; const a11yStatuses: typeof componentTestStatuses = []; for (const { storyId, reports } of testCaseResultsToFlush) { + if (reports?.length) { + reportsByStoryId[storyId] = reports; + } + const storyA11yReports = reports?.filter((r) => r.type === 'a11y'); if (!storyA11yReports?.length) { continue; } - a11yReportsByStoryId[storyId] = storyA11yReports.map((r) => r.result) as A11yReport[]; + a11yReportsByStoryId[storyId] = storyA11yReports.map((report) => report.result); for (const a11yReport of storyA11yReports) { a11yStatuses.push({ storyId, @@ -281,13 +283,25 @@ export class TestManager { currentRun: { ...s.currentRun, componentTestCount: { success: ctSuccess, error: ctError }, - a11yCount: { success: a11ySuccess, warning: a11yWarning, error: a11yError }, + a11yCount: { + success: a11ySuccess, + warning: a11yWarning, + error: a11yError, + }, componentTestStatuses: s.currentRun.componentTestStatuses.concat(componentTestStatuses), a11yStatuses: s.currentRun.a11yStatuses.concat(a11yStatuses), + /* + TODO: a11yReports is just here for backwards compatibility with older versions of addon-mcp. + They are also part of the more generic reports property, so we can remove this in a future major release when we can break compatibility. + */ a11yReports: { ...s.currentRun.a11yReports, ...a11yReportsByStoryId, }, + reports: { + ...s.currentRun.reports, + ...reportsByStoryId, + }, // in some cases successes and errors can exceed the anticipated totalTestCount // e.g. when testing more tests than the stories we know about upfront // in those cases, we set the totalTestCount to the sum of successes and errors diff --git a/code/addons/vitest/src/node/vitest-manager.ts b/code/addons/vitest/src/node/vitest-manager.ts index ca1ed83f9f9a..b5a6a9eb56d4 100644 --- a/code/addons/vitest/src/node/vitest-manager.ts +++ b/code/addons/vitest/src/node/vitest-manager.ts @@ -19,7 +19,7 @@ import path, { dirname, join, normalize, resolve } from 'pathe'; // eslint-disable-next-line depend/ban-dependencies import slash from 'slash'; -import { COVERAGE_DIRECTORY } from '../constants.ts'; +import { COVERAGE_DIRECTORY, STORYBOOK_TEST_PROVIDE_KEY } from '../constants.ts'; import { log } from '../logger.ts'; import type { TriggerRunEvent } from '../types.ts'; import type { StorybookCoverageReporterOptions } from './coverage-reporter.ts'; @@ -102,7 +102,10 @@ export class VitestManager { for (const location of potentialConfigFileLocations) { for (const file of configFiles) { - const maybe = find.any([file], { cwd: location, last: getProjectRoot() }); + const maybe = find.any([file], { + cwd: location, + last: getProjectRoot(), + }); if (maybe && existsSync(maybe)) { firstVitestConfig ??= dirname(maybe); const content = readFileSync(maybe, 'utf8'); @@ -363,10 +366,19 @@ export class VitestManager { return { filteredTestSpecifications, filteredStoryIds }; } + private getCurrentRunConfig() { + return this.testManager.store.getState().currentRun.config; + } + + private provideRunConfig() { + this.vitest?.provide(STORYBOOK_TEST_PROVIDE_KEY, this.getCurrentRunConfig()); + } + async runTests(runPayload: TriggerRunEvent['payload']) { - const { watching, config } = this.testManager.store.getState(); + const { watching } = this.testManager.store.getState(); + const runConfig = this.getCurrentRunConfig(); const coverageShouldBeEnabled = - config.coverage && !watching && (runPayload?.storyIds?.length ?? 0) === 0; + !!runConfig.coverage && !watching && (runPayload?.storyIds?.length ?? 0) === 0; const currentCoverage = this.vitest?.config.coverage?.enabled; if (!this.vitest) { @@ -377,6 +389,8 @@ export class VitestManager { await this.vitestRestartPromise; } + this.provideRunConfig(); + this.resetGlobalTestNamePattern(); await this.cancelCurrentRun(); @@ -519,6 +533,7 @@ export class VitestManager { })); await this.vitest!.cancelCurrentRun('keyboard-input'); await this.runningPromise; + this.provideRunConfig(); await this.vitest!.runTestSpecifications(filteredTestSpecifications, false); }, }); diff --git a/code/addons/vitest/src/types.ts b/code/addons/vitest/src/types.ts index e1f4d64ccdd7..362cb515f595 100644 --- a/code/addons/vitest/src/types.ts +++ b/code/addons/vitest/src/types.ts @@ -1,10 +1,7 @@ import type { experimental_UniversalStore } from 'storybook/internal/core-server'; import type { PreviewAnnotation, Status, StoryId, StoryIndex } from 'storybook/internal/types'; import type { API_HashEntry } from 'storybook/internal/types'; - -// import type { A11yReport } from '@storybook/addon-a11y'; -// TODO: There's a type error in axe-core that makes this error during production builds -type A11yReport = any; +import type { Report } from 'storybook/preview-api'; export interface VitestError extends Error { VITEST_TEST_PATH?: string; @@ -24,6 +21,8 @@ export type ErrorLike = { cause?: ErrorLike; }; +export type RunConfig = Record; + export type RunTrigger = | 'run-all' | 'global' @@ -31,9 +30,11 @@ export type RunTrigger = | Extract | `external:${string}`; +export type A11yRunReport = Report['result']; + export type CurrentRun = { triggeredBy: RunTrigger | undefined; - config: StoreState['config']; + config: RunConfig; componentTestStatuses: Status[]; a11yStatuses: Status[]; componentTestCount: { @@ -45,7 +46,9 @@ export type CurrentRun = { warning: number; error: number; }; - a11yReports: Record; + // Backwards compatibility for consumers that still read the legacy a11y-only shape. + a11yReports: Record; + reports: Record; totalTestCount: number | undefined; storyIds: StoryId[] | undefined; startedAt: number | undefined; @@ -84,7 +87,7 @@ export type TriggerRunEvent = { payload: { storyIds?: string[] | undefined; triggeredBy: RunTrigger; - configOverride?: StoreState['config']; + configOverride?: RunConfig; }; }; diff --git a/code/addons/vitest/src/vitest-plugin/index.ts b/code/addons/vitest/src/vitest-plugin/index.ts index 8c61af2e65a0..82cd351a54af 100644 --- a/code/addons/vitest/src/vitest-plugin/index.ts +++ b/code/addons/vitest/src/vitest-plugin/index.ts @@ -40,6 +40,10 @@ import type { PluginOption } from 'vite'; // Shared plugins from builder-vite (relative import to prebundle without adding a package dependency) import { withoutVitePlugins } from '../../../../builders/builder-vite/src/utils/without-vite-plugins.ts'; +import { + STORYBOOK_CORE_GHOST_STORIES_PROVIDE_KEY, + STORYBOOK_CORE_RENDER_ANALYSIS_PROVIDE_KEY, +} from '../constants.ts'; import type { InternalOptions, UserOptions } from './types.ts'; import { requiresProjectAnnotations } from './utils.ts'; import { AgentTelemetryReporter } from './agent-telemetry-reporter.ts'; @@ -361,6 +365,11 @@ export const storybookTest = async (options?: UserOptions): Promise => __VITEST_SKIP_TAGS__: finalOptions.tags.skip.join(','), }, + provide: { + [STORYBOOK_CORE_GHOST_STORIES_PROVIDE_KEY]: !!process.env.STORYBOOK_COMPONENT_PATHS, + [STORYBOOK_CORE_RENDER_ANALYSIS_PROVIDE_KEY]: !!process.env.STORYBOOK_COMPONENT_PATHS, + }, + include: [...includeStories, ...getComponentTestPaths()], exclude: [ ...(nonMutableInputConfig.test?.exclude ?? []), @@ -380,34 +389,6 @@ export const storybookTest = async (options?: UserOptions): Promise => : {}), browser: { - commands: { - getInitialGlobals: () => { - const envConfig = JSON.parse(process.env.VITEST_STORYBOOK_CONFIG ?? '{}'); - - const shouldRunA11yTests = isVitestStorybook ? (envConfig.a11y ?? false) : true; - const globals: Record = {}; - globals.a11y = { - manual: !shouldRunA11yTests, - }; - - if (process.env.STORYBOOK_COMPONENT_PATHS) { - globals.ghostStories = { - enabled: true, - }; - globals.renderAnalysis = { - enabled: true, - }; - } - - if (withinAgenticSetupSession) { - globals.renderAnalysis = { - enabled: true, - }; - } - - return globals; - }, - }, // if there is a test.browser config AND test.browser.screenshotFailures is not explicitly set, we set it to false ...(nonMutableInputConfig.test?.browser && nonMutableInputConfig.test.browser.screenshotFailures === undefined @@ -479,6 +460,9 @@ export const storybookTest = async (options?: UserOptions): Promise => // detailed test result telemetry (pass/fail, error analysis, empty renders) const agent = detectAgent(); withinAgenticSetupSession = !!agent && (await isWithinInitialSession('ai-setup')); + if (withinAgenticSetupSession) { + await context.vitest.provide(STORYBOOK_CORE_RENDER_ANALYSIS_PROVIDE_KEY, true); + } if (agent && withinAgenticSetupSession) { context.vitest.config.reporters.push( new AgentTelemetryReporter({ diff --git a/code/addons/vitest/src/vitest-plugin/test-utils.ts b/code/addons/vitest/src/vitest-plugin/test-utils.ts index 876ced8f055c..5ca0ebca8fd7 100644 --- a/code/addons/vitest/src/vitest-plugin/test-utils.ts +++ b/code/addons/vitest/src/vitest-plugin/test-utils.ts @@ -1,21 +1,17 @@ -import { type RunnerTask, type TaskMeta, type TestContext } from 'vitest'; +import { inject, type RunnerTask, type TaskMeta, type TestContext } from 'vitest'; import { type Meta, type Story, getStoryChildren, isStory } from 'storybook/internal/csf'; import type { ComponentAnnotations, ComposedStoryFn, Renderer } from 'storybook/internal/types'; -import { server } from '@vitest/browser/context'; import { type Report, composeStory, getCsfFactoryAnnotations } from 'storybook/preview-api'; +import { + STORYBOOK_CORE_GHOST_STORIES_PROVIDE_KEY, + STORYBOOK_CORE_RENDER_ANALYSIS_PROVIDE_KEY, + STORYBOOK_TEST_PROVIDE_KEY, +} from '../constants.ts'; import { setViewport } from './viewports.ts'; -declare module 'vitest/browser' { - interface BrowserCommands { - getInitialGlobals: () => Promise>; - } -} - -const { getInitialGlobals } = server.commands; - /** * Converts a file URL to a file path, handling URL encoding * @@ -60,10 +56,53 @@ export const testStory = ({ const storyAnnotations = test ? test.input : annotations.story; + let runConfig: Record = { a11y: true }; + try { + runConfig = inject(STORYBOOK_TEST_PROVIDE_KEY) ?? { a11y: true }; + } catch { + // Standalone Vitest runs might not provide Storybook run config. + } + + let ghostStoriesEnabled = false; + try { + ghostStoriesEnabled = inject(STORYBOOK_CORE_GHOST_STORIES_PROVIDE_KEY) ?? false; + } catch { + // Standalone Vitest runs might not provide Storybook ghost stories config. + } + + let renderAnalysisEnabled = false; + try { + renderAnalysisEnabled = inject(STORYBOOK_CORE_RENDER_ANALYSIS_PROVIDE_KEY) ?? false; + } catch { + // Standalone Vitest runs might not provide Storybook render analysis config. + } + + const shouldRunA11yTests = !!runConfig.a11y; + const initialGlobals = { + [STORYBOOK_TEST_PROVIDE_KEY]: runConfig, + ...(ghostStoriesEnabled + ? { + ghostStories: { + enabled: true, + }, + } + : {}), + ...(renderAnalysisEnabled + ? { + renderAnalysis: { + enabled: true, + }, + } + : {}), + a11y: { + manual: !shouldRunA11yTests, + }, + }; + const composedStory = composeStory( storyAnnotations, annotations.meta!, - { initialGlobals: (await getInitialGlobals?.()) ?? {} }, + { initialGlobals }, annotations.preview ?? globalThis.globalProjectAnnotations, exportName ); diff --git a/code/addons/vitest/src/vitest-provided-context.d.ts b/code/addons/vitest/src/vitest-provided-context.d.ts new file mode 100644 index 000000000000..fd5071d82e18 --- /dev/null +++ b/code/addons/vitest/src/vitest-provided-context.d.ts @@ -0,0 +1,15 @@ +import 'vitest'; + +import type { + STORYBOOK_CORE_GHOST_STORIES_PROVIDE_KEY, + STORYBOOK_CORE_RENDER_ANALYSIS_PROVIDE_KEY, + STORYBOOK_TEST_PROVIDE_KEY, +} from './constants.ts'; + +declare module 'vitest' { + interface ProvidedContext { + [STORYBOOK_TEST_PROVIDE_KEY]: Record; + [STORYBOOK_CORE_GHOST_STORIES_PROVIDE_KEY]: boolean; + [STORYBOOK_CORE_RENDER_ANALYSIS_PROVIDE_KEY]: boolean; + } +}