Skip to content

Commit

Permalink
Merge pull request storybookjs#30181 from storybookjs/valentin/add-vi…
Browse files Browse the repository at this point in the history
…test-3-support

Addon Test: Add Vitest 3 support
  • Loading branch information
valentinpalkovic authored Jan 15, 2025
2 parents 3f702aa + f4373a5 commit b16498b
Show file tree
Hide file tree
Showing 37 changed files with 4,588 additions and 2,832 deletions.
2 changes: 1 addition & 1 deletion code/.storybook/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default mergeConfig(
plugins: [
import('@storybook/experimental-addon-test/vitest-plugin').then(({ storybookTest }) =>
storybookTest({
configDir: process.cwd(),
configDir: __dirname,
tags: {
include: ['vitest'],
},
Expand Down
12 changes: 6 additions & 6 deletions code/addons/test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@
"@types/istanbul-lib-report": "^3.0.3",
"@types/node": "^22.0.0",
"@types/semver": "^7",
"@vitest/browser": "^2.1.3",
"@vitest/runner": "^2.1.3",
"@vitest/browser": "3.0.0-beta.4",
"@vitest/runner": "3.0.0-beta.4",
"ansi-to-html": "^0.7.2",
"boxen": "^8.0.1",
"es-toolkit": "^1.22.0",
Expand All @@ -116,13 +116,13 @@
"tree-kill": "^1.2.2",
"ts-dedent": "^2.2.0",
"typescript": "^5.3.2",
"vitest": "^2.1.3"
"vitest": "3.0.0-beta.4"
},
"peerDependencies": {
"@vitest/browser": "^2.1.1",
"@vitest/runner": "^2.1.1",
"@vitest/browser": "^2.1.1 || ^3.0.0",
"@vitest/runner": "^2.1.1 || ^3.0.0",
"storybook": "workspace:^",
"vitest": "^2.1.1"
"vitest": "^2.1.1 || ^3.0.0"
},
"peerDependenciesMeta": {
"@vitest/browser": {
Expand Down
14 changes: 13 additions & 1 deletion code/addons/test/src/node/reporter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { TaskState } from 'vitest';
import type { Vitest } from 'vitest/node';
import * as vitestNode from 'vitest/node';
import { type Reporter } from 'vitest/reporters';

import type {
Expand All @@ -12,6 +13,7 @@ import type { API_StatusUpdate } from '@storybook/types';

import type { Suite } from '@vitest/runner';
import { throttle } from 'es-toolkit';
import { satisfies } from 'semver';

import { TEST_PROVIDER_ID } from '../constants';
import type { TestManager } from './test-manager';
Expand Down Expand Up @@ -50,8 +52,15 @@ const statusMap: Record<TaskState, TestStatus> = {
run: 'pending',
skip: 'skipped',
todo: 'skipped',
queued: 'pending',
};

const vitestVersion = vitestNode.version;

const isVitest3OrLater = vitestVersion
? satisfies(vitestVersion, '>=3.0.0-beta.3', { includePrerelease: true })
: false;

export class StorybookReporter implements Reporter {
testStatusData: API_StatusUpdate = {};

Expand Down Expand Up @@ -213,7 +222,10 @@ export class StorybookReporter implements Reporter {
async onFinished() {
const unhandledErrors = this.ctx.state.getUnhandledErrors();

const isCancelled = this.ctx.isCancelling;
const isCancelled = isVitest3OrLater
? this.testManager.vitestManager.isCancelling
: // @ts-expect-error isCancelling is private in Vitest 3.
this.ctx.isCancelling;
const report = await this.getProgressReport(Date.now());

const testSuiteFailures = report.details.testResults.filter(
Expand Down
20 changes: 10 additions & 10 deletions code/addons/test/src/node/test-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,21 @@ const vitest = vi.hoisted(() => ({
cancelCurrentRun: vi.fn(),
globTestSpecs: vi.fn(),
getModuleProjects: vi.fn(() => []),
configOverride: {
actualTestNamePattern: undefined,
get testNamePattern() {
return this.actualTestNamePattern!;
setGlobalTestNamePattern: setTestNamePattern,
vite: {
watcher: {
removeAllListeners: vi.fn(),
on: vi.fn(),
},
set testNamePattern(value: string) {
setTestNamePattern(value);
// @ts-expect-error Ignore for testing
this.actualTestNamePattern = value;
moduleGraph: {
getModulesByFile: () => [],
invalidateModule: vi.fn(),
},
},
}));

vi.mock('vitest/node', () => ({
vi.mock('vitest/node', async (importOriginal) => ({
...(await importOriginal()),
createVitest: vi.fn(() => Promise.resolve(vitest)),
}));
const createVitest = vi.mocked(actualCreateVitest);
Expand Down Expand Up @@ -137,7 +138,6 @@ describe('TestManager', () => {
storyIds: [],
});
expect(vitest.runFiles).toHaveBeenCalledWith([], true);
expect(vitest.configOverride.testNamePattern).toBeUndefined();

await testManager.handleRunRequest({
providerId: TEST_PROVIDER_ID,
Expand Down
115 changes: 84 additions & 31 deletions code/addons/test/src/node/vitest-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import type {
CoverageOptions,
ResolvedCoverageOptions,
TestProject,
TestRunResult,
TestSpecification,
Vitest,
WorkspaceProject,
} from 'vitest/node';
import * as vitestNode from 'vitest/node';

import { resolvePathInStorybookCache } from 'storybook/internal/common';
import type { TestingModuleRunRequestPayload } from 'storybook/internal/core-events';
Expand All @@ -16,6 +18,7 @@ import type { DocsIndexEntry, StoryIndex, StoryIndexEntry } from '@storybook/typ

import { findUp } from 'find-up';
import path, { dirname, join, normalize } from 'pathe';
import { satisfies } from 'semver';
import slash from 'slash';

import { COVERAGE_DIRECTORY, type Config } from '../constants';
Expand All @@ -35,6 +38,11 @@ type TagsFilter = {

const packageDir = dirname(require.resolve('@storybook/experimental-addon-test/package.json'));

const vitestVersion = vitestNode.version;
const isVitest3OrLater = vitestVersion
? satisfies(vitestVersion, '>=3.0.0-beta.3', { includePrerelease: true })
: false;

// We have to tell Vitest that it runs as part of Storybook
process.env.VITEST_STORYBOOK = 'true';

Expand All @@ -47,6 +55,10 @@ export class VitestManager {

storyCountForCurrentRun: number = 0;

runningPromise: Promise<any> | null = null;

isCancelling = false;

constructor(private testManager: TestManager) {}

async startVitest({ coverage = false } = {}) {
Expand Down Expand Up @@ -123,7 +135,7 @@ export class VitestManager {
await this.vitestRestartPromise;
this.vitestRestartPromise = new Promise(async (resolve, reject) => {
try {
await this.vitest?.runningPromise;
await this.runningPromise;
await this.closeVitest();
await this.startVitest({ coverage });
resolve();
Expand All @@ -136,6 +148,21 @@ export class VitestManager {
return this.vitestRestartPromise;
}

private setGlobalTestNamePattern(pattern: string | RegExp) {
if (isVitest3OrLater) {
this.vitest!.setGlobalTestNamePattern(pattern);
} else {
// @ts-expect-error vitest.configOverride is a Vitest < 3 API.
this.vitest!.configOverride.testNamePattern = pattern;
}
}

private resetGlobalTestNamePattern() {
if (this.vitest) {
this.setGlobalTestNamePattern('');
}
}

private updateLastChanged(filepath: string) {
const projects = this.vitest!.getModuleProjects(filepath);
projects.forEach(({ server, browser }) => {
Expand Down Expand Up @@ -183,14 +210,31 @@ export class VitestManager {
return true;
}

private get vite() {
// TODO: vitest.server is a Vitest < 3.0.0 API. Remove as soon as we don't support < 3.0.0 anymore.
return isVitest3OrLater ? this.vitest?.vite : this.vitest?.server;
}

async runFiles(specifications: TestSpecification[], allTestsRun?: boolean) {
this.isCancelling = false;
const runTest: (
specifications: TestSpecification[],
allTestsRun?: boolean | undefined
// @ts-expect-error vitest.runFiles is a Vitest < 3.0.0 API. Remove as soon as we don't support < 3.0.0 anymore.
) => Promise<TestRunResult> = this.vitest!.runFiles ?? this.vitest!.runTestSpecifications;
this.runningPromise = runTest.call(this.vitest, specifications, allTestsRun);
await this.runningPromise;
this.runningPromise = null;
}

async runTests(requestPayload: TestingModuleRunRequestPayload<Config>) {
if (!this.vitest) {
await this.startVitest();
} else {
await this.vitestRestartPromise;
}

this.resetTestNamePattern();
this.resetGlobalTestNamePattern();

const stories = await this.fetchStories(requestPayload.indexUrl, requestPayload.storyIds);
const vitestTestSpecs = await this.getStorybookTestSpecs();
Expand Down Expand Up @@ -229,18 +273,19 @@ export class VitestManager {

if (isSingleStoryRun) {
const storyName = stories[0].name;
this.vitest!.configOverride.testNamePattern = new RegExp(
`^${storyName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`
);
const regex = new RegExp(`^${storyName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
this.setGlobalTestNamePattern(regex);
}

await this.vitest!.runFiles(filteredTestFiles, true);
this.resetTestNamePattern();
await this.runFiles(filteredTestFiles, true);
this.resetGlobalTestNamePattern();
}

async cancelCurrentRun() {
this.isCancelling = true;
await this.vitest?.cancelCurrentRun('keyboard-input');
await this.vitest?.runningPromise;
await this.runningPromise;
this.isCancelling = false;
}

async closeVitest() {
Expand All @@ -255,22 +300,26 @@ export class VitestManager {
}

private async getTestDependencies(spec: TestSpecification, deps = new Set<string>()) {
const addImports = async (project: WorkspaceProject, filepath: string) => {
const addImports = async (project: TestProject, filepath: string) => {
if (deps.has(filepath)) {
return;
}
deps.add(filepath);

const mod = project.server.moduleGraph.getModuleById(filepath);
const transformed =
mod?.ssrTransformResult || (await project.vitenode.transformRequest(filepath));
// TODO: Remove project.server once we don't support Vitest < 3.0.0 anymore
const server = isVitest3OrLater ? project.vite : project.server;

const mod = server.moduleGraph.getModuleById(filepath);
// @ts-expect-error project.vitenode is a Vitest < 3 API.
const viteNode = isVitest3OrLater ? project.vite : project.vitenode;
const transformed = mod?.ssrTransformResult || (await viteNode.transformRequest(filepath));
if (!transformed) {
return;
}
const dependencies = [...(transformed.deps || []), ...(transformed.dynamicDeps || [])];
await Promise.all(
dependencies.map(async (dep) => {
const idPath = await project.server.pluginContainer.resolveId(dep, filepath, {
const idPath = await server.pluginContainer.resolveId(dep, filepath, {
ssr: true,
});
const fsPath = idPath && !idPath.external && idPath.id.split('?')[0];
Expand All @@ -286,7 +335,11 @@ export class VitestManager {
);
};

await addImports(spec.project.workspaceProject, spec.moduleId);
await addImports(
// @ts-expect-error spec.project.workspaceProject is a Vitest < 3 API.
isVitest3OrLater ? spec.project : spec.project.workspaceProject,
spec.moduleId
);
deps.delete(spec.moduleId);

return deps;
Expand All @@ -296,9 +349,14 @@ export class VitestManager {
if (!this.vitest) {
return;
}
this.resetTestNamePattern();
this.resetGlobalTestNamePattern();

const globTestSpecs: (filters?: string[] | undefined) => Promise<TestSpecification[]> =
// TODO: vitest.globTestSpecs is a Vitest < 3.0.0 API.
isVitest3OrLater ? this.vitest.globTestSpecifications : this.vitest.globTestSpecs;

const globTestFiles = await globTestSpecs.call(this.vitest);

const globTestFiles = await this.vitest.globTestSpecs();
const testGraphs = await Promise.all(
globTestFiles
.filter((workspace) => this.isStorybookProject(workspace.project))
Expand All @@ -317,8 +375,8 @@ export class VitestManager {

if (triggerAffectedTests.length) {
await this.vitest.cancelCurrentRun('keyboard-input');
await this.vitest.runningPromise;
await this.vitest.runFiles(triggerAffectedTests, false);
await this.runningPromise;
await this.runFiles(triggerAffectedTests, false);
}
}

Expand All @@ -338,9 +396,9 @@ export class VitestManager {
}

async registerVitestConfigListener() {
this.vitest?.server?.watcher.on('change', async (file) => {
this.vite?.watcher.on('change', async (file) => {
file = normalize(file);
const isConfig = file === this.vitest?.server.config.configFile;
const isConfig = file === this.vite?.config.configFile;
if (isConfig) {
log('Restarting Vitest due to config change');
await this.closeVitest();
Expand All @@ -350,20 +408,15 @@ export class VitestManager {
}

async setupWatchers() {
this.resetTestNamePattern();
this.vitest?.server?.watcher.removeAllListeners('change');
this.vitest?.server?.watcher.removeAllListeners('add');
this.vitest?.server?.watcher.on('change', this.runAffectedTestsAfterChange.bind(this));
this.vitest?.server?.watcher.on('add', this.runAffectedTestsAfterChange.bind(this));
this.resetGlobalTestNamePattern();
const server = this.vite;
server?.watcher.removeAllListeners('change');
server?.watcher.removeAllListeners('add');
server?.watcher.on('change', this.runAffectedTestsAfterChange.bind(this));
server?.watcher.on('add', this.runAffectedTestsAfterChange.bind(this));
this.registerVitestConfigListener();
}

resetTestNamePattern() {
if (this.vitest) {
this.vitest.configOverride.testNamePattern = undefined;
}
}

isStorybookProject(project: TestProject | WorkspaceProject) {
// eslint-disable-next-line no-underscore-dangle
return !!project.config.env?.__STORYBOOK_URL__;
Expand Down
Loading

0 comments on commit b16498b

Please sign in to comment.