From 40ca30b93e93b6f4889075a1b49817c27e908cd2 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 12 Sep 2024 16:59:52 +0200 Subject: [PATCH 01/23] Add experimental_TEST_PROVIDER addon type --- code/core/src/manager-api/lib/addons.ts | 5 ++++- code/core/src/types/modules/addons.ts | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/code/core/src/manager-api/lib/addons.ts b/code/core/src/manager-api/lib/addons.ts index c937c0508a2b..a7aa438e0863 100644 --- a/code/core/src/manager-api/lib/addons.ts +++ b/code/core/src/manager-api/lib/addons.ts @@ -8,6 +8,7 @@ import type { Addon_PageType, Addon_SidebarBottomType, Addon_SidebarTopType, + Addon_TestProviderType, Addon_Type, Addon_Types, Addon_TypesMapping, @@ -70,7 +71,8 @@ export class AddonStore { | Addon_Types | Addon_TypesEnum.experimental_PAGE | Addon_TypesEnum.experimental_SIDEBAR_BOTTOM - | Addon_TypesEnum.experimental_SIDEBAR_TOP, + | Addon_TypesEnum.experimental_SIDEBAR_TOP + | Addon_TypesEnum.experimental_TEST_PROVIDER, >(type: T): Addon_Collection | any { if (!this.elements[type]) { this.elements[type] = {}; @@ -91,6 +93,7 @@ export class AddonStore { | Addon_BaseType | Omit | Omit + | Omit | Omit | Omit ): void { diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index 7c80da4f297c..5aa530064699 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -461,12 +461,22 @@ export interface Addon_SidebarTopType { render: FC; } +export interface Addon_TestProviderType { + type: Addon_TypesEnum.experimental_TEST_PROVIDER; + /** The unique id of the test provider. */ + id: string; + icon: ReactNode; + title: string; + description: FC; +} + type Addon_TypeBaseNames = Exclude< Addon_TypesEnum, | Addon_TypesEnum.PREVIEW | Addon_TypesEnum.experimental_PAGE | Addon_TypesEnum.experimental_SIDEBAR_BOTTOM | Addon_TypesEnum.experimental_SIDEBAR_TOP + | Addon_TypesEnum.experimental_TEST_PROVIDER >; export interface Addon_TypesMapping extends Record { @@ -474,6 +484,7 @@ export interface Addon_TypesMapping extends Record = (api: API) => void; @@ -537,4 +548,6 @@ export enum Addon_TypesEnum { * @deprecated This will be removed in Storybook 9.0. */ experimental_SIDEBAR_TOP = 'sidebar-top', + /** This adds items to the Testing Module in the sidebar. */ + experimental_TEST_PROVIDER = 'test-provider', } From b423331e9529940e25ccfc7255dd7fff44e8e4cb Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 13 Sep 2024 09:44:33 +0200 Subject: [PATCH 02/23] Test for experimental_TEST_PROVIDER addon type --- code/core/src/manager-api/tests/addons.test.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/code/core/src/manager-api/tests/addons.test.js b/code/core/src/manager-api/tests/addons.test.js index 4d64ecc95224..6fd93fb4dca5 100644 --- a/code/core/src/manager-api/tests/addons.test.js +++ b/code/core/src/manager-api/tests/addons.test.js @@ -19,11 +19,21 @@ const PANELS = { }, }; +const TEST_PROVIDERS = { + 'storybook/test/test-provider': { + id: 'storybook/test/test-provider', + title: 'Component tests', + }, +} + const provider = { getElements(type) { if (type === types.PANEL) { return PANELS; } + if (type === types.experimental_TEST_PROVIDER) { + return TEST_PROVIDERS; + } return null; }, }; @@ -38,14 +48,13 @@ const store = { describe('Addons API', () => { describe('#getElements', () => { it('should return provider elements', () => { - // given const { api } = initAddons({ provider, store }); - // when const panels = api.getElements(types.PANEL); - - // then expect(panels).toBe(PANELS); + + const testProviders = api.getElements(types.experimental_TEST_PROVIDER); + expect(testProviders).toBe(TEST_PROVIDERS); }); }); From cea8021a57de22e0de725949b18dad9ebae1ac5e Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 13 Sep 2024 10:17:27 +0200 Subject: [PATCH 03/23] Register Test addon as TEST_PROVIDER addon type --- code/addons/test/package.json | 3 ++- code/addons/test/src/constants.ts | 3 ++- code/addons/test/src/manager.tsx | 18 +++++++++++++++--- code/yarn.lock | 1 + 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/code/addons/test/package.json b/code/addons/test/package.json index ca52df1494d3..4a158133de8b 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -70,7 +70,8 @@ "prep": "jiti ../../../scripts/prepare/addon-bundle.ts" }, "dependencies": { - "@storybook/csf": "^0.1.11" + "@storybook/csf": "^0.1.11", + "@storybook/icons": "^1.2.10" }, "devDependencies": { "@types/semver": "^7", diff --git a/code/addons/test/src/constants.ts b/code/addons/test/src/constants.ts index d0a3762620c9..ebc63021f0a4 100644 --- a/code/addons/test/src/constants.ts +++ b/code/addons/test/src/constants.ts @@ -1 +1,2 @@ -export const ADDON_ID = 'storybook/vitest'; +export const ADDON_ID = 'storybook/test'; +export const TEST_PROVIDER_ID = `${ADDON_ID}/test-provider`; diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 8d706325046a..b7744659eff5 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -1,5 +1,17 @@ -import { type API, addons } from 'storybook/internal/manager-api'; +import React from 'react'; -import { ADDON_ID } from './constants'; +import { addons } from 'storybook/internal/manager-api'; +import { Addon_TypesEnum } from 'storybook/internal/types'; -addons.register(ADDON_ID, () => {}); +import { PointerHandIcon } from '@storybook/icons'; + +import { ADDON_ID, TEST_PROVIDER_ID } from './constants'; + +addons.register(ADDON_ID, () => { + addons.add(TEST_PROVIDER_ID, { + type: Addon_TypesEnum.experimental_TEST_PROVIDER, + icon: , + title: 'Component tests', + description: () => 'Not yet run', + }); +}); diff --git a/code/yarn.lock b/code/yarn.lock index 80ebe411b341..bb28a7bf8772 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6239,6 +6239,7 @@ __metadata: resolution: "@storybook/experimental-addon-test@workspace:addons/test" dependencies: "@storybook/csf": "npm:^0.1.11" + "@storybook/icons": "npm:^1.2.10" "@types/semver": "npm:^7" "@vitest/browser": "npm:^2.0.0" boxen: "npm:^8.0.1" From cb619aefd1b7aa9569f4b1e28e2d4d30d5783b42 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 20 Sep 2024 15:54:27 +0200 Subject: [PATCH 04/23] Addon-Test: Implement Addon Test TestProvider Backend --- code/.storybook/vitest.config.ts | 5 +- code/addons/test/package.json | 14 +- code/addons/test/src/logger.ts | 7 + code/addons/test/src/node/boot-test-runner.ts | 74 +++++ code/addons/test/src/node/reporter.ts | 192 +++++++++++++ code/addons/test/src/node/test-manager.ts | 84 ++++++ code/addons/test/src/node/vitest-manager.ts | 104 +++++++ code/addons/test/src/node/vitest.ts | 35 +++ code/addons/test/src/preset.ts | 3 + .../src/core-events/data/testing-module.ts | 87 ++++++ code/core/src/core-events/index.ts | 14 + .../manager/components/sidebar/Sidebar.tsx | 35 ++- .../src/manager/components/sidebar/Tree.tsx | 85 +++++- code/core/src/manager/globals/exports.ts | 18 ++ code/package.json | 9 +- code/yarn.lock | 264 +++++++++++------- 16 files changed, 906 insertions(+), 124 deletions(-) create mode 100644 code/addons/test/src/logger.ts create mode 100644 code/addons/test/src/node/boot-test-runner.ts create mode 100644 code/addons/test/src/node/reporter.ts create mode 100644 code/addons/test/src/node/test-manager.ts create mode 100644 code/addons/test/src/node/vitest-manager.ts create mode 100644 code/addons/test/src/node/vitest.ts create mode 100644 code/core/src/core-events/data/testing-module.ts diff --git a/code/.storybook/vitest.config.ts b/code/.storybook/vitest.config.ts index 25a83b4396ea..9745a67949e6 100644 --- a/code/.storybook/vitest.config.ts +++ b/code/.storybook/vitest.config.ts @@ -31,9 +31,8 @@ export default mergeConfig( test: { name: 'storybook-ui', include: [ - // TODO: test all core and addon stories later - // './core/**/components/**/*.{story,stories}.?(c|m)[jt]s?(x)', - '../addons/**/src/**/*.{story,stories}.?(c|m)[jt]s?(x)', + // TODO: Can be reverted. Temporarily I am adding all stories so that I can trigger tests for all stories in the UI. + '../{core,addons,lib}/**/{src,components,template}/**/*.{story,stories}.?(c|m)[jt]s?(x)', ], exclude: [ ...defaultExclude, diff --git a/code/addons/test/package.json b/code/addons/test/package.json index 4a158133de8b..c8ca27c7f2eb 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -71,20 +71,23 @@ }, "dependencies": { "@storybook/csf": "^0.1.11", - "@storybook/icons": "^1.2.10" + "@storybook/icons": "^1.2.10", + "chalk": "^5.3.0" }, "devDependencies": { "@types/semver": "^7", - "@vitest/browser": "^2.0.0", + "@vitest/browser": "^2.1.1", + "@vitest/runner": "^2.1.1", "boxen": "^8.0.1", "find-up": "^7.0.0", "semver": "^7.6.3", "tinyrainbow": "^1.2.0", "ts-dedent": "^2.2.0", - "vitest": "^2.0.0" + "vitest": "^2.1.1" }, "peerDependencies": { - "@vitest/browser": "^2.0.0", + "@vitest/browser": "^2.1.1", + "@vitest/runner": "^2.1.1", "storybook": "workspace:^", "vitest": "^2.0.0" }, @@ -104,7 +107,8 @@ "./src/preset.ts", "./src/vitest-plugin/index.ts", "./src/vitest-plugin/global-setup.ts", - "./src/postinstall.ts" + "./src/postinstall.ts", + "./src/node/vitest.ts" ] } } diff --git a/code/addons/test/src/logger.ts b/code/addons/test/src/logger.ts new file mode 100644 index 000000000000..24289135495c --- /dev/null +++ b/code/addons/test/src/logger.ts @@ -0,0 +1,7 @@ +import chalk from 'chalk'; + +import { ADDON_ID } from './constants'; + +export const log = (message: any) => { + console.log(`${chalk.magenta(ADDON_ID)}: ${message.toString().trim()}`); +}; diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts new file mode 100644 index 000000000000..b4b86b8a6518 --- /dev/null +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -0,0 +1,74 @@ +import { type ChildProcess, fork } from 'node:child_process'; +import { join } from 'node:path'; + +import type { Channel } from 'storybook/internal/channels'; +import { + TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, + TESTING_MODULE_RUN_ALL_REQUEST, + TESTING_MODULE_RUN_REQUEST, + TESTING_MODULE_WATCH_MODE_REQUEST, +} from 'storybook/internal/core-events'; + +import { log } from '../logger'; + +export function bootTestRunner(channel: Channel) { + // This path is a bit confusing, but essentiall `boot-test-runner` gets bundled into the preset bundle + // which is at the root. Then, from the root, we want to load `node/vitest.js` + const sub = join(__dirname, 'node', 'vitest.js'); + + let child: ChildProcess; + + function restartChildProcess() { + child?.kill(); + log('Restarting Child Process...'); + child = startChildProcess(); + } + + function startChildProcess() { + child = fork(sub, [], { + // We want to pipe output and error + // so that we can prefix the logs in the terminal + // with a clear identifier + stdio: ['inherit', 'pipe', 'pipe', 'ipc'], + silent: true, + }); + + child.stdout?.on('data', (data) => { + log(data); + }); + + child.stderr?.on('data', (data) => { + log(data); + }); + + child.on('message', (result: any) => { + if (result.type === 'error') { + log(result.message); + log(result.error); + restartChildProcess(); + } else { + channel.emit(result.type, ...(result.args || [])); + } + }); + + return child; + } + + child = startChildProcess(); + + channel.on(TESTING_MODULE_RUN_REQUEST, (...args) => { + child.send({ type: TESTING_MODULE_RUN_REQUEST, args, from: 'server' }); + }); + + channel.on(TESTING_MODULE_RUN_ALL_REQUEST, (...args) => { + child.send({ type: TESTING_MODULE_RUN_ALL_REQUEST, args, from: 'server' }); + }); + + channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, (...args) => { + child.send({ type: TESTING_MODULE_WATCH_MODE_REQUEST, args, from: 'server' }); + }); + + channel.on(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, (...args) => { + child.send({ type: TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, args, from: 'server' }); + }); +} diff --git a/code/addons/test/src/node/reporter.ts b/code/addons/test/src/node/reporter.ts new file mode 100644 index 000000000000..6a735a8fe8a6 --- /dev/null +++ b/code/addons/test/src/node/reporter.ts @@ -0,0 +1,192 @@ +import type { TaskState } from 'vitest'; +import type { Vitest } from 'vitest/node'; +import { type Reporter } from 'vitest/reporters'; + +import type { + TestingModuleRunAssertionResultPayload, + TestingModuleRunResponsePayload, + TestingModuleRunTestResultPayload, +} from 'storybook/internal/core-events'; + +import type { API_StatusUpdate } from '@storybook/types'; + +import type { Suite } from '@vitest/runner'; +// TODO +// We can theoretically avoid the `@vitest/runner` dependency by copying over the necessary +// functions from the `@vitest/runner` package. It is not complex and does not have +// any significant dependencies. +import { getTests } from '@vitest/runner/utils'; + +import { TEST_PROVIDER_ID } from '../constants'; +import type { TestManager } from './test-manager'; + +type Status = 'passed' | 'failed' | 'skipped' | 'pending' | 'todo' | 'disabled'; + +function isDefined(value: any): value is NonNullable { + return value !== undefined && value !== null; +} + +const StatusMap: Record = { + fail: 'failed', + only: 'pending', + pass: 'passed', + run: 'pending', + skip: 'skipped', + todo: 'todo', +}; + +export default class StorybookReporter implements Reporter { + testStatusData: API_StatusUpdate = {}; + + start = 0; + + ctx!: Vitest; + + constructor(private testManager: TestManager) {} + + onInit(ctx: Vitest) { + this.ctx = ctx; + this.start = Date.now(); + } + + getProgressReport(): TestingModuleRunResponsePayload { + const files = this.ctx.state.getFiles(); + const fileTests = getTests(files); + // The number of total tests is dynamic and can change during the run + const numTotalTests = fileTests.length; + + const numFailedTests = fileTests.filter((t) => t.result?.state === 'fail').length; + const numPassedTests = fileTests.filter((t) => t.result?.state === 'pass').length; + const numPendingTests = fileTests.filter( + (t) => t.result?.state === 'run' || t.mode === 'skip' || t.result?.state === 'skip' + ).length; + const testResults: Array = []; + + for (const file of files) { + const tests = getTests([file]); + let startTime = tests.reduce( + (prev, next) => Math.min(prev, next.result?.startTime ?? Number.POSITIVE_INFINITY), + Number.POSITIVE_INFINITY + ); + if (startTime === Number.POSITIVE_INFINITY) { + startTime = this.start; + } + + const endTime = tests.reduce( + (prev, next) => + Math.max(prev, (next.result?.startTime ?? 0) + (next.result?.duration ?? 0)), + startTime + ); + + const assertionResults: TestingModuleRunAssertionResultPayload[] = tests + .map((t) => { + const ancestorTitles: string[] = []; + let iter: Suite | undefined = t.suite; + while (iter) { + ancestorTitles.push(iter.name); + iter = iter.suite; + } + ancestorTitles.reverse(); + + const status = StatusMap[t.result?.state || t.mode] || 'skipped'; + + if (status === 'passed' || status === 'pending') { + return { + status, + duration: t.result?.duration || 0, + storyId: (t.meta as any).storyId, + }; + } + + if (status === 'failed') { + return { + status, + duration: t.result?.duration || 0, + failureMessages: t.result?.errors?.map((e) => e.stack || e.message) || [], + storyId: (t.meta as any).storyId, + }; + } + + return null; + }) + .filter(isDefined); + + const hasFailedTests = tests.some((t) => t.result?.state === 'fail'); + + testResults.push({ + results: assertionResults, + startTime, + endTime, + status: file.result?.state === 'fail' || hasFailedTests ? 'failed' : 'passed', + message: file.result?.errors?.[0]?.message, + }); + } + + return { + numFailedTests, + numPassedTests, + numPendingTests, + numTotalTests, + testResults, + success: true, + // TODO + // It is not simply (numPassedTests + numFailedTests) / numTotalTests + // because numTotalTests is dyanmic and can change during the run + // We need to calculate the progress based on the number of tests that have been run + progress: 0, + startTime: this.start, + }; + } + + async onTaskUpdate() { + try { + const progress = this.getProgressReport(); + + this.testManager.sendProgressReport({ + status: 'success', + payload: progress, + providerId: TEST_PROVIDER_ID, + }); + } catch (e) { + if (e instanceof Error) { + this.testManager.sendProgressReport({ + status: 'failed', + providerId: TEST_PROVIDER_ID, + error: { + name: 'Failed to gather test results', + message: e.message, + stack: e.stack, + }, + }); + } else { + this.testManager.sendProgressReport({ + status: 'failed', + providerId: TEST_PROVIDER_ID, + error: { + name: 'Failed to gather test results', + message: String(e), + stack: undefined, + }, + }); + } + } + } + + // TODO + // Clearing the whole internal state of Vitest might be too aggressive + // Essentially, we want to reset the calculated total number of tests and the + // test results when a new test run starts, so that the getProgressReport + // method can calculate the correct values + async clearVitestState() { + this.ctx.state.filesMap.clear(); + this.ctx.state.pathsSet.clear(); + this.ctx.state.idMap.clear(); + this.ctx.state.errorsSet.clear(); + this.ctx.state.processTimeoutCauses.clear(); + } + + async onFinished() { + this.clearVitestState(); + } +} +export { StorybookReporter }; diff --git a/code/addons/test/src/node/test-manager.ts b/code/addons/test/src/node/test-manager.ts new file mode 100644 index 000000000000..f0542b930aa0 --- /dev/null +++ b/code/addons/test/src/node/test-manager.ts @@ -0,0 +1,84 @@ +import type { Channel } from 'storybook/internal/channels'; +import { + TESTING_MODULE_RUN_ALL_REQUEST, + TESTING_MODULE_RUN_PROGRESS_RESPONSE, + TESTING_MODULE_RUN_REQUEST, + TESTING_MODULE_WATCH_MODE_REQUEST, + type TestingModuleRunAllRequestPayload, + type TestingModuleRunProgressPayload, + type TestingModuleRunRequestPayload, + type TestingModuleWatchModeRequestPayload, +} from 'storybook/internal/core-events'; + +import { TEST_PROVIDER_ID } from '../constants'; +import { VitestManager } from './vitest-manager'; + +export class TestManager { + private vitestManager: VitestManager; + + watchMode = false; + + constructor(private channel: Channel) { + process.env.TEST = 'true'; + process.env.VITEST = 'true'; + process.env.NODE_ENV ??= 'test'; + + this.vitestManager = new VitestManager(channel, this); + + this.channel.on(TESTING_MODULE_RUN_REQUEST, this.handleRunRequest.bind(this)); + this.channel.on(TESTING_MODULE_RUN_ALL_REQUEST, this.handleRunAllRequest.bind(this)); + this.channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, this.handleWatchModeRequest.bind(this)); + } + + async restartVitest(watchMode = false) { + await this.vitestManager.closeVitest(); + await this.vitestManager.startVitest(watchMode); + } + + async handleWatchModeRequest(request: TestingModuleWatchModeRequestPayload) { + try { + if (request.providerId !== TEST_PROVIDER_ID) { + return; + } + + if (this.watchMode !== request.watchMode) { + this.watchMode = request.watchMode; + await this.restartVitest(this.watchMode); + } + } catch (e) { + this.reportFatalError('Failed to change watch mode', e); + } + } + + async handleRunRequest(request: TestingModuleRunRequestPayload) { + try { + if (request.providerId !== TEST_PROVIDER_ID) { + return; + } + + await this.vitestManager.runTests(request.payload); + } catch (e) { + this.reportFatalError('Failed to run tests', e); + } + } + + async handleRunAllRequest(request: TestingModuleRunAllRequestPayload) { + try { + if (request.providerId !== TEST_PROVIDER_ID) { + return; + } + + await this.vitestManager.runAllTests(); + } catch (e) { + this.reportFatalError('Failed to run all tests', e); + } + } + + async sendProgressReport(payload: TestingModuleRunProgressPayload) { + this.channel.emit(TESTING_MODULE_RUN_PROGRESS_RESPONSE, payload); + } + + async reportFatalError(message: string, error: Error | any) { + process.send?.({ type: 'error', message, error: error.stack ?? error }); + } +} diff --git a/code/addons/test/src/node/vitest-manager.ts b/code/addons/test/src/node/vitest-manager.ts new file mode 100644 index 000000000000..98460f9e2926 --- /dev/null +++ b/code/addons/test/src/node/vitest-manager.ts @@ -0,0 +1,104 @@ +import path from 'node:path'; + +import type { TestProject, TestSpecification, Vitest, WorkspaceProject } from 'vitest/node'; + +import type { Channel } from 'storybook/internal/channels'; +import type { TestingModuleRunRequestPayload } from 'storybook/internal/core-events'; + +import StorybookReporter from './reporter'; +import type { TestManager } from './test-manager'; + +export class VitestManager { + vitest: Vitest | null = null; + + vitestStartupCounter = 0; + + constructor( + private channel: Channel, + private testManager: TestManager + ) {} + + async startVitest(watchMode = false) { + const { createVitest } = await import('vitest/node'); + + this.vitest = await createVitest('test', { + watch: watchMode, + passWithNoTests: true, + // standalone: true, + changed: watchMode, + // TODO: + // Do we want to enable Vite's default reporter? + // The output in the terminal might be too spamy and it might be better to + // find a way to just show errors and warnings for example + // Otherwise it might be hard for the user to discover Storybook related logs + reporters: ['default', new StorybookReporter(this.testManager)], + coverage: { + enabled: false, + }, + }); + + if (!this.vitest || this.vitest.projects.length < 1) { + return; + } + + await this.vitest.init(); + } + + async runAllTests() { + if (!this.vitest) { + await this.startVitest(); + } + + const tests = await this.getStorybookTestSpecs(); + await this.cancelCurrentRun(); + await this.vitest!.runFiles(tests, true); + } + + async runTests(testPayload: TestingModuleRunRequestPayload['payload']) { + if (!this.vitest) { + await this.startVitest(); + } + + // This list contains all the test files (story files) that need to be run + // based on the test files that are passed in the tests array + // This list does NOT contain any filtering of specific + // test cases (story) within the test files + const testList: TestSpecification[] = []; + + const storybookTests = await this.getStorybookTestSpecs(); + + for (const storybookTest of storybookTests) { + const match = testPayload.find((test) => { + const absoluteImportPath = path.join(process.cwd(), test.importPath); + return absoluteImportPath === storybookTest.moduleId; + }); + if (match) { + testList.push(storybookTest); + } + } + + await this.cancelCurrentRun(); + await this.vitest!.runFiles(testList, true); + } + + async cancelCurrentRun() { + await this.vitest?.cancelCurrentRun('keyboard-input'); + await this.vitest?.runningPromise; + } + + async closeVitest() { + await this.vitest?.close(); + } + + async getStorybookTestSpecs() { + const globTestSpecs = (await this.vitest?.globTestSpecs()) ?? []; + return ( + globTestSpecs.filter((workspaceSpec) => this.isStorybookProject(workspaceSpec.project)) ?? [] + ); + } + + isStorybookProject(project: TestProject | WorkspaceProject) { + // eslint-disable-next-line no-underscore-dangle + return !!project.config.env?.__STORYBOOK_URL__; + } +} diff --git a/code/addons/test/src/node/vitest.ts b/code/addons/test/src/node/vitest.ts new file mode 100644 index 000000000000..b9053c2caaf7 --- /dev/null +++ b/code/addons/test/src/node/vitest.ts @@ -0,0 +1,35 @@ +import process from 'node:process'; + +import { Channel } from 'storybook/internal/channels'; + +import { TestManager } from './test-manager'; + +process.env.TEST = 'true'; +process.env.VITEST = 'true'; +process.env.NODE_ENV ??= 'test'; + +const channel: Channel = new Channel({ + async: true, + transport: { + send: (event) => { + if (process.send) { + process.send(event); + } + }, + setHandler: (handler) => { + process.on('message', handler); + }, + }, +}); + +const testManager = new TestManager(channel); +testManager.restartVitest(); + +process.on('uncaughtException', (err) => { + process.send?.({ type: 'error', message: 'Uncaught Exception', error: err.stack }); + process.exit(1); +}); + +process.on('unhandledRejection', (reason) => { + throw new Error(`Unhandled Rejection: ${reason}`); +}); diff --git a/code/addons/test/src/preset.ts b/code/addons/test/src/preset.ts index 8b4bc31a9013..be1665356f66 100644 --- a/code/addons/test/src/preset.ts +++ b/code/addons/test/src/preset.ts @@ -1,7 +1,10 @@ import type { Channel } from 'storybook/internal/channels'; import type { Options } from 'storybook/internal/types'; +import { bootTestRunner } from './node/boot-test-runner'; + // eslint-disable-next-line @typescript-eslint/naming-convention export const experimental_serverChannel = async (channel: Channel, options: Options) => { + bootTestRunner(channel); return channel; }; diff --git a/code/core/src/core-events/data/testing-module.ts b/code/core/src/core-events/data/testing-module.ts new file mode 100644 index 000000000000..fa83bc6dd6d9 --- /dev/null +++ b/code/core/src/core-events/data/testing-module.ts @@ -0,0 +1,87 @@ +export type ProviderId = string; + +export type TestingModuleRunRequestStories = { + id: string; + name: string; +}; + +export type TestingModuleRunRequestPayload = { + providerId: ProviderId; + payload: { + stories: TestingModuleRunRequestStories[]; + importPath: string; + componentPath: string; + }[]; +}; + +export type TestingModuleRunAllRequestPayload = { + providerId: ProviderId; +}; + +export type TestingModuleRunProgressPayload = + | { + providerId: ProviderId; + payload: TestingModuleRunResponsePayload; + status: 'success' | 'pending'; + } + | { + providerId: ProviderId; + error: { + name: string; + message: string; + stack?: string; + }; + status: 'failed'; + }; + +export type TestingModuleRunResponsePayload = { + numTotalTests: number; + numPassedTests: number; + numFailedTests: number; + numPendingTests: number; + progress: number; + startTime: number; + success: boolean; + testResults: TestingModuleRunTestResultPayload[]; +}; + +export type TestingModuleRunTestResultPayload = { + results: TestingModuleRunAssertionResultPayload[]; + startTime: number; + endTime: number; + status: 'passed' | 'failed'; + message?: string; +}; + +export type TestingModuleRunAssertionResultPayload = + | { + status: 'success' | 'pending'; + duration: number; + storyId: string; + } + | { + status: 'failed'; + duration: number; + failureMessages: string[]; + storyId: string; + }; + +export type Status = 'success' | 'failed' | 'pending'; + +export type TestingModuleCancelTestRunRequestPayload = { + providerId: ProviderId; +}; + +export type TestingModuleCancelTestRunResponsePayload = + | { + status: 'success'; + } + | { + status: 'failed'; + message: string; + }; + +export type TestingModuleWatchModeRequestPayload = { + providerId: ProviderId; + watchMode: boolean; +}; diff --git a/code/core/src/core-events/index.ts b/code/core/src/core-events/index.ts index 90eed7b0d5b6..3ebf839d43c6 100644 --- a/code/core/src/core-events/index.ts +++ b/code/core/src/core-events/index.ts @@ -84,6 +84,13 @@ enum events { ARGTYPES_INFO_RESPONSE = 'argtypesInfoResponse', CREATE_NEW_STORYFILE_REQUEST = 'createNewStoryfileRequest', CREATE_NEW_STORYFILE_RESPONSE = 'createNewStoryfileResponse', + + TESTING_MODULE_RUN_REQUEST = 'testingModuleRunRequest', + TESTING_MODULE_RUN_PROGRESS_RESPONSE = 'testingModuleRunProgressResponse', + TESTING_MODULE_RUN_ALL_REQUEST = 'testingModuleRunAllRequest', + TESTING_MODULE_CANCEL_TEST_RUN_REQUEST = 'testingModuleCancelTestRunRequest', + TESTING_MODULE_CANCEL_TEST_RUN_RESPONSE = 'testingModuleCancelTestRunResponse', + TESTING_MODULE_WATCH_MODE_REQUEST = 'testingModuleWatchModeRequest', } // Enables: `import Events from ...` @@ -147,6 +154,12 @@ export const { SAVE_STORY_RESPONSE, ARGTYPES_INFO_REQUEST, ARGTYPES_INFO_RESPONSE, + TESTING_MODULE_RUN_REQUEST, + TESTING_MODULE_RUN_PROGRESS_RESPONSE, + TESTING_MODULE_RUN_ALL_REQUEST, + TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, + TESTING_MODULE_CANCEL_TEST_RUN_RESPONSE, + TESTING_MODULE_WATCH_MODE_REQUEST, } = events; export * from './data/create-new-story'; @@ -155,3 +168,4 @@ export * from './data/argtypes-info'; export * from './data/request-response'; export * from './data/save-story'; export * from './data/whats-new'; +export * from './data/testing-module'; diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index e937012c43e6..f658f00e4bf8 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -1,10 +1,16 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; -import { ScrollArea, Spaced } from '@storybook/core/components'; +import { Button, ScrollArea, Spaced } from '@storybook/core/components'; import { styled } from '@storybook/core/theming'; import type { API_LoadedRefData, Addon_SidebarTopType } from '@storybook/core/types'; -import type { State } from '@storybook/core/manager-api'; +import { + TESTING_MODULE_RUN_ALL_REQUEST, + TESTING_MODULE_WATCH_MODE_REQUEST, + type TestingModuleRunAllRequestPayload, + type TestingModuleWatchModeRequestPayload, +} from '@storybook/core/core-events'; +import { type State, useStorybookApi } from '@storybook/core/manager-api'; import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants'; import { Explorer } from './Explorer'; @@ -13,6 +19,7 @@ import { Heading } from './Heading'; import { Search } from './Search'; import { SearchResults } from './SearchResults'; import { SidebarBottom } from './SidebarBottom'; +import { TEST_PROVIDER_ID } from './Tree'; import type { CombinedDataset, Selection } from './types'; import { useLastViewed } from './useLastViewed'; @@ -133,6 +140,15 @@ export const Sidebar = React.memo(function Sidebar({ const dataset = useCombination(index, indexError, previewInitialized, status, refs); const isLoading = !index && !indexError; const lastViewedProps = useLastViewed(selected); + const api = useStorybookApi(); + const [watchMode, setWatchMode] = useState(false); + + useEffect(() => { + api.emit(TESTING_MODULE_WATCH_MODE_REQUEST, { + providerId: TEST_PROVIDER_ID, + watchMode, + } as TestingModuleWatchModeRequestPayload); + }, [api, watchMode]); return ( @@ -188,6 +204,19 @@ export const Sidebar = React.memo(function Sidebar({ {isLoading ? null : ( + + )} diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index 4f542d3140a7..0d5c990cb595 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -6,14 +6,21 @@ import { styled, useTheme } from '@storybook/core/theming'; import { CollapseIcon as CollapseIconSvg, ExpandAltIcon, + PlayIcon, StatusFailIcon, StatusPassIcon, StatusWarnIcon, SyncIcon, } from '@storybook/icons'; -import type { API_StatusValue, StoryId } from '@storybook/types'; +import type { API_HashEntry, API_StatusValue, StoryId } from '@storybook/types'; -import { PRELOAD_ENTRIES } from '@storybook/core/core-events'; +import { + PRELOAD_ENTRIES, + TESTING_MODULE_RUN_REQUEST, + type TestingModuleRunRequestPayload, + type TestingModuleRunRequestStories, +} from '@storybook/core/core-events'; +import { useStorybookApi } from '@storybook/core/manager-api'; import type { API, ComponentEntry, @@ -22,7 +29,6 @@ import type { StoriesHash, StoryEntry, } from '@storybook/core/manager-api'; -import { useStorybookApi } from '@storybook/core/manager-api'; import { transparentize } from 'polished'; @@ -44,6 +50,9 @@ import type { Highlight, Item } from './types'; import type { ExpandAction, ExpandedState } from './useExpanded'; import { useExpanded } from './useExpanded'; +export const TEST_ADDON_ID = 'storybook/test'; +export const TEST_PROVIDER_ID = `${TEST_ADDON_ID}/test-provider`; + const Container = styled.div<{ hasOrphans: boolean }>((props) => ({ marginTop: props.hasOrphans ? 20 : 0, marginBottom: 20, @@ -133,6 +142,7 @@ interface NodeProps { status: State['status'][keyof State['status']]; groupStatus: Record; api: API; + collapsedData: Record; } const Node = React.memo(function Node({ @@ -149,6 +159,7 @@ const Node = React.memo(function Node({ isExpanded, setExpanded, onSelectStoryId, + collapsedData, api, }) { const { isDesktop, isMobile, setMobileMenuOpen } = useLayout(); @@ -264,21 +275,64 @@ const Node = React.memo(function Node({ {item.renderLabel?.(item, api) || item.name} - {isExpanded && ( +
+ {isExpanded && ( + { + event.preventDefault(); + // @ts-expect-error (non strict) + setFullyExpanded(); + }} + > + {isFullyExpanded ? : } + + )} { - event.preventDefault(); - // @ts-expect-error (non strict) - setFullyExpanded(); + onClick={() => { + type ImportPath = string; + type ComponentPath = string; + + const importPathMap = new Map(); + const storyFilesMap = new Map(); + + const traverseChildren = (children: string[]) => { + children.forEach((childId) => { + const child = collapsedData[childId]; + if (child.type === 'story') { + const componentPath = (child as any).componentPath; + const importPath = child.importPath; + const storyFile = storyFilesMap.get(importPath) || []; + + storyFile.push({ id: child.id, name: child.name }); + storyFilesMap.set(importPath, storyFile); + importPathMap.set(importPath, componentPath); + } else if (child.type !== 'docs') { + traverseChildren(child.children); + } + }); + }; + + traverseChildren(item.children); + + const testingModuleRunRequestPayload: TestingModuleRunRequestPayload = { + providerId: TEST_PROVIDER_ID, + payload: Array.from(importPathMap.entries()).map(([importPath, componentPath]) => ({ + importPath, + componentPath, + stories: storyFilesMap.get(importPath) ?? [], + })), + }; + + api.emit(TESTING_MODULE_RUN_REQUEST, testingModuleRunRequestPayload); }} > - {isFullyExpanded ? : } + - )} +
); } @@ -560,9 +614,11 @@ export const Tree = React.memo<{ return ( // @ts-expect-error (TODO) = 4.7.x" + typescript: ">= 4.8.x" peerDependenciesMeta: typescript: optional: true bin: msw: cli/index.js - checksum: 10c0/f944d8eb67ccdcf74aba0ce433395a9616420b8b36d43f033635817ef7535553c7bf6854392a95b093546e3481ca99e9017525b6c0bd02f51528b669e672214d + checksum: 10c0/33a8c5697f7cb003a2af33ff6b259eaf7babf180fadf0697d107d0856ab0d2ff1a80d319e788d9127f289ff091334bee589f348180a1fdd0914bf8c4725830dc languageName: node linkType: hard @@ -22077,7 +22147,7 @@ __metadata: languageName: node linkType: hard -"outvariant@npm:^1.2.1, outvariant@npm:^1.4.0, outvariant@npm:^1.4.2": +"outvariant@npm:^1.4.0, outvariant@npm:^1.4.2, outvariant@npm:^1.4.3": version: 1.4.3 resolution: "outvariant@npm:1.4.3" checksum: 10c0/5976ca7740349cb8c71bd3382e2a762b1aeca6f33dc984d9d896acdf3c61f78c3afcf1bfe9cc633a7b3c4b295ec94d292048f83ea2b2594fae4496656eba992c @@ -22579,10 +22649,10 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^6.2.0": - version: 6.2.2 - resolution: "path-to-regexp@npm:6.2.2" - checksum: 10c0/4b60852d3501fd05ca9dd08c70033d73844e5eca14e41f499f069afa8364f780f15c5098002f93bd42af8b3514de62ac6e82a53b5662de881d2b08c9ef21ea6b +"path-to-regexp@npm:^6.3.0": + version: 6.3.0 + resolution: "path-to-regexp@npm:6.3.0" + checksum: 10c0/73b67f4638b41cde56254e6354e46ae3a2ebc08279583f6af3d96fe4664fc75788f74ed0d18ca44fa4a98491b69434f9eee73b97bb5314bd1b5adb700f5c18d6 languageName: node linkType: hard @@ -27094,10 +27164,17 @@ __metadata: languageName: node linkType: hard -"tinybench@npm:^2.8.0": - version: 2.8.0 - resolution: "tinybench@npm:2.8.0" - checksum: 10c0/5a9a642351fa3e4955e0cbf38f5674be5f3ba6730fd872fd23a5c953ad6c914234d5aba6ea41ef88820180a81829ceece5bd8d3967c490c5171bca1141c2f24d +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c + languageName: node + linkType: hard + +"tinyexec@npm:^0.3.0": + version: 0.3.0 + resolution: "tinyexec@npm:0.3.0" + checksum: 10c0/138a4f4241aea6b6312559508468ab275a31955e66e2f57ed206e0aaabecee622624f208c5740345f0a66e33478fd065e359ed1eb1269eb6fd4fa25d44d0ba3b languageName: node linkType: hard @@ -28515,18 +28592,17 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:2.0.5": - version: 2.0.5 - resolution: "vite-node@npm:2.0.5" +"vite-node@npm:2.1.1": + version: 2.1.1 + resolution: "vite-node@npm:2.1.1" dependencies: cac: "npm:^6.7.14" - debug: "npm:^4.3.5" + debug: "npm:^4.3.6" pathe: "npm:^1.1.2" - tinyrainbow: "npm:^1.2.0" vite: "npm:^5.0.0" bin: vite-node: vite-node.mjs - checksum: 10c0/affcc58ae8d45bce3e8bc3b5767acd57c24441634e2cd967cf97f4e5ed2bcead1714b60150cdf7ee153ebad47659c5cd419883207e1a95b69790331e3243749f + checksum: 10c0/8a8b958df3d48af915e07e7efb042ee4c036ca0b73d2c411dc29254fd3533ada0807ce5096d8339894d3e786418b7d1a9c4ae02718c6aca11b5098de2b14c336 languageName: node linkType: hard @@ -28666,34 +28742,34 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^2.0.0, vitest@npm:^2.0.5": - version: 2.0.5 - resolution: "vitest@npm:2.0.5" - dependencies: - "@ampproject/remapping": "npm:^2.3.0" - "@vitest/expect": "npm:2.0.5" - "@vitest/pretty-format": "npm:^2.0.5" - "@vitest/runner": "npm:2.0.5" - "@vitest/snapshot": "npm:2.0.5" - "@vitest/spy": "npm:2.0.5" - "@vitest/utils": "npm:2.0.5" +"vitest@npm:^2.1.1": + version: 2.1.1 + resolution: "vitest@npm:2.1.1" + dependencies: + "@vitest/expect": "npm:2.1.1" + "@vitest/mocker": "npm:2.1.1" + "@vitest/pretty-format": "npm:^2.1.1" + "@vitest/runner": "npm:2.1.1" + "@vitest/snapshot": "npm:2.1.1" + "@vitest/spy": "npm:2.1.1" + "@vitest/utils": "npm:2.1.1" chai: "npm:^5.1.1" - debug: "npm:^4.3.5" - execa: "npm:^8.0.1" - magic-string: "npm:^0.30.10" + debug: "npm:^4.3.6" + magic-string: "npm:^0.30.11" pathe: "npm:^1.1.2" std-env: "npm:^3.7.0" - tinybench: "npm:^2.8.0" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^0.3.0" tinypool: "npm:^1.0.0" tinyrainbow: "npm:^1.2.0" vite: "npm:^5.0.0" - vite-node: "npm:2.0.5" + vite-node: "npm:2.1.1" why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 2.0.5 - "@vitest/ui": 2.0.5 + "@vitest/browser": 2.1.1 + "@vitest/ui": 2.1.1 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -28711,7 +28787,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10c0/b4e6cca00816bf967a8589111ded72faa12f92f94ccdd0dcd0698ffcfdfc52ec662753f66b387549c600ac699b993fd952efbd99dc57fcf4d1c69a2f1022b259 + checksum: 10c0/77a67092338613376dadd8f6f6872383db8409402ce400ac1de48efd87a7214183e798484a3eb2310221c03554e37a00f9fdbc91e49194e7c68e009a5589f494 languageName: node linkType: hard From 123c6d2ea1cd5fc9b3c0d7b3f715d1143ef202a3 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Mon, 23 Sep 2024 23:19:02 +0200 Subject: [PATCH 05/23] Lazy boot for Vitest runner --- code/addons/test/package.json | 1 + code/addons/test/src/node/boot-test-runner.ts | 145 ++++++++++++------ code/addons/test/src/node/reporter.ts | 14 +- code/addons/test/src/node/test-manager.ts | 1 + code/addons/test/src/node/vitest.ts | 15 +- code/addons/test/src/preset.ts | 39 ++++- .../components/sidebar/SidebarBottom.tsx | 45 +++++- code/yarn.lock | 1 + 8 files changed, 203 insertions(+), 58 deletions(-) diff --git a/code/addons/test/package.json b/code/addons/test/package.json index c8ca27c7f2eb..fd2c1140108d 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -80,6 +80,7 @@ "@vitest/runner": "^2.1.1", "boxen": "^8.0.1", "find-up": "^7.0.0", + "lodash": "^4.17.21", "semver": "^7.6.3", "tinyrainbow": "^1.2.0", "ts-dedent": "^2.2.0", diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts index b4b86b8a6518..6127080fd812 100644 --- a/code/addons/test/src/node/boot-test-runner.ts +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -11,64 +11,107 @@ import { import { log } from '../logger'; -export function bootTestRunner(channel: Channel) { - // This path is a bit confusing, but essentiall `boot-test-runner` gets bundled into the preset bundle - // which is at the root. Then, from the root, we want to load `node/vitest.js` - const sub = join(__dirname, 'node', 'vitest.js'); - - let child: ChildProcess; - - function restartChildProcess() { - child?.kill(); - log('Restarting Child Process...'); - child = startChildProcess(); - } - - function startChildProcess() { - child = fork(sub, [], { - // We want to pipe output and error - // so that we can prefix the logs in the terminal - // with a clear identifier - stdio: ['inherit', 'pipe', 'pipe', 'ipc'], - silent: true, - }); +const MAX_RESTART_ATTEMPTS = 2; - child.stdout?.on('data', (data) => { - log(data); - }); +// This path is a bit confusing, but essentially `boot-test-runner` gets bundled into the preset bundle +// which is at the root. Then, from the root, we want to load `node/vitest.js` +const vitestModulePath = join(__dirname, 'node', 'vitest.js'); - child.stderr?.on('data', (data) => { - log(data); - }); +export const bootTestRunner = (channel: Channel, initEvent?: string, initArgs?: any[]) => + new Promise((resolve, reject) => { + let attempts = 0; + let child: null | ChildProcess; - child.on('message', (result: any) => { - if (result.type === 'error') { - log(result.message); - log(result.error); - restartChildProcess(); - } else { - channel.emit(result.type, ...(result.args || [])); - } - }); + const forwardRun = (...args: any[]): void => { + child?.send({ type: TESTING_MODULE_RUN_REQUEST, args, from: 'server' }); + }; + const forwardRunAll = (...args: any[]): void => { + child?.send({ type: TESTING_MODULE_RUN_ALL_REQUEST, args, from: 'server' }); + }; + const forwardWatchMode = (...args: any[]): void => { + child?.send({ type: TESTING_MODULE_WATCH_MODE_REQUEST, args, from: 'server' }); + }; + const forwardCancel = (...args: any[]): void => { + child?.send({ type: TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, args, from: 'server' }); + }; - return child; - } + const startChildProcess = () => { + child = fork(vitestModulePath, [], { + // We want to pipe output and error + // so that we can prefix the logs in the terminal + // with a clear identifier + stdio: ['inherit', 'pipe', 'pipe', 'ipc'], + silent: true, + }); - child = startChildProcess(); + child.stdout?.on('data', (data) => { + log(data); + }); - channel.on(TESTING_MODULE_RUN_REQUEST, (...args) => { - child.send({ type: TESTING_MODULE_RUN_REQUEST, args, from: 'server' }); - }); + child.stderr?.on('data', (data) => { + log(data); + }); - channel.on(TESTING_MODULE_RUN_ALL_REQUEST, (...args) => { - child.send({ type: TESTING_MODULE_RUN_ALL_REQUEST, args, from: 'server' }); - }); + child.on('message', (result: any) => { + switch (result.type) { + case 'ready': { + attempts = 0; + child?.send({ type: initEvent, args: initArgs, from: 'server' }); + channel.on(TESTING_MODULE_RUN_REQUEST, forwardRun); + channel.on(TESTING_MODULE_RUN_ALL_REQUEST, forwardRunAll); + channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, forwardWatchMode); + channel.on(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, forwardCancel); + channel.emit(result.type, ...(result.args || [])); + resolve(result); + return; + } - channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, (...args) => { - child.send({ type: TESTING_MODULE_WATCH_MODE_REQUEST, args, from: 'server' }); - }); + case 'error': { + channel.off(TESTING_MODULE_RUN_REQUEST, forwardRun); + channel.off(TESTING_MODULE_RUN_ALL_REQUEST, forwardRunAll); + channel.off(TESTING_MODULE_WATCH_MODE_REQUEST, forwardWatchMode); + channel.off(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, forwardCancel); - channel.on(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, (...args) => { - child.send({ type: TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, args, from: 'server' }); + child?.kill(); + child = null; + + if (result.message) { + log(result.message); + } + if (result.error) { + log(result.error); + } + + if (attempts >= MAX_RESTART_ATTEMPTS) { + log(`Aborting test runner process after ${MAX_RESTART_ATTEMPTS} restart attempts`); + channel.emit( + 'error', + `Aborting test runner process after ${MAX_RESTART_ATTEMPTS} restart attempts` + ); + reject(new Error('Test runner process failed to start')); + } else { + attempts += 1; + log(`Restarting test runner process (attempt ${attempts}/${MAX_RESTART_ATTEMPTS})`); + setTimeout(startChildProcess, 500); + } + return; + } + } + }); + }; + + startChildProcess(); + + process.on('exit', () => { + child?.kill(); + process.exit(0); + }); + process.on('SIGINT', () => { + child?.kill(); + process.exit(0); + }); + process.on('SIGTERM', () => { + child?.kill(); + process.exit(0); + }); }); -} diff --git a/code/addons/test/src/node/reporter.ts b/code/addons/test/src/node/reporter.ts index 6a735a8fe8a6..4e8d4771f9a9 100644 --- a/code/addons/test/src/node/reporter.ts +++ b/code/addons/test/src/node/reporter.ts @@ -4,6 +4,7 @@ import { type Reporter } from 'vitest/reporters'; import type { TestingModuleRunAssertionResultPayload, + TestingModuleRunProgressPayload, TestingModuleRunResponsePayload, TestingModuleRunTestResultPayload, } from 'storybook/internal/core-events'; @@ -16,6 +17,7 @@ import type { Suite } from '@vitest/runner'; // functions from the `@vitest/runner` package. It is not complex and does not have // any significant dependencies. import { getTests } from '@vitest/runner/utils'; +import throttle from 'lodash/throttle.js'; import { TEST_PROVIDER_ID } from '../constants'; import type { TestManager } from './test-manager'; @@ -42,7 +44,11 @@ export default class StorybookReporter implements Reporter { ctx!: Vitest; - constructor(private testManager: TestManager) {} + sendReport: (payload: TestingModuleRunProgressPayload) => void; + + constructor(private testManager: TestManager) { + this.sendReport = throttle((payload) => this.testManager.sendProgressReport(payload), 200); + } onInit(ctx: Vitest) { this.ctx = ctx; @@ -142,14 +148,14 @@ export default class StorybookReporter implements Reporter { try { const progress = this.getProgressReport(); - this.testManager.sendProgressReport({ + this.sendReport({ status: 'success', payload: progress, providerId: TEST_PROVIDER_ID, }); } catch (e) { if (e instanceof Error) { - this.testManager.sendProgressReport({ + this.sendReport({ status: 'failed', providerId: TEST_PROVIDER_ID, error: { @@ -159,7 +165,7 @@ export default class StorybookReporter implements Reporter { }, }); } else { - this.testManager.sendProgressReport({ + this.sendReport({ status: 'failed', providerId: TEST_PROVIDER_ID, error: { diff --git a/code/addons/test/src/node/test-manager.ts b/code/addons/test/src/node/test-manager.ts index f0542b930aa0..91e4396b94b9 100644 --- a/code/addons/test/src/node/test-manager.ts +++ b/code/addons/test/src/node/test-manager.ts @@ -33,6 +33,7 @@ export class TestManager { async restartVitest(watchMode = false) { await this.vitestManager.closeVitest(); await this.vitestManager.startVitest(watchMode); + process.send?.({ type: 'ready', watchMode }); } async handleWatchModeRequest(request: TestingModuleWatchModeRequestPayload) { diff --git a/code/addons/test/src/node/vitest.ts b/code/addons/test/src/node/vitest.ts index b9053c2caaf7..b663aa7b83ba 100644 --- a/code/addons/test/src/node/vitest.ts +++ b/code/addons/test/src/node/vitest.ts @@ -31,5 +31,18 @@ process.on('uncaughtException', (err) => { }); process.on('unhandledRejection', (reason) => { - throw new Error(`Unhandled Rejection: ${reason}`); + throw reason; +}); + +process.on('exit', () => { + channel?.removeAllListeners(); + process.exit(0); +}); +process.on('SIGINT', () => { + channel?.removeAllListeners(); + process.exit(0); +}); +process.on('SIGTERM', () => { + channel?.removeAllListeners(); + process.exit(0); }); diff --git a/code/addons/test/src/preset.ts b/code/addons/test/src/preset.ts index be1665356f66..83e4cde6eba9 100644 --- a/code/addons/test/src/preset.ts +++ b/code/addons/test/src/preset.ts @@ -1,10 +1,47 @@ import type { Channel } from 'storybook/internal/channels'; +import { + TESTING_MODULE_RUN_ALL_REQUEST, + TESTING_MODULE_RUN_REQUEST, + TESTING_MODULE_WATCH_MODE_REQUEST, +} from 'storybook/internal/core-events'; import type { Options } from 'storybook/internal/types'; import { bootTestRunner } from './node/boot-test-runner'; // eslint-disable-next-line @typescript-eslint/naming-convention export const experimental_serverChannel = async (channel: Channel, options: Options) => { - bootTestRunner(channel); + let booting = false; + let booted = false; + const start = + (eventName: string) => + (...args: any[]) => { + if (!booted && !booting) { + booting = true; + bootTestRunner(channel, eventName, args) + .then(() => { + booted = true; + }) + .catch(() => { + booted = false; + }) + .finally(() => { + booting = false; + }); + } + }; + + channel.on(TESTING_MODULE_RUN_ALL_REQUEST, start(TESTING_MODULE_RUN_ALL_REQUEST)); + channel.on(TESTING_MODULE_RUN_REQUEST, start(TESTING_MODULE_RUN_REQUEST)); + channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, (payload) => { + if (payload.watchMode) { + start(TESTING_MODULE_WATCH_MODE_REQUEST)(payload); + } + }); + return channel; }; + +// TODO: +// 1 - Do not boot Vitest on Storybook boot, but rather on the first test run +// 2 - Handle cases where Vitest is already booted, so we dont boot it again +// 3 - Upon crash, provide a notification to the user diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index 7b126968963f..80ea16af6d44 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -1,14 +1,20 @@ import React, { useCallback, useEffect } from 'react'; import { styled } from '@storybook/core/theming'; -import type { API_FilterFunction } from '@storybook/types'; +import type { API_FilterFunction, API_StatusUpdate, API_StatusValue } from '@storybook/types'; +import { + TESTING_MODULE_RUN_PROGRESS_RESPONSE, + type TestingModuleRunProgressPayload, + type TestingModuleRunResponsePayload, +} from '@storybook/core/core-events'; import { type API, type State, useStorybookApi, useStorybookState, } from '@storybook/core/manager-api'; +import { useChannel } from '@storybook/core/preview-api'; import { FilterToggle } from './FilterToggle'; @@ -45,6 +51,30 @@ interface SidebarBottomProps { status: State['status']; } +const statusMap: Record = { + failed: 'error', + passed: 'success', + pending: 'pending', +}; + +function processTestReport(payload: TestingModuleRunResponsePayload) { + const result: API_StatusUpdate = {}; + + payload.testResults.forEach((testResult: any) => { + testResult.results.forEach(({ storyId, status, failureMessages }: any) => { + if (storyId) { + result[storyId] = { + title: 'Vitest', + status: statusMap[status], + description: failureMessages?.length ? failureMessages.join('\n') : '', + }; + } + }); + }); + + return result; +} + export const SidebarBottomBase = ({ api, status = {} }: SidebarBottomProps) => { const [showWarnings, setShowWarnings] = React.useState(false); const [showErrors, setShowErrors] = React.useState(false); @@ -99,5 +129,18 @@ export const SidebarBottomBase = ({ api, status = {} }: SidebarBottomProps) => { export const SidebarBottom = () => { const api = useStorybookApi(); const { status } = useStorybookState(); + + useEffect(() => { + api.getChannel()?.on(TESTING_MODULE_RUN_PROGRESS_RESPONSE, (data) => { + if ('payload' in data) { + console.log('progress', data); + // TODO clear statuses + api.experimental_updateStatus('figure-out-id', processTestReport(data.payload)); + } else { + console.log('error', data); + } + }); + }, [api]); + return ; }; diff --git a/code/yarn.lock b/code/yarn.lock index df9d7a6bf626..b6767c3250d9 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6246,6 +6246,7 @@ __metadata: boxen: "npm:^8.0.1" chalk: "npm:^5.3.0" find-up: "npm:^7.0.0" + lodash: "npm:^4.17.21" semver: "npm:^7.6.3" tinyrainbow: "npm:^1.2.0" ts-dedent: "npm:^2.2.0" From 72bd86295eccad00c3696c5f85747afcf0a42232 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 24 Sep 2024 13:15:03 +0200 Subject: [PATCH 06/23] Replace Node's process.fork with Execa and refactor/cleanup a bunch of things --- code/addons/test/package.json | 1 + code/addons/test/src/node/boot-test-runner.ts | 164 ++++++++---------- code/addons/test/src/node/test-manager.ts | 1 - code/addons/test/src/node/vitest.ts | 35 ++-- code/yarn.lock | 1 + 5 files changed, 90 insertions(+), 112 deletions(-) diff --git a/code/addons/test/package.json b/code/addons/test/package.json index fd2c1140108d..23856319cf4b 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -79,6 +79,7 @@ "@vitest/browser": "^2.1.1", "@vitest/runner": "^2.1.1", "boxen": "^8.0.1", + "execa": "^8.0.1", "find-up": "^7.0.0", "lodash": "^4.17.21", "semver": "^7.6.3", diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts index 6127080fd812..411706ee5150 100644 --- a/code/addons/test/src/node/boot-test-runner.ts +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -1,4 +1,4 @@ -import { type ChildProcess, fork } from 'node:child_process'; +import { type ChildProcess } from 'node:child_process'; import { join } from 'node:path'; import type { Channel } from 'storybook/internal/channels'; @@ -9,109 +9,91 @@ import { TESTING_MODULE_WATCH_MODE_REQUEST, } from 'storybook/internal/core-events'; +import { execaNode } from 'execa'; + import { log } from '../logger'; -const MAX_RESTART_ATTEMPTS = 2; +const MAX_START_ATTEMPTS = 3; +const MAX_START_TIME = 8000; // This path is a bit confusing, but essentially `boot-test-runner` gets bundled into the preset bundle // which is at the root. Then, from the root, we want to load `node/vitest.js` const vitestModulePath = join(__dirname, 'node', 'vitest.js'); -export const bootTestRunner = (channel: Channel, initEvent?: string, initArgs?: any[]) => - new Promise((resolve, reject) => { - let attempts = 0; - let child: null | ChildProcess; - - const forwardRun = (...args: any[]): void => { - child?.send({ type: TESTING_MODULE_RUN_REQUEST, args, from: 'server' }); - }; - const forwardRunAll = (...args: any[]): void => { - child?.send({ type: TESTING_MODULE_RUN_ALL_REQUEST, args, from: 'server' }); - }; - const forwardWatchMode = (...args: any[]): void => { - child?.send({ type: TESTING_MODULE_WATCH_MODE_REQUEST, args, from: 'server' }); - }; - const forwardCancel = (...args: any[]): void => { - child?.send({ type: TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, args, from: 'server' }); - }; - - const startChildProcess = () => { - child = fork(vitestModulePath, [], { - // We want to pipe output and error - // so that we can prefix the logs in the terminal - // with a clear identifier - stdio: ['inherit', 'pipe', 'pipe', 'ipc'], - silent: true, - }); +export const bootTestRunner = async (channel: Channel, initEvent?: string, initArgs?: any[]) => { + const now = Date.now(); + let child: null | ChildProcess; + + const forwardRun = (...args: any[]) => + child?.send({ args, from: 'server', type: TESTING_MODULE_RUN_REQUEST }); + const forwardRunAll = (...args: any[]) => + child?.send({ args, from: 'server', type: TESTING_MODULE_RUN_ALL_REQUEST }); + const forwardWatchMode = (...args: any[]) => + child?.send({ args, from: 'server', type: TESTING_MODULE_WATCH_MODE_REQUEST }); + const forwardCancel = (...args: any[]) => + child?.send({ args, from: 'server', type: TESTING_MODULE_CANCEL_TEST_RUN_REQUEST }); + + const killChild = () => { + channel.off(TESTING_MODULE_RUN_REQUEST, forwardRun); + channel.off(TESTING_MODULE_RUN_ALL_REQUEST, forwardRunAll); + channel.off(TESTING_MODULE_WATCH_MODE_REQUEST, forwardWatchMode); + channel.off(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, forwardCancel); + child?.kill(); + child = null; + }; + + const exit = (code = 0) => { + killChild(); + process.exit(code); + }; + + process.on('exit', exit); + process.on('SIGINT', () => exit(0)); + process.on('SIGTERM', () => exit(0)); + + const startChildProcess = (attempt = 1) => + new Promise((resolve, reject) => { + child = execaNode(vitestModulePath); + child.stdout?.on('data', log); + child.stderr?.on('data', log); - child.stdout?.on('data', (data) => { - log(data); - }); + child.on('message', (result: any) => { + if (result.type === 'ready') { + // Resend the event that triggered the boot sequence, now that the child is ready to handle it + child?.send({ type: initEvent, args: initArgs, from: 'server' }); - child.stderr?.on('data', (data) => { - log(data); - }); + // Forward all events from the channel to the child process + channel.on(TESTING_MODULE_RUN_REQUEST, forwardRun); + channel.on(TESTING_MODULE_RUN_ALL_REQUEST, forwardRunAll); + channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, forwardWatchMode); + channel.on(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, forwardCancel); - child.on('message', (result: any) => { - switch (result.type) { - case 'ready': { - attempts = 0; - child?.send({ type: initEvent, args: initArgs, from: 'server' }); - channel.on(TESTING_MODULE_RUN_REQUEST, forwardRun); - channel.on(TESTING_MODULE_RUN_ALL_REQUEST, forwardRunAll); - channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, forwardWatchMode); - channel.on(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, forwardCancel); - channel.emit(result.type, ...(result.args || [])); - resolve(result); - return; + resolve(); + } + + if (result.type === 'error') { + killChild(); + + if (result.message) { + log(result.message); + } + if (result.error) { + log(result.error); } - case 'error': { - channel.off(TESTING_MODULE_RUN_REQUEST, forwardRun); - channel.off(TESTING_MODULE_RUN_ALL_REQUEST, forwardRunAll); - channel.off(TESTING_MODULE_WATCH_MODE_REQUEST, forwardWatchMode); - channel.off(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, forwardCancel); - - child?.kill(); - child = null; - - if (result.message) { - log(result.message); - } - if (result.error) { - log(result.error); - } - - if (attempts >= MAX_RESTART_ATTEMPTS) { - log(`Aborting test runner process after ${MAX_RESTART_ATTEMPTS} restart attempts`); - channel.emit( - 'error', - `Aborting test runner process after ${MAX_RESTART_ATTEMPTS} restart attempts` - ); - reject(new Error('Test runner process failed to start')); - } else { - attempts += 1; - log(`Restarting test runner process (attempt ${attempts}/${MAX_RESTART_ATTEMPTS})`); - setTimeout(startChildProcess, 500); - } - return; + if (attempt >= MAX_START_ATTEMPTS) { + log(`Aborting test runner process after ${attempt} restart attempts`); + reject(); + } else if (Date.now() - now > MAX_START_TIME) { + log(`Aborting test runner process after ${MAX_START_TIME / 1000} seconds`); + reject(); + } else { + log(`Restarting test runner process (attempt ${attempt}/${MAX_START_ATTEMPTS})`); + setTimeout(() => startChildProcess(attempt + 1).then(resolve, reject), 1000); } } }); - }; - - startChildProcess(); - - process.on('exit', () => { - child?.kill(); - process.exit(0); }); - process.on('SIGINT', () => { - child?.kill(); - process.exit(0); - }); - process.on('SIGTERM', () => { - child?.kill(); - process.exit(0); - }); - }); + + await startChildProcess(); +}; diff --git a/code/addons/test/src/node/test-manager.ts b/code/addons/test/src/node/test-manager.ts index 91e4396b94b9..f0542b930aa0 100644 --- a/code/addons/test/src/node/test-manager.ts +++ b/code/addons/test/src/node/test-manager.ts @@ -33,7 +33,6 @@ export class TestManager { async restartVitest(watchMode = false) { await this.vitestManager.closeVitest(); await this.vitestManager.startVitest(watchMode); - process.send?.({ type: 'ready', watchMode }); } async handleWatchModeRequest(request: TestingModuleWatchModeRequestPayload) { diff --git a/code/addons/test/src/node/vitest.ts b/code/addons/test/src/node/vitest.ts index b663aa7b83ba..976b1869c40a 100644 --- a/code/addons/test/src/node/vitest.ts +++ b/code/addons/test/src/node/vitest.ts @@ -12,9 +12,7 @@ const channel: Channel = new Channel({ async: true, transport: { send: (event) => { - if (process.send) { - process.send(event); - } + process.send?.(event); }, setHandler: (handler) => { process.on('message', handler); @@ -23,26 +21,23 @@ const channel: Channel = new Channel({ }); const testManager = new TestManager(channel); -testManager.restartVitest(); +testManager.restartVitest().then(() => process.send?.({ type: 'ready' })); + +const exit = (code = 0) => { + channel?.removeAllListeners(); + process.exit(code); +}; + +process.on('exit', exit); +process.on('SIGINT', () => exit(0)); +process.on('SIGTERM', () => exit(0)); process.on('uncaughtException', (err) => { - process.send?.({ type: 'error', message: 'Uncaught Exception', error: err.stack }); - process.exit(1); + process.send?.({ type: 'error', message: 'Uncaught exception', error: err.stack }); + exit(1); }); process.on('unhandledRejection', (reason) => { - throw reason; -}); - -process.on('exit', () => { - channel?.removeAllListeners(); - process.exit(0); -}); -process.on('SIGINT', () => { - channel?.removeAllListeners(); - process.exit(0); -}); -process.on('SIGTERM', () => { - channel?.removeAllListeners(); - process.exit(0); + process.send?.({ type: 'error', message: 'Unhandled rejection', error: reason }); + exit(1); }); diff --git a/code/yarn.lock b/code/yarn.lock index b6767c3250d9..5cae1a7f021f 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6245,6 +6245,7 @@ __metadata: "@vitest/runner": "npm:^2.1.1" boxen: "npm:^8.0.1" chalk: "npm:^5.3.0" + execa: "npm:^8.0.1" find-up: "npm:^7.0.0" lodash: "npm:^4.17.21" semver: "npm:^7.6.3" From 840fa049a29503c6ce843335d82270fb06b7e9f1 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 24 Sep 2024 14:27:49 +0200 Subject: [PATCH 07/23] make test run succeed --- code/.storybook/preview.tsx | 6 +++++- code/.storybook/vitest.config.ts | 11 +++++++++++ .../src/components/components/tabs/tabs.stories.tsx | 8 ++++++-- code/core/template/stories/hooks.stories.ts | 7 +++---- code/core/template/stories/loaders.stories.ts | 8 ++++---- code/lib/blocks/src/controls/Boolean.stories.tsx | 2 ++ 6 files changed, 31 insertions(+), 11 deletions(-) diff --git a/code/.storybook/preview.tsx b/code/.storybook/preview.tsx index 1f366074846f..7730317876dc 100644 --- a/code/.storybook/preview.tsx +++ b/code/.storybook/preview.tsx @@ -135,7 +135,9 @@ export const loaders = [ * The DocsContext will then be added via the decorator below. */ async ({ parameters: { relativeCsfPaths, attached = true } }) => { - if (!relativeCsfPaths) { + // TODO bring a better way to skip tests when running as part of the vitest plugin instead of __STORYBOOK_URL__ + // eslint-disable-next-line no-underscore-dangle + if (!relativeCsfPaths || (import.meta as any).env?.__STORYBOOK_URL__) { return {}; } const csfFiles = await Promise.all( @@ -358,3 +360,5 @@ export const parameters = { }, }, }; + +export const tags = ['test', 'vitest']; diff --git a/code/.storybook/vitest.config.ts b/code/.storybook/vitest.config.ts index 9745a67949e6..82754f87e4cd 100644 --- a/code/.storybook/vitest.config.ts +++ b/code/.storybook/vitest.config.ts @@ -19,11 +19,15 @@ if (process.env.INSPECT === 'true') { export default mergeConfig( vitestCommonConfig, + // @ts-expect-error added this because of testNamePattern below defineProject({ plugins: [ import('@storybook/experimental-addon-test/vitest-plugin').then(({ storybookTest }) => storybookTest({ configDir: process.cwd(), + tags: { + include: ['vitest'], + }, }) ), ...extraPlugins, @@ -36,11 +40,18 @@ export default mergeConfig( ], exclude: [ ...defaultExclude, + // these stories mostly depend on things that won't be available + // like useOf hook, PreviewWeb and DocsContext + '../lib/blocks/**/*.stories.tsx', '../node_modules/**', '**/__mockdata__/**', + '../**/__mockdata__/**', // expected to fail in Vitest because of fetching /iframe.html to cause ECONNREFUSED '**/Zoom.stories.tsx', ], + // TODO: bring this back once portable stories support @storybook/core/preview-api hooks + // @ts-expect-error this type does not exist but the property does! + testNamePattern: /^(?!.*(UseState)).*$/, browser: { enabled: true, name: 'chromium', diff --git a/code/core/src/components/components/tabs/tabs.stories.tsx b/code/core/src/components/components/tabs/tabs.stories.tsx index 5e23fd7ecb7b..9a67158ec33c 100644 --- a/code/core/src/components/components/tabs/tabs.stories.tsx +++ b/code/core/src/components/components/tabs/tabs.stories.tsx @@ -178,13 +178,15 @@ const customViewports = { }; export const StatefulDynamicWithOpenTooltip = { + // TODO: remove this when we support new viewport global format in the vitest integration + tags: ['!vitest'], parameters: { viewport: { - viewports: customViewports, + options: customViewports, }, chromatic: { viewports: [380] }, }, - globals: { sb_theme: 'light', viewport: 'sized' }, + globals: { sb_theme: 'light', viewport: { value: 'sized' } }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -222,6 +224,8 @@ export const StatefulDynamicWithOpenTooltip = { export const StatefulDynamicWithSelectedAddon = { ...StatefulDynamicWithOpenTooltip, + // TODO: remove this when we support new viewport global format in the vitest integration + tags: ['!vitest'], play: async (context) => { await StatefulDynamicWithOpenTooltip.play(context); const canvas = within(context.canvasElement); diff --git a/code/core/template/stories/hooks.stories.ts b/code/core/template/stories/hooks.stories.ts index ce145288f147..6db9b35a2284 100644 --- a/code/core/template/stories/hooks.stories.ts +++ b/code/core/template/stories/hooks.stories.ts @@ -1,4 +1,4 @@ -import type { PartialStoryFn, PlayFunctionContext } from '@storybook/core/types'; +import type { PartialStoryFn, StoryContext } from '@storybook/core/types'; import { global as globalThis } from '@storybook/global'; import { userEvent, within } from '@storybook/test'; @@ -12,7 +12,6 @@ export const UseState = { decorators: [ (story: PartialStoryFn) => { const [count, setCount] = useState(0); - return story({ args: { label: `Clicked ${count} times`, @@ -23,7 +22,7 @@ export const UseState = { }); }, ], - play: async ({ canvasElement }: PlayFunctionContext) => { + play: async ({ canvasElement }: StoryContext) => { const button = await within(canvasElement).findByText('Clicked 0 times'); await userEvent.click(button); @@ -32,7 +31,7 @@ export const UseState = { }; // NOTE: it isn't possible to write a play function for this story, as the -// useEffect hooked doesn't fire until *after* the story has rendered, which includes +// useEffect hook doesn't fire until *after* the story has rendered, which includes // the play function running. export const UseEffect = { decorators: [ diff --git a/code/core/template/stories/loaders.stories.ts b/code/core/template/stories/loaders.stories.ts index 37a4ac62c0c8..1c24f5214fa2 100644 --- a/code/core/template/stories/loaders.stories.ts +++ b/code/core/template/stories/loaders.stories.ts @@ -1,10 +1,10 @@ -import type { PartialStoryFn, PlayFunctionContext, StoryContext } from '@storybook/core/types'; +import type { PartialStoryFn, StoryContext } from '@storybook/core/types'; import { global as globalThis } from '@storybook/global'; import { expect, within } from '@storybook/test'; export default { component: globalThis.Components.Pre, - loaders: [async () => new Promise((r) => setTimeout(() => r({ componentValue: 7 }), 3000))], + loaders: [async () => new Promise((r) => setTimeout(() => r({ componentValue: 7 }), 1000))], decorators: [ (storyFn: PartialStoryFn, context: StoryContext) => storyFn({ args: { ...context.args, object: context.loaded } }), @@ -12,8 +12,8 @@ export default { }; export const Inheritance = { - loaders: [async () => new Promise((r) => setTimeout(() => r({ storyValue: 3 }), 1000))], - play: async ({ canvasElement }: PlayFunctionContext) => { + loaders: [async () => new Promise((r) => setTimeout(() => r({ storyValue: 3 }), 500))], + play: async ({ canvasElement }: StoryContext) => { const canvas = within(canvasElement); await expect(JSON.parse(canvas.getByTestId('pre').innerText)).toEqual({ projectValue: 2, diff --git a/code/lib/blocks/src/controls/Boolean.stories.tsx b/code/lib/blocks/src/controls/Boolean.stories.tsx index f8b5e96c94a7..ad7365c03838 100644 --- a/code/lib/blocks/src/controls/Boolean.stories.tsx +++ b/code/lib/blocks/src/controls/Boolean.stories.tsx @@ -83,6 +83,7 @@ export const Toggling: Story = { }); }); }, + tags: ['!vitest'], }; export const TogglingInDocs: Story = { @@ -95,6 +96,7 @@ export const TogglingInDocs: Story = { autoplay: true, }, }, + tags: ['!vitest'], }; export const Readonly: Story = { From 76c1c4e5ed6bca3905cb80e6d8029103e2fdfad8 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 24 Sep 2024 15:44:28 +0200 Subject: [PATCH 08/23] Add unit tests and fix timeout logic --- .../test/src/node/boot-test-runner.test.ts | 141 ++++++++++++++++++ code/addons/test/src/node/boot-test-runner.ts | 21 ++- 2 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 code/addons/test/src/node/boot-test-runner.test.ts diff --git a/code/addons/test/src/node/boot-test-runner.test.ts b/code/addons/test/src/node/boot-test-runner.test.ts new file mode 100644 index 000000000000..c39330422cfd --- /dev/null +++ b/code/addons/test/src/node/boot-test-runner.test.ts @@ -0,0 +1,141 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { Channel, type ChannelTransport } from '@storybook/core/channels'; + +import { + TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, + TESTING_MODULE_RUN_ALL_REQUEST, + TESTING_MODULE_RUN_REQUEST, + TESTING_MODULE_WATCH_MODE_REQUEST, +} from '@storybook/core/core-events'; + +import { execaNode } from 'execa'; + +import { log } from '../logger'; +import { bootTestRunner } from './boot-test-runner'; + +let stdout; +let stderr; +let message; + +const child = vi.hoisted(() => ({ + stdout: { + on: vi.fn((event, callback) => { + stdout = callback; + }), + }, + stderr: { + on: vi.fn((event, callback) => { + stderr = callback; + }), + }, + on: vi.fn((event, callback) => { + message = callback; + }), + send: vi.fn(), + kill: vi.fn(), +})); + +vi.mock('execa', () => ({ + execaNode: vi.fn().mockReturnValue(child), +})); + +vi.mock('../logger', () => ({ + log: vi.fn(), +})); + +beforeEach(() => { + vi.useFakeTimers(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +const transport = { setHandler: vi.fn(), send: vi.fn() } satisfies ChannelTransport; +const mockChannel = new Channel({ transport }); + +describe('bootTestRunner', () => { + it('should execute vitest.js', async () => { + bootTestRunner(mockChannel); + expect(execaNode).toHaveBeenCalledWith(expect.stringMatching(/\/vitest\.js$/)); + }); + + it('should log stdout and stderr', async () => { + bootTestRunner(mockChannel); + stdout('foo'); + stderr('bar'); + expect(log).toHaveBeenCalledWith('foo'); + expect(log).toHaveBeenCalledWith('bar'); + }); + + it('should wait for vitest to be ready', async () => { + let ready; + const promise = bootTestRunner(mockChannel).then(() => { + ready = true; + }); + expect(ready).toBeUndefined(); + message({ type: 'ready' }); + await expect(promise).resolves.toBeUndefined(); + expect(ready).toBe(true); + }); + + it('should abort if vitest doesn’t become ready in time', async () => { + const promise = bootTestRunner(mockChannel); + vi.advanceTimersByTime(10000); + await expect(promise).rejects.toThrow(); + }); + + it('should abort if vitest fails to start repeatedly', async () => { + const promise = bootTestRunner(mockChannel); + message({ type: 'error' }); + vi.advanceTimersByTime(1000); + message({ type: 'error' }); + vi.advanceTimersByTime(1000); + message({ type: 'error' }); + await expect(promise).rejects.toThrow(); + }); + + it('should forward channel events', async () => { + bootTestRunner(mockChannel); + message({ type: 'ready' }); + + mockChannel.emit(TESTING_MODULE_RUN_REQUEST, 'foo'); + expect(child.send).toHaveBeenCalledWith({ + args: ['foo'], + from: 'server', + type: TESTING_MODULE_RUN_REQUEST, + }); + + mockChannel.emit(TESTING_MODULE_RUN_ALL_REQUEST, 'bar'); + expect(child.send).toHaveBeenCalledWith({ + args: ['bar'], + from: 'server', + type: TESTING_MODULE_RUN_ALL_REQUEST, + }); + + mockChannel.emit(TESTING_MODULE_WATCH_MODE_REQUEST, 'baz'); + expect(child.send).toHaveBeenCalledWith({ + args: ['baz'], + from: 'server', + type: TESTING_MODULE_WATCH_MODE_REQUEST, + }); + + mockChannel.emit(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, 'qux'); + expect(child.send).toHaveBeenCalledWith({ + args: ['qux'], + from: 'server', + type: TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, + }); + }); + + it('should resend init event', async () => { + bootTestRunner(mockChannel, 'init', ['foo']); + message({ type: 'ready' }); + expect(child.send).toHaveBeenCalledWith({ + args: ['foo'], + from: 'server', + type: 'init', + }); + }); +}); diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts index 411706ee5150..8c55817a778b 100644 --- a/code/addons/test/src/node/boot-test-runner.ts +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -21,7 +21,7 @@ const MAX_START_TIME = 8000; const vitestModulePath = join(__dirname, 'node', 'vitest.js'); export const bootTestRunner = async (channel: Channel, initEvent?: string, initArgs?: any[]) => { - const now = Date.now(); + let aborted = false; let child: null | ChildProcess; const forwardRun = (...args: any[]) => @@ -60,7 +60,9 @@ export const bootTestRunner = async (channel: Channel, initEvent?: string, initA child.on('message', (result: any) => { if (result.type === 'ready') { // Resend the event that triggered the boot sequence, now that the child is ready to handle it - child?.send({ type: initEvent, args: initArgs, from: 'server' }); + if (initEvent && initArgs) { + child?.send({ type: initEvent, args: initArgs, from: 'server' }); + } // Forward all events from the channel to the child process channel.on(TESTING_MODULE_RUN_REQUEST, forwardRun); @@ -84,10 +86,7 @@ export const bootTestRunner = async (channel: Channel, initEvent?: string, initA if (attempt >= MAX_START_ATTEMPTS) { log(`Aborting test runner process after ${attempt} restart attempts`); reject(); - } else if (Date.now() - now > MAX_START_TIME) { - log(`Aborting test runner process after ${MAX_START_TIME / 1000} seconds`); - reject(); - } else { + } else if (!aborted) { log(`Restarting test runner process (attempt ${attempt}/${MAX_START_ATTEMPTS})`); setTimeout(() => startChildProcess(attempt + 1).then(resolve, reject), 1000); } @@ -95,5 +94,13 @@ export const bootTestRunner = async (channel: Channel, initEvent?: string, initA }); }); - await startChildProcess(); + const timeout = new Promise((resolve, reject) => { + setTimeout(() => { + log(`Aborting test runner process after ${MAX_START_TIME / 1000} seconds`); + aborted = true; + reject(); + }, MAX_START_TIME); + }); + + await Promise.race([startChildProcess(), timeout]); }; From 998b8e1200a3177eae92f1a4112be829e8ef577e Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 24 Sep 2024 16:59:35 +0200 Subject: [PATCH 09/23] fix remaining tests --- code/.storybook/main.ts | 8 ++++---- code/.storybook/vitest.config.ts | 9 +++++---- .../core/src/components/components/tabs/tabs.stories.tsx | 4 ++-- .../src/manager/components/sidebar/Sidebar.stories.tsx | 3 +++ code/core/src/preview-api/Errors.stories.tsx | 2 ++ code/core/template/stories/hooks.stories.ts | 2 ++ 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/code/.storybook/main.ts b/code/.storybook/main.ts index a4cedde66608..24f90201dce8 100644 --- a/code/.storybook/main.ts +++ b/code/.storybook/main.ts @@ -79,10 +79,10 @@ const config: StorybookConfig = { directory: '../addons/interactions/src', titlePrefix: 'addons/interactions', }, - // { - // directory: '../addons/interactions/template/stories', - // titlePrefix: 'addons/interactions', - // }, + { + directory: '../addons/interactions/template/stories', + titlePrefix: 'addons/interactions/tests', + }, ], addons: [ '@storybook/addon-links', diff --git a/code/.storybook/vitest.config.ts b/code/.storybook/vitest.config.ts index 82754f87e4cd..9bb705c06e8d 100644 --- a/code/.storybook/vitest.config.ts +++ b/code/.storybook/vitest.config.ts @@ -36,13 +36,14 @@ export default mergeConfig( name: 'storybook-ui', include: [ // TODO: Can be reverted. Temporarily I am adding all stories so that I can trigger tests for all stories in the UI. - '../{core,addons,lib}/**/{src,components,template}/**/*.{story,stories}.?(c|m)[jt]s?(x)', + '../addons/**/*.{story,stories}.?(c|m)[jt]s?(x)', + '../core/template/stories/**/*.{story,stories}.?(c|m)[jt]s?(x)', + '../core/src/manager/**/*.{story,stories}.?(c|m)[jt]s?(x)', + '../core/src/preview-api/**/*.{story,stories}.?(c|m)[jt]s?(x)', + '../core/src/components/{brand,components}/**/*.{story,stories}.?(c|m)[jt]s?(x)', ], exclude: [ ...defaultExclude, - // these stories mostly depend on things that won't be available - // like useOf hook, PreviewWeb and DocsContext - '../lib/blocks/**/*.stories.tsx', '../node_modules/**', '**/__mockdata__/**', '../**/__mockdata__/**', diff --git a/code/core/src/components/components/tabs/tabs.stories.tsx b/code/core/src/components/components/tabs/tabs.stories.tsx index 9a67158ec33c..b45d82ed8529 100644 --- a/code/core/src/components/components/tabs/tabs.stories.tsx +++ b/code/core/src/components/components/tabs/tabs.stories.tsx @@ -178,7 +178,7 @@ const customViewports = { }; export const StatefulDynamicWithOpenTooltip = { - // TODO: remove this when we support new viewport global format in the vitest integration + // TODO VITEST INTEGRATION: remove this when we support new viewport global format in the vitest integration tags: ['!vitest'], parameters: { viewport: { @@ -224,7 +224,7 @@ export const StatefulDynamicWithOpenTooltip = { export const StatefulDynamicWithSelectedAddon = { ...StatefulDynamicWithOpenTooltip, - // TODO: remove this when we support new viewport global format in the vitest integration + // TODO VITEST INTEGRATION: remove this when we support new viewport global format in the vitest integration tags: ['!vitest'], play: async (context) => { await StatefulDynamicWithOpenTooltip.play(context); diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx index 972606b79598..30be9d3e9bf5 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx @@ -41,6 +41,7 @@ const managerContext: any = { getShortcutKeys: fn(() => ({ search: ['control', 'shift', 's'] })).mockName( 'api::getShortcutKeys' ), + getChannel: fn().mockName('api::getChannel'), selectStory: fn().mockName('api::selectStory'), experimental_setFilter: fn().mockName('api::experimental_setFilter'), }, @@ -177,6 +178,8 @@ export const WithRefsNarrow: Story = { value: 'narrow', }, }, + // TODO VITEST INTEGRATION: remove this when we support new viewport global format in the vitest integration + tags: ['!vitest'], }; export const LoadingWithRefs: Story = { diff --git a/code/core/src/preview-api/Errors.stories.tsx b/code/core/src/preview-api/Errors.stories.tsx index 2782999de392..dc70f7af4fe2 100644 --- a/code/core/src/preview-api/Errors.stories.tsx +++ b/code/core/src/preview-api/Errors.stories.tsx @@ -46,6 +46,8 @@ export default { args: { id: 'sb-errordisplay', }, + // This story cannot be properly tested in portable stories as it depends on an element that only exists in Storybook's preview HTML + tags: ['!vitest'], }; export const MyError = { diff --git a/code/core/template/stories/hooks.stories.ts b/code/core/template/stories/hooks.stories.ts index 6db9b35a2284..5e7d8ae0c4cd 100644 --- a/code/core/template/stories/hooks.stories.ts +++ b/code/core/template/stories/hooks.stories.ts @@ -28,6 +28,8 @@ export const UseState = { await userEvent.click(button); await within(canvasElement).findByText('Clicked 1 times'); }, + // TODO VITEST INTEGRATION: remove this once we support Storybook hooks in portable stories + tags: ['!vitest'], }; // NOTE: it isn't possible to write a play function for this story, as the From 1d4536674725a5495a8477232b4a9cd2a5f7b598 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 25 Sep 2024 09:23:11 +0200 Subject: [PATCH 10/23] Fix unit test on Windows and avoid default export --- code/addons/test/src/node/boot-test-runner.test.ts | 2 +- code/addons/test/src/node/reporter.ts | 3 +-- code/addons/test/src/node/vitest-manager.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/code/addons/test/src/node/boot-test-runner.test.ts b/code/addons/test/src/node/boot-test-runner.test.ts index c39330422cfd..3df44635f676 100644 --- a/code/addons/test/src/node/boot-test-runner.test.ts +++ b/code/addons/test/src/node/boot-test-runner.test.ts @@ -58,7 +58,7 @@ const mockChannel = new Channel({ transport }); describe('bootTestRunner', () => { it('should execute vitest.js', async () => { bootTestRunner(mockChannel); - expect(execaNode).toHaveBeenCalledWith(expect.stringMatching(/\/vitest\.js$/)); + expect(execaNode).toHaveBeenCalledWith(expect.stringMatching(/vitest\.js$/)); }); it('should log stdout and stderr', async () => { diff --git a/code/addons/test/src/node/reporter.ts b/code/addons/test/src/node/reporter.ts index 4e8d4771f9a9..b993868bc273 100644 --- a/code/addons/test/src/node/reporter.ts +++ b/code/addons/test/src/node/reporter.ts @@ -37,7 +37,7 @@ const StatusMap: Record = { todo: 'todo', }; -export default class StorybookReporter implements Reporter { +export class StorybookReporter implements Reporter { testStatusData: API_StatusUpdate = {}; start = 0; @@ -195,4 +195,3 @@ export default class StorybookReporter implements Reporter { this.clearVitestState(); } } -export { StorybookReporter }; diff --git a/code/addons/test/src/node/vitest-manager.ts b/code/addons/test/src/node/vitest-manager.ts index 98460f9e2926..bf2dfefcadb3 100644 --- a/code/addons/test/src/node/vitest-manager.ts +++ b/code/addons/test/src/node/vitest-manager.ts @@ -5,7 +5,7 @@ import type { TestProject, TestSpecification, Vitest, WorkspaceProject } from 'v import type { Channel } from 'storybook/internal/channels'; import type { TestingModuleRunRequestPayload } from 'storybook/internal/core-events'; -import StorybookReporter from './reporter'; +import { StorybookReporter } from './reporter'; import type { TestManager } from './test-manager'; export class VitestManager { From b307cd9aaee6e5f38c2467bd5b7eac73265c8c6d Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 25 Sep 2024 12:40:01 +0200 Subject: [PATCH 11/23] add test module crash report event --- code/addons/test/src/node/boot-test-runner.ts | 11 ++++++++++- code/core/src/core-events/index.ts | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts index 8c55817a778b..99655101a49f 100644 --- a/code/addons/test/src/node/boot-test-runner.ts +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -4,6 +4,7 @@ import { join } from 'node:path'; import type { Channel } from 'storybook/internal/channels'; import { TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, + TESTING_MODULE_CRASH_REPORT, TESTING_MODULE_RUN_ALL_REQUEST, TESTING_MODULE_RUN_REQUEST, TESTING_MODULE_WATCH_MODE_REQUEST, @@ -55,7 +56,15 @@ export const bootTestRunner = async (channel: Channel, initEvent?: string, initA new Promise((resolve, reject) => { child = execaNode(vitestModulePath); child.stdout?.on('data', log); - child.stderr?.on('data', log); + child.stderr?.on('data', (data) => { + const message = data.toString(); + // TODO: improve this error handling. Example use case is Playwright is not installed + if (message.includes('Error: browserType.launch')) { + channel.emit(TESTING_MODULE_CRASH_REPORT, message); + } + + log(data); + }); child.on('message', (result: any) => { if (result.type === 'ready') { diff --git a/code/core/src/core-events/index.ts b/code/core/src/core-events/index.ts index 3ebf839d43c6..086590bd6df6 100644 --- a/code/core/src/core-events/index.ts +++ b/code/core/src/core-events/index.ts @@ -85,6 +85,7 @@ enum events { CREATE_NEW_STORYFILE_REQUEST = 'createNewStoryfileRequest', CREATE_NEW_STORYFILE_RESPONSE = 'createNewStoryfileResponse', + TESTING_MODULE_CRASH_REPORT = 'testingModuleCrashReport', TESTING_MODULE_RUN_REQUEST = 'testingModuleRunRequest', TESTING_MODULE_RUN_PROGRESS_RESPONSE = 'testingModuleRunProgressResponse', TESTING_MODULE_RUN_ALL_REQUEST = 'testingModuleRunAllRequest', @@ -154,6 +155,7 @@ export const { SAVE_STORY_RESPONSE, ARGTYPES_INFO_REQUEST, ARGTYPES_INFO_RESPONSE, + TESTING_MODULE_CRASH_REPORT, TESTING_MODULE_RUN_REQUEST, TESTING_MODULE_RUN_PROGRESS_RESPONSE, TESTING_MODULE_RUN_ALL_REQUEST, From 1966455f7bcf3a8c5fbfca10bcf3dd5fc3ecee6c Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 25 Sep 2024 13:21:17 +0200 Subject: [PATCH 12/23] fix exports --- code/core/src/manager/globals/exports.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts index b992ff74b4d5..b7cac2e8a3c6 100644 --- a/code/core/src/manager/globals/exports.ts +++ b/code/core/src/manager/globals/exports.ts @@ -808,6 +808,7 @@ export default { 'TELEMETRY_ERROR', 'TESTING_MODULE_CANCEL_TEST_RUN_REQUEST', 'TESTING_MODULE_CANCEL_TEST_RUN_RESPONSE', + 'TESTING_MODULE_CRASH_REPORT', 'TESTING_MODULE_RUN_ALL_REQUEST', 'TESTING_MODULE_RUN_PROGRESS_RESPONSE', 'TESTING_MODULE_RUN_REQUEST', @@ -871,6 +872,7 @@ export default { 'TELEMETRY_ERROR', 'TESTING_MODULE_CANCEL_TEST_RUN_REQUEST', 'TESTING_MODULE_CANCEL_TEST_RUN_RESPONSE', + 'TESTING_MODULE_CRASH_REPORT', 'TESTING_MODULE_RUN_ALL_REQUEST', 'TESTING_MODULE_RUN_PROGRESS_RESPONSE', 'TESTING_MODULE_RUN_REQUEST', @@ -934,6 +936,7 @@ export default { 'TELEMETRY_ERROR', 'TESTING_MODULE_CANCEL_TEST_RUN_REQUEST', 'TESTING_MODULE_CANCEL_TEST_RUN_RESPONSE', + 'TESTING_MODULE_CRASH_REPORT', 'TESTING_MODULE_RUN_ALL_REQUEST', 'TESTING_MODULE_RUN_PROGRESS_RESPONSE', 'TESTING_MODULE_RUN_REQUEST', From 04d892b09442f27cf1b4479af2dfc61810056bd8 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 25 Sep 2024 13:56:52 +0200 Subject: [PATCH 13/23] Fix timeout after ready and debouple test manager from process context --- code/addons/test/src/node/boot-test-runner.ts | 16 ++++++++-------- code/addons/test/src/node/test-manager.ts | 15 +++++++++++++-- code/addons/test/src/node/vitest-manager.ts | 7 +++---- code/addons/test/src/node/vitest.ts | 10 ++++++++-- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts index 99655101a49f..0d96e2e10831 100644 --- a/code/addons/test/src/node/boot-test-runner.ts +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -103,13 +103,13 @@ export const bootTestRunner = async (channel: Channel, initEvent?: string, initA }); }); - const timeout = new Promise((resolve, reject) => { - setTimeout(() => { - log(`Aborting test runner process after ${MAX_START_TIME / 1000} seconds`); - aborted = true; - reject(); - }, MAX_START_TIME); + const timeout = new Promise((_, reject) => + setTimeout(reject, MAX_START_TIME, new Error('Aborting test runner process due to timeout')) + ); + + await Promise.race([startChildProcess(), timeout]).catch((e) => { + log(e.message); + aborted = true; + throw e; }); - - await Promise.race([startChildProcess(), timeout]); }; diff --git a/code/addons/test/src/node/test-manager.ts b/code/addons/test/src/node/test-manager.ts index f0542b930aa0..54577fea8291 100644 --- a/code/addons/test/src/node/test-manager.ts +++ b/code/addons/test/src/node/test-manager.ts @@ -14,20 +14,31 @@ import { TEST_PROVIDER_ID } from '../constants'; import { VitestManager } from './vitest-manager'; export class TestManager { + private options: { + onError: (message: string, error: Error) => void; + onReady: () => void; + }; + private vitestManager: VitestManager; watchMode = false; - constructor(private channel: Channel) { + constructor( + private channel: Channel, + options: typeof TestManager.prototype.options + ) { process.env.TEST = 'true'; process.env.VITEST = 'true'; process.env.NODE_ENV ??= 'test'; + this.options = options; this.vitestManager = new VitestManager(channel, this); this.channel.on(TESTING_MODULE_RUN_REQUEST, this.handleRunRequest.bind(this)); this.channel.on(TESTING_MODULE_RUN_ALL_REQUEST, this.handleRunAllRequest.bind(this)); this.channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, this.handleWatchModeRequest.bind(this)); + + this.vitestManager.startVitest().then(options.onReady); } async restartVitest(watchMode = false) { @@ -79,6 +90,6 @@ export class TestManager { } async reportFatalError(message: string, error: Error | any) { - process.send?.({ type: 'error', message, error: error.stack ?? error }); + this.options.onError(message, error); } } diff --git a/code/addons/test/src/node/vitest-manager.ts b/code/addons/test/src/node/vitest-manager.ts index bf2dfefcadb3..c478e390417f 100644 --- a/code/addons/test/src/node/vitest-manager.ts +++ b/code/addons/test/src/node/vitest-manager.ts @@ -37,11 +37,10 @@ export class VitestManager { }, }); - if (!this.vitest || this.vitest.projects.length < 1) { - return; + // TODO what should happen if there's no projects? + if (this.vitest?.projects.length) { + await this.vitest.init(); } - - await this.vitest.init(); } async runAllTests() { diff --git a/code/addons/test/src/node/vitest.ts b/code/addons/test/src/node/vitest.ts index 976b1869c40a..c4a4cb30bada 100644 --- a/code/addons/test/src/node/vitest.ts +++ b/code/addons/test/src/node/vitest.ts @@ -20,8 +20,14 @@ const channel: Channel = new Channel({ }, }); -const testManager = new TestManager(channel); -testManager.restartVitest().then(() => process.send?.({ type: 'ready' })); +new TestManager(channel, { + onError: (message, error) => { + process.send?.({ type: 'error', message, error: error.stack ?? error }); + }, + onReady: () => { + process.send?.({ type: 'ready' }); + }, +}); const exit = (code = 0) => { channel?.removeAllListeners(); From 857383175dec27927942f5142c638a93e473a695 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 25 Sep 2024 15:32:43 +0200 Subject: [PATCH 14/23] Fix progress reporting --- code/addons/test/src/node/boot-test-runner.test.ts | 10 +++++++--- code/addons/test/src/node/boot-test-runner.ts | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/code/addons/test/src/node/boot-test-runner.test.ts b/code/addons/test/src/node/boot-test-runner.test.ts index 3df44635f676..5ea559cb15df 100644 --- a/code/addons/test/src/node/boot-test-runner.test.ts +++ b/code/addons/test/src/node/boot-test-runner.test.ts @@ -5,6 +5,7 @@ import { Channel, type ChannelTransport } from '@storybook/core/channels'; import { TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, TESTING_MODULE_RUN_ALL_REQUEST, + TESTING_MODULE_RUN_PROGRESS_RESPONSE, TESTING_MODULE_RUN_REQUEST, TESTING_MODULE_WATCH_MODE_REQUEST, } from '@storybook/core/core-events'; @@ -14,9 +15,9 @@ import { execaNode } from 'execa'; import { log } from '../logger'; import { bootTestRunner } from './boot-test-runner'; -let stdout; -let stderr; -let message; +let stdout: (chunk: any) => void; +let stderr: (chunk: any) => void; +let message: (event: any) => void; const child = vi.hoisted(() => ({ stdout: { @@ -100,6 +101,9 @@ describe('bootTestRunner', () => { bootTestRunner(mockChannel); message({ type: 'ready' }); + message({ type: TESTING_MODULE_RUN_PROGRESS_RESPONSE, args: ['foo'] }); + expect(mockChannel.last(TESTING_MODULE_RUN_PROGRESS_RESPONSE)).toEqual(['foo']); + mockChannel.emit(TESTING_MODULE_RUN_REQUEST, 'foo'); expect(child.send).toHaveBeenCalledWith({ args: ['foo'], diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts index 0d96e2e10831..6a018dc57c0b 100644 --- a/code/addons/test/src/node/boot-test-runner.ts +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -80,9 +80,7 @@ export const bootTestRunner = async (channel: Channel, initEvent?: string, initA channel.on(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, forwardCancel); resolve(); - } - - if (result.type === 'error') { + } else if (result.type === 'error') { killChild(); if (result.message) { @@ -99,6 +97,8 @@ export const bootTestRunner = async (channel: Channel, initEvent?: string, initA log(`Restarting test runner process (attempt ${attempt}/${MAX_START_ATTEMPTS})`); setTimeout(() => startChildProcess(attempt + 1).then(resolve, reject), 1000); } + } else { + channel.emit(result.type, ...result.args); } }); }); From 0d002352ca66f5ee9f19fef0075df65c3af81e66 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 25 Sep 2024 15:59:04 +0200 Subject: [PATCH 15/23] Add missing deps --- code/addons/test/package.json | 2 ++ code/yarn.lock | 2 ++ 2 files changed, 4 insertions(+) diff --git a/code/addons/test/package.json b/code/addons/test/package.json index ab0f83da5ff1..4072e0a766f0 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -82,6 +82,8 @@ "execa": "^8.0.1", "find-up": "^7.0.0", "lodash": "^4.17.21", + "react": "^18.2.0", + "react-dom": "^18.2.0", "semver": "^7.6.3", "tinyrainbow": "^1.2.0", "ts-dedent": "^2.2.0", diff --git a/code/yarn.lock b/code/yarn.lock index 449bb28f72b0..6f475ca40a43 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6243,6 +6243,8 @@ __metadata: execa: "npm:^8.0.1" find-up: "npm:^7.0.0" lodash: "npm:^4.17.21" + react: "npm:^18.2.0" + react-dom: "npm:^18.2.0" semver: "npm:^7.6.3" tinyrainbow: "npm:^1.2.0" ts-dedent: "npm:^2.2.0" From 54dc6e3cc0cc7cfebeb6a446c0fffeb8e835f240 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 25 Sep 2024 16:01:31 +0200 Subject: [PATCH 16/23] Linting --- code/core/src/manager-api/tests/addons.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/core/src/manager-api/tests/addons.test.js b/code/core/src/manager-api/tests/addons.test.js index 6fd93fb4dca5..c4f5c46d41df 100644 --- a/code/core/src/manager-api/tests/addons.test.js +++ b/code/core/src/manager-api/tests/addons.test.js @@ -24,7 +24,7 @@ const TEST_PROVIDERS = { id: 'storybook/test/test-provider', title: 'Component tests', }, -} +}; const provider = { getElements(type) { From b332fac75871bb50342c971cb7ebb81c7c00f8f7 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 25 Sep 2024 16:30:12 +0200 Subject: [PATCH 17/23] bring back vitest patch --- code/lib/test/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/lib/test/package.json b/code/lib/test/package.json index d6d3dcd561de..78c907ac3543 100644 --- a/code/lib/test/package.json +++ b/code/lib/test/package.json @@ -54,6 +54,9 @@ "@vitest/spy": "2.0.5", "util": "^0.12.4" }, + "resolutions": { + "@vitest/expect@npm:2.0.5": "patch:@vitest/expect@npm%3A2.0.5#~/.yarn/patches/@vitest-expect-npm-2.0.5-8933466cce.patch" + }, "devDependencies": { "chai": "^5.1.1", "tinyspy": "^3.0.0", From b0a68afff209b2ef8b9414c07084529a3770ab44 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 25 Sep 2024 16:31:29 +0200 Subject: [PATCH 18/23] fix user event interactions on firefox --- .../interactions/template/stories/basics.stories.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/code/addons/interactions/template/stories/basics.stories.ts b/code/addons/interactions/template/stories/basics.stories.ts index 2e07b7c0e624..2db5f0e016f3 100644 --- a/code/addons/interactions/template/stories/basics.stories.ts +++ b/code/addons/interactions/template/stories/basics.stories.ts @@ -115,10 +115,17 @@ const UserEventSetup = { { keys: '[TouchA>]', target: canvas.getByRole('textbox') }, { keys: '[/TouchA]' }, ]); - await user.tab(); - await user.keyboard('{enter}'); const submitButton = await canvas.findByRole('button'); - await expect(submitButton).toHaveFocus(); + + if (navigator.userAgent.toLowerCase().includes('firefox')) { + // user event has a few issues on firefox, therefore we do it differently + await fireEvent.click(submitButton); + } else { + await user.tab(); + await user.keyboard('{enter}'); + await expect(submitButton).toHaveFocus(); + } + await expect(args.onSuccess).toHaveBeenCalled(); }); }, From 0123f8f008875ed85a60de1ad1e599e492b995fa Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 25 Sep 2024 16:53:19 +0200 Subject: [PATCH 19/23] fix vitest patch again --- code/lib/test/package.json | 3 --- code/package.json | 3 ++- code/yarn.lock | 12 ++++++++++++ .../portable-stories-kitchen-sink/react/package.json | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/code/lib/test/package.json b/code/lib/test/package.json index 78c907ac3543..d6d3dcd561de 100644 --- a/code/lib/test/package.json +++ b/code/lib/test/package.json @@ -54,9 +54,6 @@ "@vitest/spy": "2.0.5", "util": "^0.12.4" }, - "resolutions": { - "@vitest/expect@npm:2.0.5": "patch:@vitest/expect@npm%3A2.0.5#~/.yarn/patches/@vitest-expect-npm-2.0.5-8933466cce.patch" - }, "devDependencies": { "chai": "^5.1.1", "tinyspy": "^3.0.0", diff --git a/code/package.json b/code/package.json index c25c4e36ab8f..7aed473f8409 100644 --- a/code/package.json +++ b/code/package.json @@ -85,7 +85,8 @@ "playwright": "1.46.0", "playwright-core": "1.46.0", "serialize-javascript": "^3.1.0", - "type-fest": "~2.19" + "type-fest": "~2.19", + "@vitest/expect@npm:2.0.5": "patch:@vitest/expect@npm%3A2.0.5#~/.yarn/patches/@vitest-expect-npm-2.0.5-8933466cce.patch" }, "dependencies": { "@chromatic-com/storybook": "^1.6.1", diff --git a/code/yarn.lock b/code/yarn.lock index 6f475ca40a43..13203d825632 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -8871,6 +8871,18 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@patch:@vitest/expect@npm%3A2.0.5#~/.yarn/patches/@vitest-expect-npm-2.0.5-8933466cce.patch": + version: 2.0.5 + resolution: "@vitest/expect@patch:@vitest/expect@npm%3A2.0.5#~/.yarn/patches/@vitest-expect-npm-2.0.5-8933466cce.patch::version=2.0.5&hash=368591" + dependencies: + "@vitest/spy": "npm:2.0.5" + "@vitest/utils": "npm:2.0.5" + chai: "npm:^5.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/0834adbe2c7094277adf3a45aa20fece84ca3b381fb3fe118fa424d2d490f20d0365c57dcaf845b413e583e36118f2815afa34bd175e305d6169c947e29576c3 + languageName: node + linkType: hard + "@vitest/mocker@npm:2.1.1": version: 2.1.1 resolution: "@vitest/mocker@npm:2.1.1" diff --git a/test-storybooks/portable-stories-kitchen-sink/react/package.json b/test-storybooks/portable-stories-kitchen-sink/react/package.json index da4d32f26d17..ab3ed4bbe8e0 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/package.json +++ b/test-storybooks/portable-stories-kitchen-sink/react/package.json @@ -115,4 +115,4 @@ "typescript": "^5.2.2", "vite": "^5.1.1" } -} \ No newline at end of file +} From 23329d9dce3c85a0eba1233b65314d7c1d057517 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 25 Sep 2024 17:00:30 +0200 Subject: [PATCH 20/23] exclude story files from coverage --- code/.storybook/vitest.config.ts | 1 - code/vitest.config.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/code/.storybook/vitest.config.ts b/code/.storybook/vitest.config.ts index 9bb705c06e8d..412e84c61c2d 100644 --- a/code/.storybook/vitest.config.ts +++ b/code/.storybook/vitest.config.ts @@ -35,7 +35,6 @@ export default mergeConfig( test: { name: 'storybook-ui', include: [ - // TODO: Can be reverted. Temporarily I am adding all stories so that I can trigger tests for all stories in the UI. '../addons/**/*.{story,stories}.?(c|m)[jt]s?(x)', '../core/template/stories/**/*.{story,stories}.?(c|m)[jt]s?(x)', '../core/src/manager/**/*.{story,stories}.?(c|m)[jt]s?(x)', diff --git a/code/vitest.config.ts b/code/vitest.config.ts index cabfb4e8b0ce..0eef8a17b4ba 100644 --- a/code/vitest.config.ts +++ b/code/vitest.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ 'playwright.config.ts', 'vitest-setup.ts', 'vitest.helpers.ts', + '**/*.stories.*', ], }, }, From 5b11aa2c0c827c1e4978d75598e1d633b2503603 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 25 Sep 2024 17:24:26 +0200 Subject: [PATCH 21/23] Add tests for TestManager and VitestManager --- .../addons/test/src/node/test-manager.test.ts | 129 ++++++++++++++++++ code/addons/test/src/node/test-manager.ts | 31 +++-- code/addons/test/src/node/vitest-manager.ts | 2 +- 3 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 code/addons/test/src/node/test-manager.test.ts diff --git a/code/addons/test/src/node/test-manager.test.ts b/code/addons/test/src/node/test-manager.test.ts new file mode 100644 index 000000000000..5bfb2e3791a8 --- /dev/null +++ b/code/addons/test/src/node/test-manager.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createVitest } from 'vitest/node'; + +import { Channel, type ChannelTransport } from '@storybook/core/channels'; + +import path from 'path'; + +import { TEST_PROVIDER_ID } from '../constants'; +import { TestManager } from './test-manager'; + +const vitest = vi.hoisted(() => ({ + projects: [{}], + init: vi.fn(), + close: vi.fn(), + runFiles: vi.fn(), + cancelCurrentRun: vi.fn(), + globTestSpecs: vi.fn(), +})); + +vi.mock('vitest/node', () => ({ + createVitest: vi.fn(() => Promise.resolve(vitest)), +})); + +const transport = { setHandler: vi.fn(), send: vi.fn() } satisfies ChannelTransport; +const mockChannel = new Channel({ transport }); + +const tests = [ + { + project: { config: { env: { __STORYBOOK_URL__: 'http://localhost:6006' } } }, + moduleId: path.join(process.cwd(), 'path/to/file'), + }, + { + project: { config: { env: { __STORYBOOK_URL__: 'http://localhost:6006' } } }, + moduleId: path.join(process.cwd(), 'path/to/another/file'), + }, +]; + +describe('TestManager', () => { + it('should create a vitest instance', async () => { + new TestManager(mockChannel); + await new Promise((r) => setTimeout(r, 10)); + expect(createVitest).toHaveBeenCalled(); + }); + + it('should call onReady callback', async () => { + const onReady = vi.fn(); + new TestManager(mockChannel, { onReady }); + await new Promise((r) => setTimeout(r, 10)); + expect(onReady).toHaveBeenCalled(); + }); + + it('TestManager.start should start vitest and resolve when ready', async () => { + const testManager = await TestManager.start(mockChannel); + expect(testManager).toBeInstanceOf(TestManager); + expect(createVitest).toHaveBeenCalled(); + }); + + it('should handle watch mode request', async () => { + const testManager = await TestManager.start(mockChannel); + expect(testManager.watchMode).toBe(false); + expect(createVitest).toHaveBeenCalledTimes(1); + + await testManager.handleWatchModeRequest({ providerId: TEST_PROVIDER_ID, watchMode: true }); + expect(testManager.watchMode).toBe(true); + expect(createVitest).toHaveBeenCalledTimes(2); + }); + + it('should handle run request', async () => { + vitest.globTestSpecs.mockImplementation(() => tests); + const testManager = await TestManager.start(mockChannel); + expect(createVitest).toHaveBeenCalledTimes(1); + + await testManager.handleRunRequest({ + providerId: TEST_PROVIDER_ID, + payload: [ + { + stories: [], + importPath: 'path/to/file', + componentPath: 'path/to/component', + }, + { + stories: [], + importPath: 'path/to/another/file', + componentPath: 'path/to/another/component', + }, + ], + }); + expect(createVitest).toHaveBeenCalledTimes(1); + expect(vitest.runFiles).toHaveBeenCalledWith(tests, true); + }); + + it('should filter tests', async () => { + vitest.globTestSpecs.mockImplementation(() => tests); + const testManager = await TestManager.start(mockChannel); + + await testManager.handleRunRequest({ + providerId: TEST_PROVIDER_ID, + payload: [ + { + stories: [], + importPath: 'path/to/unknown/file', + componentPath: 'path/to/unknown/component', + }, + ], + }); + expect(vitest.runFiles).toHaveBeenCalledWith([], true); + + await testManager.handleRunRequest({ + providerId: TEST_PROVIDER_ID, + payload: [ + { + stories: [], + importPath: 'path/to/file', + componentPath: 'path/to/component', + }, + ], + }); + expect(vitest.runFiles).toHaveBeenCalledWith(tests.slice(0, 1), true); + }); + + it('should handle run all request', async () => { + const testManager = await TestManager.start(mockChannel); + expect(createVitest).toHaveBeenCalledTimes(1); + + await testManager.handleRunAllRequest({ providerId: TEST_PROVIDER_ID }); + expect(createVitest).toHaveBeenCalledTimes(1); + expect(vitest.runFiles).toHaveBeenCalledWith(tests, true); + }); +}); diff --git a/code/addons/test/src/node/test-manager.ts b/code/addons/test/src/node/test-manager.ts index 54577fea8291..5f5c9264f03f 100644 --- a/code/addons/test/src/node/test-manager.ts +++ b/code/addons/test/src/node/test-manager.ts @@ -14,31 +14,24 @@ import { TEST_PROVIDER_ID } from '../constants'; import { VitestManager } from './vitest-manager'; export class TestManager { - private options: { - onError: (message: string, error: Error) => void; - onReady: () => void; - }; - private vitestManager: VitestManager; watchMode = false; constructor( private channel: Channel, - options: typeof TestManager.prototype.options + private options: { + onError?: (message: string, error: Error) => void; + onReady?: () => void; + } = {} ) { - process.env.TEST = 'true'; - process.env.VITEST = 'true'; - process.env.NODE_ENV ??= 'test'; - - this.options = options; this.vitestManager = new VitestManager(channel, this); this.channel.on(TESTING_MODULE_RUN_REQUEST, this.handleRunRequest.bind(this)); this.channel.on(TESTING_MODULE_RUN_ALL_REQUEST, this.handleRunAllRequest.bind(this)); this.channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, this.handleWatchModeRequest.bind(this)); - this.vitestManager.startVitest().then(options.onReady); + this.vitestManager.startVitest().then(() => options.onReady?.()); } async restartVitest(watchMode = false) { @@ -90,6 +83,18 @@ export class TestManager { } async reportFatalError(message: string, error: Error | any) { - this.options.onError(message, error); + this.options.onError?.(message, error); + } + + static async start(channel: Channel, options: typeof TestManager.prototype.options = {}) { + return new Promise((resolve) => { + const testManager = new TestManager(channel, { + ...options, + onReady: () => { + resolve(testManager); + options.onReady?.(); + }, + }); + }); } } diff --git a/code/addons/test/src/node/vitest-manager.ts b/code/addons/test/src/node/vitest-manager.ts index c478e390417f..5b6f136040bd 100644 --- a/code/addons/test/src/node/vitest-manager.ts +++ b/code/addons/test/src/node/vitest-manager.ts @@ -24,7 +24,7 @@ export class VitestManager { this.vitest = await createVitest('test', { watch: watchMode, passWithNoTests: true, - // standalone: true, + standalone: true, changed: watchMode, // TODO: // Do we want to enable Vite's default reporter? From b7c33b59aa5eeab617591ce87d0f4a1f3f05d6f1 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 25 Sep 2024 17:27:53 +0200 Subject: [PATCH 22/23] Comment out log messages for now --- code/core/src/manager/components/sidebar/SidebarBottom.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index 80ea16af6d44..290e44ac8f6a 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -133,11 +133,11 @@ export const SidebarBottom = () => { useEffect(() => { api.getChannel()?.on(TESTING_MODULE_RUN_PROGRESS_RESPONSE, (data) => { if ('payload' in data) { - console.log('progress', data); + // console.log('progress', data); // TODO clear statuses api.experimental_updateStatus('figure-out-id', processTestReport(data.payload)); } else { - console.log('error', data); + // console.log('error', data); } }); }, [api]); From f8095ba05fa9921f6030f5bc9038acd214c3e7ed Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 25 Sep 2024 20:15:16 +0200 Subject: [PATCH 23/23] Increase wait time --- code/addons/test/src/node/test-manager.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/addons/test/src/node/test-manager.test.ts b/code/addons/test/src/node/test-manager.test.ts index 5bfb2e3791a8..1b87bdedf0a5 100644 --- a/code/addons/test/src/node/test-manager.test.ts +++ b/code/addons/test/src/node/test-manager.test.ts @@ -38,14 +38,14 @@ const tests = [ describe('TestManager', () => { it('should create a vitest instance', async () => { new TestManager(mockChannel); - await new Promise((r) => setTimeout(r, 10)); + await new Promise((r) => setTimeout(r, 1000)); expect(createVitest).toHaveBeenCalled(); }); it('should call onReady callback', async () => { const onReady = vi.fn(); new TestManager(mockChannel, { onReady }); - await new Promise((r) => setTimeout(r, 10)); + await new Promise((r) => setTimeout(r, 1000)); expect(onReady).toHaveBeenCalled(); });