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
40 changes: 40 additions & 0 deletions code/addons/vitest/src/node/test-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,46 @@ describe('TestManager', () => {
]);
});

it('should describe failures with the source-mapped frames rather than the raw browser stack', async () => {
const testManager = await TestManager.start(options);
const failedResult = {
state: 'failed',
errors: [
{
message:
'\n\x1B[34mClick to debug the error directly in Storybook: http://localhost:6006/?path=/story/story--one\x1B[39m\n\nexpect(element).toBeInTheDocument()',
stack:
'Error: expect(element).toBeInTheDocument()\n at Proxy.expectWrapper (http://localhost:6006/deps/storybook_test.js?v=8c5dc1d5:13691:16)',
stacks: [
{
method: 'toBeInTheDocument',
file: '/project/src/stories/Page.stories.ts',
line: 30,
column: 60,
},
],
},
],
} as unknown as TestResult;

await testManager.runTestsWithState({
storyIds: ['story--one'],
triggeredBy: 'global',
callback: async () => {
testManager.onTestCaseResult({ storyId: 'story--one', testResult: failedResult });
testManager.onTestRunEnd({ totalTestCount: 1, unhandledErrors: [] });
},
});

expect(mockComponentTestStatusStore.set).toHaveBeenCalledWith([
expect.objectContaining({
storyId: 'story--one',
description:
'expect(element).toBeInTheDocument()\n at toBeInTheDocument (/project/src/stories/Page.stories.ts:30:60)',
}),
]);
});

it('should filter tests', async () => {
vitest.globTestSpecifications.mockImplementation(() => tests);
const testManager = await TestManager.start(options);
Expand Down
24 changes: 23 additions & 1 deletion code/addons/vitest/src/node/test-manager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { TestError } from 'vitest';
import type { TestResult, TestState } from 'vitest/node';

import type { experimental_UniversalStore } from 'storybook/internal/core-server';
Expand Down Expand Up @@ -37,6 +38,27 @@ export type TestManagerOptions = {
onReady?: () => void;
};

/** Matches the banner that vitest-plugin/setup-file.ts prepends to the message of failed stories. */
const DEBUG_BANNER_RE =
/\n?(?:\x1B\[\d+m)?Click to debug the error directly in Storybook: [^\n]*\n+/g;

/**
* `error.stack` holds the raw browser stack, pointing at the Vite URLs of Storybook's pre-bundled
* internals. Vitest replaces it with `error.stacks`: source-mapped frames with Storybook's
* instrumentation filtered out, matching what its own terminal output shows.
*/
function formatError(error: TestError): string {
if (!error.stacks?.length) {
return error.stack || error.message || '';
}
const message = (error.message ?? '').replace(DEBUG_BANNER_RE, '');
const frames = error.stacks.map(
({ method, file, line, column }) =>
` at ${method || '<anonymous>'} (${file}:${line}:${column})`
);
return [message, ...frames].join('\n');
}

const testStateToStatusValueMap: Record<TestState | 'warning', StatusValue> = {
pending: 'status-value:pending',
passed: 'status-value:success',
Expand Down Expand Up @@ -225,7 +247,7 @@ export class TestManager {
typeId: STATUS_TYPE_ID_COMPONENT_TEST,
value: testStateToStatusValueMap[testResult.state],
title: 'Component tests',
description: testResult.errors?.map((error) => error.stack || error.message).join('\n') ?? '',
description: testResult.errors?.map(formatError).join('\n') ?? '',
sidebarContextMenu: false,
}));

Expand Down
9 changes: 9 additions & 0 deletions code/addons/vitest/src/vitest-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
import type { InternalOptions, UserOptions } from './types.ts';
import { requiresProjectAnnotations } from './utils.ts';
import { AgentTelemetryReporter } from './agent-telemetry-reporter.ts';
import { isStorybookInternalFrame } from './stack-frames.ts';

const WORKING_DIR = process.cwd();

Expand Down Expand Up @@ -345,6 +346,14 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
cacheDir: resolvePathInStorybookCache('sb-vitest', projectId),
test: {
expect: { requireAssertions: false },

onStackTrace: (error, frame) => {
if (isStorybookInternalFrame(frame.file)) {
return false;
}
return nonMutableInputConfig.test?.onStackTrace?.(error, frame) ?? true;
},

setupFiles: [
...internalSetupFiles,
// if the existing setupFiles is a string, we have to include it otherwise we're overwriting it
Expand Down
28 changes: 28 additions & 0 deletions code/addons/vitest/src/vitest-plugin/stack-frames.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';

import { isStorybookInternalFrame } from './stack-frames.ts';

describe('isStorybookInternalFrame', () => {
it.each([
'/project/node_modules/storybook/dist/test/index.js',
'/project/node_modules/storybook/dist/instrumenter/index.js',
'/project/node_modules/storybook/dist/_browser-chunks/chunk-ORWLP677.js',
'/project/node_modules/@storybook/addon-vitest/dist/vitest-plugin/test-utils.js',
'C:\\project\\node_modules\\storybook\\dist\\instrumenter\\index.js',
'/project/node_modules/.cache/storybook/10.6.0/hash/sb-vitest/deps/storybook_test.js',
'/project/node_modules/.cache/storybook/10.6.0/hash/sb-vitest/deps/@storybook_addon-vitest_internal_test-utils.js?v=777b44a5',
])('filters out %s', (file) => {
expect(isStorybookInternalFrame(file)).toBe(true);
});

it.each([
'/project/src/stories/Page.stories.ts',
'/project/src/stories/Page.tsx',
'/project/.storybook/preview.ts',
'/project/node_modules/@testing-library/dom/dist/index.js',
'/project/node_modules/.cache/storybook/10.6.0/hash/sb-vitest/deps/react-dom_client.js',
undefined,
])('keeps %s', (file) => {
expect(isStorybookInternalFrame(file)).toBe(false);
});
});
19 changes: 19 additions & 0 deletions code/addons/vitest/src/vitest-plugin/stack-frames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Storybook instruments `expect`, `userEvent` and Testing Library so the Interactions panel can
* replay a play function step by step. Every instrumented call therefore sits behind a handful of
* wrapper frames inside Storybook's own bundles, which push the failing line in the story far down
* the stack trace Vitest reports.
*
* These patterns match those bundles in the shapes a frame can take: the published package layout
* after source maps are applied, and the pre-bundled dependency files Vite serves when no source
* map is available.
*/
const INTERNAL_FRAME_PATTERNS = [
/[\\/]storybook[\\/]dist[\\/]/,
/[\\/]@storybook[\\/][^\\/]+[\\/]dist[\\/]/,
/[\\/]deps[\\/]@?storybook[@_]/,
];

export function isStorybookInternalFrame(file: string | undefined): boolean {
return !!file && INTERNAL_FRAME_PATTERNS.some((pattern) => pattern.test(file));
}
Loading