Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .external/addon-svelte-csf
Submodule addon-svelte-csf added at 0ff845
1 change: 1 addition & 0 deletions .external/addon-webpack5-compiler-babel
Submodule addon-webpack5-compiler-babel added at ae95d5
1 change: 1 addition & 0 deletions .external/addon-webpack5-compiler-swc
Submodule addon-webpack5-compiler-swc added at 3d64e2
1 change: 1 addition & 0 deletions .rollout-repos/addon-coverage
Submodule addon-coverage added at 0279cd
1 change: 1 addition & 0 deletions .rollout-repos/addon-designs
Submodule addon-designs added at 50824a
1 change: 1 addition & 0 deletions .rollout-repos/addon-kit
Submodule addon-kit added at e17a0f
1 change: 1 addition & 0 deletions .rollout-repos/addon-styling-webpack
Submodule addon-styling-webpack added at 7df09e
1 change: 1 addition & 0 deletions .rollout-repos/addon-visual-tests
Submodule addon-visual-tests added at be1829
1 change: 1 addition & 0 deletions .rollout-repos/addon-webpack5-compiler-babel
Submodule addon-webpack5-compiler-babel added at 5d9ade
1 change: 1 addition & 0 deletions .rollout-repos/addon-webpack5-compiler-swc
Submodule addon-webpack5-compiler-swc added at 5af316
1 change: 1 addition & 0 deletions .rollout-repos/icons
Submodule icons added at 70f13d
1 change: 1 addition & 0 deletions .rollout-repos/telejson
Submodule telejson added at 78136d
1 change: 1 addition & 0 deletions .rollout-repos/test-runner
Submodule test-runner added at c1be8e
1 change: 1 addition & 0 deletions .rollout-repos/vite-plugin-storybook-nextjs
Submodule vite-plugin-storybook-nextjs added at bbbb87
6 changes: 5 additions & 1 deletion code/addons/vitest/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const storeOptions = {
componentTestStatuses: [],
a11yStatuses: [],
a11yReports: {},
reports: {},
componentTestCount: {
success: 0,
error: 0,
Expand Down Expand Up @@ -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`;
Expand All @@ -75,7 +79,7 @@ export type TriggerTestRunRequestPayload = {
requestId: string;
actor: string;
storyIds?: string[];
config?: Partial<StoreState['config']>;
config?: Record<string, unknown>;
};

export type TestRunResult = CurrentRun;
Expand Down
164 changes: 156 additions & 8 deletions code/addons/vitest/src/node/test-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(),
Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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'),
},
];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down
34 changes: 24 additions & 10 deletions code/addons/vitest/src/node/test-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -129,7 +130,7 @@ export class TestManager {
}: {
storyIds?: string[];
triggeredBy: RunTrigger;
configOverride?: StoreState['config'];
configOverride?: RunConfig;
callback: () => Promise<void>;
}) {
this.componentTestStatusStore.unset(storyIds);
Expand All @@ -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({
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading