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
10 changes: 10 additions & 0 deletions code/addons/vitest/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ const config: BuildEntries = {
entryPoint: './src/vitest-plugin/setup-file.ts',
dts: false,
},
{
exportEntries: ['./internal/setup-file.browser.3'],
entryPoint: './src/vitest-plugin/setup-file.browser.3.ts',
dts: false,
},
{
exportEntries: ['./internal/setup-file.browser.4'],
entryPoint: './src/vitest-plugin/setup-file.browser.4.ts',
dts: false,
},
{
exportEntries: ['./internal/setup-file-with-project-annotations'],
entryPoint: './src/vitest-plugin/setup-file-with-project-annotations.ts',
Expand Down
2 changes: 2 additions & 0 deletions code/addons/vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
"./internal/global-setup": "./dist/vitest-plugin/global-setup.js",
"./internal/setup-file": "./dist/vitest-plugin/setup-file.js",
"./internal/setup-file-with-project-annotations": "./dist/vitest-plugin/setup-file-with-project-annotations.js",
"./internal/setup-file.browser.3": "./dist/vitest-plugin/setup-file.browser.3.js",
"./internal/setup-file.browser.4": "./dist/vitest-plugin/setup-file.browser.4.js",
"./internal/test-utils": "./dist/vitest-plugin/test-utils.js",
"./manager": "./dist/manager.js",
"./package.json": "./package.json",
Expand Down
29 changes: 22 additions & 7 deletions code/addons/vitest/src/vitest-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,13 +324,11 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
finalOptions
);

const internalSetupFiles = (
[
'@storybook/addon-vitest/internal/setup-file',
areProjectAnnotationRequired &&
'@storybook/addon-vitest/internal/setup-file-with-project-annotations',
].filter(Boolean) as string[]
).map((filePath) => fileURLToPath(import.meta.resolve(filePath)));
const internalSetupFiles = [
'@storybook/addon-vitest/internal/setup-file',
areProjectAnnotationRequired &&
'@storybook/addon-vitest/internal/setup-file-with-project-annotations',
].filter(Boolean) as string[];

