Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addon Test: Add Vitest 3 support #30181

Merged
merged 26 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
09196b1
Addon Test: Add Vitest 3 support
valentinpalkovic Jan 3, 2025
e61762b
Adjust tests for Vitest 3
valentinpalkovic Jan 3, 2025
cbc7305
Remove unnecessary spread of browser config in vitest-plugin
valentinpalkovic Jan 3, 2025
91ad81f
Merge remote-tracking branch 'origin/valentin/a11y-refactorings' into…
valentinpalkovic Jan 3, 2025
dc6db06
Update yarn.lock
valentinpalkovic Jan 3, 2025
dba7ad3
Downgrade Vite and fix type issues
valentinpalkovic Jan 3, 2025
c2245b0
Further Vitest 3 compat adjustments
valentinpalkovic Jan 3, 2025
aded285
Fix tests
valentinpalkovic Jan 3, 2025
161a29c
Merge remote-tracking branch 'origin/valentin/a11y-refactorings' into…
valentinpalkovic Jan 10, 2025
61996e1
Upgrade to Vitest 3.0.0-beta.4
valentinpalkovic Jan 10, 2025
d099241
Remove obsolete comma
valentinpalkovic Jan 10, 2025
a6be1e6
Format file outputs in postinstall of addon-test
valentinpalkovic Jan 10, 2025
e6ec8b1
Merge remote-tracking branch 'origin/valentin/a11y-refactorings' into…
valentinpalkovic Jan 10, 2025
ecf9f7f
Fix tests
valentinpalkovic Jan 10, 2025
644690c
Fix tests
valentinpalkovic Jan 10, 2025
ccbdb71
Merge remote-tracking branch 'origin/valentin/a11y-refactorings' into…
valentinpalkovic Jan 13, 2025
ed3ed9a
Merge remote-tracking branch 'origin/next' into valentin/add-vitest-3…
valentinpalkovic Jan 13, 2025
b2da8de
Update Vitest packages to 3.0.0 beta
valentinpalkovic Jan 13, 2025
e90d717
Refactor Vitest browser configuration
valentinpalkovic Jan 15, 2025
a00619c
Remove comment
valentinpalkovic Jan 15, 2025
b1e99df
Remove test timeout
valentinpalkovic Jan 15, 2025
2fdb0bf
Merge remote-tracking branch 'origin/next' into valentin/add-vitest-3…
valentinpalkovic Jan 15, 2025
4c45960
Refactor Vitest version import and check for compatibility with versi…
valentinpalkovic Jan 15, 2025
a539692
Fix optional chaining for Vitest compatibility checks
valentinpalkovic Jan 15, 2025
3071566
fix watch mode in Vitest 3
valentinpalkovic Jan 15, 2025
f4373a5
Cleanup and apply requested changes from review
valentinpalkovic Jan 15, 2025
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
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
Loading