const baseConfig: Omit<ViteUserConfig, 'plugins'> = {
cacheDir: resolvePathInStorybookCache('sb-vitest', projectId),
Expand Down Expand Up @@ -423,6 +421,8 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
optimizeDeps: {
include: [
'@storybook/addon-vitest/internal/setup-file',
'@storybook/addon-vitest/internal/setup-file.browser.3',
'@storybook/addon-vitest/internal/setup-file.browser.4',
'@storybook/addon-vitest/internal/global-setup',
'@storybook/addon-vitest/internal/test-utils',
'storybook/preview-api',
Expand Down Expand Up @@ -464,6 +464,21 @@ export const storybookTest = async (options?: UserOptions): Promise<Plugin[]> =>
async configureVitest(context) {
context.vitest.config.coverage.exclude.push('storybook-static');

const isBrowserModeEnabled = context.vitest.config.browser?.enabled === true;

if (isBrowserModeEnabled) {
const setupFilePath = context.vitest.version.startsWith('3')
? '@storybook/addon-vitest/internal/setup-file.browser.3'
: '@storybook/addon-vitest/internal/setup-file.browser.4';

context.vitest.config.setupFiles = [
setupFilePath,
...(context.vitest.config.setupFiles ?? []).filter(
(configuredSetupFile) => configuredSetupFile !== setupFilePath
),
];
}

// NOTE: we start telemetry immediately but do not wait on it. Typically it should complete
// before the tests do. If not we may miss the event, we are OK with that.
telemetry(
Expand Down
15 changes: 15 additions & 0 deletions code/addons/vitest/src/vitest-plugin/setup-file.browser.3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { beforeEach } from 'vitest';

import { commands } from '@vitest/browser/context';

import { isFunction } from 'es-toolkit/predicate';

export const resetMousePositionBeforeTests = async () => {
if ('resetMousePosition' in commands && isFunction(commands.resetMousePosition)) {
await commands.resetMousePosition();
}
};

beforeEach(async () => {
await resetMousePositionBeforeTests();
});
15 changes: 15 additions & 0 deletions code/addons/vitest/src/vitest-plugin/setup-file.browser.4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { beforeEach } from 'vitest';

import { commands } from 'vitest/browser';

import { isFunction } from 'es-toolkit/predicate';

export const resetMousePositionBeforeTests = async () => {
if ('resetMousePosition' in commands && isFunction(commands.resetMousePosition)) {
await commands.resetMousePosition();
}
};

beforeEach(async () => {
await resetMousePositionBeforeTests();
});
70 changes: 36 additions & 34 deletions code/addons/vitest/src/vitest-plugin/setup-file.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { type Task, modifyErrorMessage, resetMousePositionBeforeTests } from './setup-file.ts';
import { Channel } from 'storybook/internal/channels';

import { type Task, initTransport, modifyErrorMessage } from './setup-file.ts';

describe('initTransport', () => {
afterEach(() => {
// Cleanup the global channel so each test can assert initialization behavior independently.

(globalThis as { __STORYBOOK_ADDONS_CHANNEL__?: Channel }).__STORYBOOK_ADDONS_CHANNEL__ =
undefined;
});

it('should initialize the addons channel when missing', () => {
(globalThis as { __STORYBOOK_ADDONS_CHANNEL__?: Channel }).__STORYBOOK_ADDONS_CHANNEL__ =
undefined;

initTransport();

expect(globalThis.__STORYBOOK_ADDONS_CHANNEL__).toBeInstanceOf(Channel);
});

it('should not overwrite an existing addons channel', () => {
const transport = { setHandler: vi.fn(), send: vi.fn() };
const existingChannel = new Channel({ transport });
globalThis.__STORYBOOK_ADDONS_CHANNEL__ = existingChannel;

initTransport();

expect(globalThis.__STORYBOOK_ADDONS_CHANNEL__).toBe(existingChannel);
});
});

describe('modifyErrorMessage', () => {
const originalUrl = import.meta.env.__STORYBOOK_URL__;
Expand Down Expand Up @@ -81,6 +111,7 @@ describe('modifyErrorMessage', () => {
describe('resetMousePositionBeforeTests', () => {
afterEach(() => {
vi.clearAllMocks();
vi.resetModules();
vi.doUnmock('vitest/browser');
vi.doUnmock('@vitest/browser/context');
});
Expand All @@ -94,6 +125,8 @@ describe('resetMousePositionBeforeTests', () => {
},
}));

const { resetMousePositionBeforeTests } = await import('./setup-file.browser.4.ts');

await resetMousePositionBeforeTests();

expect(resetMousePosition).toHaveBeenCalledTimes(1);
Expand All @@ -106,39 +139,8 @@ describe('resetMousePositionBeforeTests', () => {
},
}));

await expect(resetMousePositionBeforeTests()).resolves.toBeUndefined();
});

it('should rethrow unexpected errors', async () => {
const error = new Error('boom');

vi.doMock('vitest/browser', () => {
throw error;
});

await expect(resetMousePositionBeforeTests()).rejects.toThrow();
});

it('should fallback to vitest v3 browser context when vitest/browser is not found', async () => {
const resetMousePosition = vi.fn().mockResolvedValue(undefined);

vi.doMock('vitest/browser', () => {
const browser = {};
Object.defineProperty(browser, 'commands', {
get: () => {
throw new Error("Cannot find module 'vitest/browser'");
},
});
return browser;
});
vi.doMock('@vitest/browser/context', () => ({
commands: {
resetMousePosition,
},
}));

await resetMousePositionBeforeTests();
const { resetMousePositionBeforeTests } = await import('./setup-file.browser.4.ts');

expect(resetMousePosition).toHaveBeenCalledTimes(1);
await expect(resetMousePositionBeforeTests()).resolves.toBeUndefined();
});
});
61 changes: 6 additions & 55 deletions code/addons/vitest/src/vitest-plugin/setup-file.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { beforeEach, afterEach, beforeAll, vi } from 'vitest';
import { afterEach, beforeAll, vi } from 'vitest';
import type { RunnerTask } from 'vitest';

import { Channel } from 'storybook/internal/channels';

import { COMPONENT_TESTING_PANEL_ID } from '../constants.ts';
import { isFunction } from 'es-toolkit/predicate';

declare global {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand All @@ -17,8 +16,10 @@ export type Task = Partial<RunnerTask> & {
meta: Record<string, any>;
};

const transport = { setHandler: vi.fn(), send: vi.fn() };
globalThis.__STORYBOOK_ADDONS_CHANNEL__ ??= new Channel({ transport });
export const initTransport = () => {
const transport = { setHandler: vi.fn(), send: vi.fn() };
globalThis.__STORYBOOK_ADDONS_CHANNEL__ ??= new Channel({ transport });
};

export const modifyErrorMessage = ({ task }: { task: Task }) => {
const meta = task.meta;
Expand All @@ -35,62 +36,12 @@ export const modifyErrorMessage = ({ task }: { task: Task }) => {
}
};

export const resetMousePositionBeforeTests = async () => {
try {
const browserCommands = await import('vitest/browser').then((module) => module.commands);
if ('resetMousePosition' in browserCommands && isFunction(browserCommands.resetMousePosition)) {
await browserCommands.resetMousePosition();
}
} catch (error) {
// Retry with Vitest 3 context module when vitest/browser is not found.
if (error instanceof Error && error.message.includes("Cannot find module 'vitest/browser'")) {
try {
const browserCommands = await import('@vitest/browser/context').then(
(module) => module.commands
);
if (
'resetMousePosition' in browserCommands &&
isFunction(browserCommands.resetMousePosition)
) {
await browserCommands.resetMousePosition();
}
return;
} catch (vitest3Error) {
if (
vitest3Error instanceof Error &&
vitest3Error.message.includes("Cannot find module '@vitest/browser/context'")
) {
return;
}
if (
vitest3Error instanceof Error &&
vitest3Error.message.includes('can be imported only inside the Browser Mode')
) {
return;
}
throw vitest3Error;
}
}

// Ignore "Error: vitest/browser can be imported only inside the Browser Mode."
if (
error instanceof Error &&
error.message.includes('can be imported only inside the Browser Mode')
) {
return;
}

// Throw anything else
throw error;
}
};
initTransport();

beforeAll(() => {
if (globalThis.globalProjectAnnotations) {
return globalThis.globalProjectAnnotations.beforeAll();
}
});

beforeEach(resetMousePositionBeforeTests);

afterEach(modifyErrorMessage);
Loading