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/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 25a83b4396ea..412e84c61c2d 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, @@ -31,17 +35,23 @@ 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)', + '../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, '../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/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(); }); }, diff --git a/code/addons/test/package.json b/code/addons/test/package.json index ab78d5cc6840..4072e0a766f0 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -70,20 +70,28 @@ "prep": "jiti ../../../scripts/prepare/addon-bundle.ts" }, "dependencies": { - "@storybook/csf": "^0.1.11" + "@storybook/csf": "^0.1.11", + "@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", + "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", - "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" }, @@ -103,7 +111,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/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/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/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/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..5ea559cb15df --- /dev/null +++ b/code/addons/test/src/node/boot-test-runner.test.ts @@ -0,0 +1,145 @@ +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_PROGRESS_RESPONSE, + 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: (chunk: any) => void; +let stderr: (chunk: any) => void; +let message: (event: any) => void; + +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' }); + + 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'], + 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 new file mode 100644 index 000000000000..6a018dc57c0b --- /dev/null +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -0,0 +1,115 @@ +import { type ChildProcess } 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_CRASH_REPORT, + TESTING_MODULE_RUN_ALL_REQUEST, + TESTING_MODULE_RUN_REQUEST, + TESTING_MODULE_WATCH_MODE_REQUEST, +} from 'storybook/internal/core-events'; + +import { execaNode } from 'execa'; + +import { log } from '../logger'; + +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 = async (channel: Channel, initEvent?: string, initArgs?: any[]) => { + let aborted = false; + 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', (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') { + // Resend the event that triggered the boot sequence, now that the child is ready to handle it + 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); + 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); + + resolve(); + } else if (result.type === 'error') { + killChild(); + + if (result.message) { + log(result.message); + } + if (result.error) { + log(result.error); + } + + if (attempt >= MAX_START_ATTEMPTS) { + log(`Aborting test runner process after ${attempt} restart attempts`); + reject(); + } else if (!aborted) { + 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); + } + }); + }); + + 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; + }); +}; diff --git a/code/addons/test/src/node/reporter.ts b/code/addons/test/src/node/reporter.ts new file mode 100644 index 000000000000..b993868bc273 --- /dev/null +++ b/code/addons/test/src/node/reporter.ts @@ -0,0 +1,197 @@ +import type { TaskState } from 'vitest'; +import type { Vitest } from 'vitest/node'; +import { type Reporter } from 'vitest/reporters'; + +import type { + TestingModuleRunAssertionResultPayload, + TestingModuleRunProgressPayload, + 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 throttle from 'lodash/throttle.js'; + +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 class StorybookReporter implements Reporter { + testStatusData: API_StatusUpdate = {}; + + start = 0; + + ctx!: Vitest; + + sendReport: (payload: TestingModuleRunProgressPayload) => void; + + constructor(private testManager: TestManager) { + this.sendReport = throttle((payload) => this.testManager.sendProgressReport(payload), 200); + } + + 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.sendReport({ + status: 'success', + payload: progress, + providerId: TEST_PROVIDER_ID, + }); + } catch (e) { + if (e instanceof Error) { + this.sendReport({ + status: 'failed', + providerId: TEST_PROVIDER_ID, + error: { + name: 'Failed to gather test results', + message: e.message, + stack: e.stack, + }, + }); + } else { + this.sendReport({ + 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(); + } +} 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..1b87bdedf0a5 --- /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, 1000)); + expect(createVitest).toHaveBeenCalled(); + }); + + it('should call onReady callback', async () => { + const onReady = vi.fn(); + new TestManager(mockChannel, { onReady }); + await new Promise((r) => setTimeout(r, 1000)); + 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 new file mode 100644 index 000000000000..5f5c9264f03f --- /dev/null +++ b/code/addons/test/src/node/test-manager.ts @@ -0,0 +1,100 @@ +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, + private options: { + onError?: (message: string, error: Error) => void; + onReady?: () => void; + } = {} + ) { + 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) { + 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) { + 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 new file mode 100644 index 000000000000..5b6f136040bd --- /dev/null +++ b/code/addons/test/src/node/vitest-manager.ts @@ -0,0 +1,103 @@ +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, + }, + }); + + // TODO what should happen if there's no projects? + if (this.vitest?.projects.length) { + 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..c4a4cb30bada --- /dev/null +++ b/code/addons/test/src/node/vitest.ts @@ -0,0 +1,49 @@ +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) => { + process.send?.(event); + }, + setHandler: (handler) => { + process.on('message', handler); + }, + }, +}); + +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(); + 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 }); + exit(1); +}); + +process.on('unhandledRejection', (reason) => { + process.send?.({ type: 'error', message: 'Unhandled rejection', error: reason }); + exit(1); +}); diff --git a/code/addons/test/src/preset.ts b/code/addons/test/src/preset.ts index 8b4bc31a9013..83e4cde6eba9 100644 --- a/code/addons/test/src/preset.ts +++ b/code/addons/test/src/preset.ts @@ -1,7 +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) => { + 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/components/components/tabs/tabs.stories.tsx b/code/core/src/components/components/tabs/tabs.stories.tsx index 5e23fd7ecb7b..b45d82ed8529 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 VITEST INTEGRATION: 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 VITEST INTEGRATION: 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/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..086590bd6df6 100644 --- a/code/core/src/core-events/index.ts +++ b/code/core/src/core-events/index.ts @@ -84,6 +84,14 @@ enum events { ARGTYPES_INFO_RESPONSE = 'argtypesInfoResponse', 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', + 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 +155,13 @@ 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, + 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 +170,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-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/manager-api/tests/addons.test.js b/code/core/src/manager-api/tests/addons.test.js index 4d64ecc95224..c4f5c46d41df 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); }); }); 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/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/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index 7b126968963f..290e44ac8f6a 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/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) ; 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', } diff --git a/code/core/template/stories/hooks.stories.ts b/code/core/template/stories/hooks.stories.ts index ce145288f147..5e7d8ae0c4cd 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,16 +22,18 @@ export const UseState = { }); }, ], - play: async ({ canvasElement }: PlayFunctionContext) => { + play: async ({ canvasElement }: StoryContext) => { const button = await within(canvasElement).findByText('Clicked 0 times'); 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 -// 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 = { diff --git a/code/package.json b/code/package.json index 247d6630af96..7aed473f8409 100644 --- a/code/package.json +++ b/code/package.json @@ -81,12 +81,12 @@ "@types/babel__traverse@npm:*": "patch:@types/babel__traverse@npm%3A7.20.6#~/.yarn/patches/@types-babel__traverse-npm-7.20.6-fac4243243.patch", "@types/babel__traverse@npm:^7.18.0": "patch:@types/babel__traverse@npm%3A7.20.6#~/.yarn/patches/@types-babel__traverse-npm-7.20.6-fac4243243.patch", "@types/node": "^22.0.0", - "@vitest/expect@npm:2.0.5": "patch:@vitest/expect@npm%3A2.0.5#~/.yarn/patches/@vitest-expect-npm-2.0.5-8933466cce.patch", "esbuild": "^0.23.0", "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", @@ -179,9 +179,9 @@ "@typescript-eslint/parser": "^6.18.1", "@vitejs/plugin-react": "^3.0.1", "@vitejs/plugin-vue": "^4.4.0", - "@vitest/browser": "^2.0.5", - "@vitest/coverage-istanbul": "^2.0.5", - "@vitest/coverage-v8": "^2.0.5", + "@vitest/browser": "^2.1.1", + "@vitest/coverage-istanbul": "^2.1.1", + "@vitest/coverage-v8": "^2.1.1", "create-storybook": "workspace:*", "cross-env": "^7.0.3", "danger": "^12.3.3", @@ -221,7 +221,7 @@ "util": "^0.12.4", "vite": "^4.0.0", "vite-plugin-inspect": "^0.8.5", - "vitest": "^2.0.5", + "vitest": "^2.1.1", "wait-on": "^7.0.1" }, "dependenciesMeta": { 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.*', ], }, }, diff --git a/code/yarn.lock b/code/yarn.lock index 08b73cfd20cc..13203d825632 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -3878,17 +3878,17 @@ __metadata: languageName: node linkType: hard -"@mswjs/interceptors@npm:^0.29.0": - version: 0.29.1 - resolution: "@mswjs/interceptors@npm:0.29.1" +"@mswjs/interceptors@npm:^0.35.6": + version: 0.35.6 + resolution: "@mswjs/interceptors@npm:0.35.6" dependencies: "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/logger": "npm:^0.3.0" "@open-draft/until": "npm:^2.0.0" is-node-process: "npm:^1.2.0" - outvariant: "npm:^1.2.1" + outvariant: "npm:^1.4.3" strict-event-emitter: "npm:^0.5.1" - checksum: 10c0/816660a17b0e89e6e6955072b96882b5807c8c9faa316eab27104e8ba80e8e7d78b1862af42e1044156a5ae3ae2071289dc9211ecdc8fd5f7078d8c8a8a7caa3 + checksum: 10c0/9472f640183675869368bf2ccf32354db0dfb320c754bcbfc683059f5380674598c59dde4fa58007f74817e31aa1dbd123787fcd0b1d37d53595aa718d06bfbe languageName: node linkType: hard @@ -6234,16 +6234,24 @@ __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" + "@vitest/browser": "npm:^2.1.1" + "@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" + 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" - vitest: "npm:^2.0.0" + vitest: "npm:^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 languageName: unknown @@ -6879,9 +6887,9 @@ __metadata: "@typescript-eslint/parser": "npm:^6.18.1" "@vitejs/plugin-react": "npm:^3.0.1" "@vitejs/plugin-vue": "npm:^4.4.0" - "@vitest/browser": "npm:^2.0.5" - "@vitest/coverage-istanbul": "npm:^2.0.5" - "@vitest/coverage-v8": "npm:^2.0.5" + "@vitest/browser": "npm:^2.1.1" + "@vitest/coverage-istanbul": "npm:^2.1.1" + "@vitest/coverage-v8": "npm:^2.1.1" create-storybook: "workspace:*" cross-env: "npm:^7.0.3" danger: "npm:^12.3.3" @@ -6921,7 +6929,7 @@ __metadata: util: "npm:^0.12.4" vite: "npm:^4.0.0" vite-plugin-inspect: "npm:^0.8.5" - vitest: "npm:^2.0.5" + vitest: "npm:^2.1.1" wait-on: "npm:^7.0.1" dependenciesMeta: ejs: @@ -8765,20 +8773,22 @@ __metadata: languageName: node linkType: hard -"@vitest/browser@npm:^2.0.0, @vitest/browser@npm:^2.0.5": - version: 2.0.5 - resolution: "@vitest/browser@npm:2.0.5" +"@vitest/browser@npm:^2.1.1": + version: 2.1.1 + resolution: "@vitest/browser@npm:2.1.1" dependencies: "@testing-library/dom": "npm:^10.4.0" "@testing-library/user-event": "npm:^14.5.2" - "@vitest/utils": "npm:2.0.5" - magic-string: "npm:^0.30.10" - msw: "npm:^2.3.2" + "@vitest/mocker": "npm:2.1.1" + "@vitest/utils": "npm:2.1.1" + magic-string: "npm:^0.30.11" + msw: "npm:^2.3.5" sirv: "npm:^2.0.4" + tinyrainbow: "npm:^1.2.0" ws: "npm:^8.18.0" peerDependencies: playwright: "*" - vitest: 2.0.5 + vitest: 2.1.1 webdriverio: "*" peerDependenciesMeta: playwright: @@ -8787,16 +8797,16 @@ __metadata: optional: true webdriverio: optional: true - checksum: 10c0/a2b0e6ddb16f679c72c79c8093164a27683f7658a9a05882f0d00f90923071f16e1176211d1851539d49d5011199da5f926410c251e4b4f06828355d2ca30c92 + checksum: 10c0/f4eabe5fa8c7919ca33b334b42906202a25d2cc8d1fef35c5b6274c87997e940cf069e73ad8f7bf5044f03e308379dc05548ceeec2aec324b4b66c8f90ae11f9 languageName: node linkType: hard -"@vitest/coverage-istanbul@npm:^2.0.5": - version: 2.0.5 - resolution: "@vitest/coverage-istanbul@npm:2.0.5" +"@vitest/coverage-istanbul@npm:^2.1.1": + version: 2.1.1 + resolution: "@vitest/coverage-istanbul@npm:2.1.1" dependencies: "@istanbuljs/schema": "npm:^0.1.3" - debug: "npm:^4.3.5" + debug: "npm:^4.3.6" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-instrument: "npm:^6.0.3" istanbul-lib-report: "npm:^3.0.1" @@ -8806,30 +8816,34 @@ __metadata: test-exclude: "npm:^7.0.1" tinyrainbow: "npm:^1.2.0" peerDependencies: - vitest: 2.0.5 - checksum: 10c0/f19744e848f06f2ce3a6364caa3ffe701d571ff89c8de31ad753c2d48d46e24eab8d8670548997839c77ec41ebe69011b92df74ef196c070964fde9eaef1b1eb + vitest: 2.1.1 + checksum: 10c0/4dd4109294c2cc51306cb1cbabf22e0815664b07f3d668df3ebb0968fbf564213e85eca2e369c8dca838baf3e5ed4fbb243e7a98f20899a8040779fa7e6ee381 languageName: node linkType: hard -"@vitest/coverage-v8@npm:^2.0.5": - version: 2.0.5 - resolution: "@vitest/coverage-v8@npm:2.0.5" +"@vitest/coverage-v8@npm:^2.1.1": + version: 2.1.1 + resolution: "@vitest/coverage-v8@npm:2.1.1" dependencies: "@ampproject/remapping": "npm:^2.3.0" "@bcoe/v8-coverage": "npm:^0.2.3" - debug: "npm:^4.3.5" + debug: "npm:^4.3.6" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" istanbul-lib-source-maps: "npm:^5.0.6" istanbul-reports: "npm:^3.1.7" - magic-string: "npm:^0.30.10" + magic-string: "npm:^0.30.11" magicast: "npm:^0.3.4" std-env: "npm:^3.7.0" test-exclude: "npm:^7.0.1" tinyrainbow: "npm:^1.2.0" peerDependencies: - vitest: 2.0.5 - checksum: 10c0/a95eef744d2a541f5d9d0287243cbcb596802c04e0250404947e36a669c477abe86607afb8d8ddb3d31bf12633b3ffa3d9a313e489e4ab7998b3c1620ad60e00 + "@vitest/browser": 2.1.1 + vitest: 2.1.1 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10c0/3deba40edfae79ac4545cadb0786ecf6c8deb72cdfd1ba0f205d84804d241740a7e78892782a3002f87bb5c0a2705ab613fe5f54374f2fe9866cd0a574d65156 languageName: node linkType: hard @@ -8845,6 +8859,18 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:2.1.1": + version: 2.1.1 + resolution: "@vitest/expect@npm:2.1.1" + dependencies: + "@vitest/spy": "npm:2.1.1" + "@vitest/utils": "npm:2.1.1" + chai: "npm:^5.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/2a467bcd37378b653040cca062a665f382087eb9f69cff670848a0c207a8458f27211c408c75b7e563e069a2e6d533c78f24e1a317c259646b948813342dbf3d + 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" @@ -8857,7 +8883,27 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:2.0.5, @vitest/pretty-format@npm:^2.0.5": +"@vitest/mocker@npm:2.1.1": + version: 2.1.1 + resolution: "@vitest/mocker@npm:2.1.1" + dependencies: + "@vitest/spy": "npm:^2.1.0-beta.1" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.11" + peerDependencies: + "@vitest/spy": 2.1.1 + msw: ^2.3.5 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/e0681bb75bf7255ce49f720d193c9c795a64d42fef13c7af5c157514ebce88a5b89dbf702aa0929d4cefaed3db73351bd3ade3ccabecc09a23a872d9c55be50d + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:2.0.5": version: 2.0.5 resolution: "@vitest/pretty-format@npm:2.0.5" dependencies: @@ -8866,24 +8912,33 @@ __metadata: languageName: node linkType: hard -"@vitest/runner@npm:2.0.5": - version: 2.0.5 - resolution: "@vitest/runner@npm:2.0.5" +"@vitest/pretty-format@npm:2.1.1, @vitest/pretty-format@npm:^2.1.1": + version: 2.1.1 + resolution: "@vitest/pretty-format@npm:2.1.1" dependencies: - "@vitest/utils": "npm:2.0.5" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/21057465a794a037a7af2c48397531eadf9b2d8a7b4d1ee5af9081cf64216cd0039b9e06317319df79aa2240fed1dbb6767b530deae2bd4b42d6fb974297e97d + languageName: node + linkType: hard + +"@vitest/runner@npm:2.1.1, @vitest/runner@npm:^2.1.1": + version: 2.1.1 + resolution: "@vitest/runner@npm:2.1.1" + dependencies: + "@vitest/utils": "npm:2.1.1" pathe: "npm:^1.1.2" - checksum: 10c0/d0ed3302a7e015bf44b7c0df9d8f7da163659e082d86f9406944b5a31a61ab9ddc1de530e06176d1f4ef0bde994b44bff4c7dab62aacdc235c8fc04b98e4a72a + checksum: 10c0/a6d1424d6224d8a60ed0bbf7cdacb165ef36bc71cc957ad2c11ed1989fa5106636173369f0d8e1fa3f319a965091e52c8ce21203fce4bafe772632ccc2bd65a6 languageName: node linkType: hard -"@vitest/snapshot@npm:2.0.5": - version: 2.0.5 - resolution: "@vitest/snapshot@npm:2.0.5" +"@vitest/snapshot@npm:2.1.1": + version: 2.1.1 + resolution: "@vitest/snapshot@npm:2.1.1" dependencies: - "@vitest/pretty-format": "npm:2.0.5" - magic-string: "npm:^0.30.10" + "@vitest/pretty-format": "npm:2.1.1" + magic-string: "npm:^0.30.11" pathe: "npm:^1.1.2" - checksum: 10c0/7bf38474248f5ae0aac6afad511785d2b7a023ac5158803c2868fd172b5b9c1a569fb1dd64a09a49e43fd342cab71ea485ada89b7f08d37b1622a5a0ac00271d + checksum: 10c0/e9dadee87a2f489883dec0360b55b2776d2a07e460bf2430b34867cd4e9f34b09b3e219a23bc8c3e1359faefdd166072d3305b66a0bea475c7d616470b7d841c languageName: node linkType: hard @@ -8896,6 +8951,15 @@ __metadata: languageName: node linkType: hard +"@vitest/spy@npm:2.1.1, @vitest/spy@npm:^2.1.0-beta.1": + version: 2.1.1 + resolution: "@vitest/spy@npm:2.1.1" + dependencies: + tinyspy: "npm:^3.0.0" + checksum: 10c0/b251be1390c105b68aa95270159c4583c3e1a0f7a2e1f82db8b7fadedc3cb459c5ef9286033a1ae764810e00715552fc80afe4507cd8b0065934fb1a64926e06 + languageName: node + linkType: hard + "@vitest/utils@npm:2.0.5, @vitest/utils@npm:^2.0.5": version: 2.0.5 resolution: "@vitest/utils@npm:2.0.5" @@ -8908,6 +8972,17 @@ __metadata: languageName: node linkType: hard +"@vitest/utils@npm:2.1.1": + version: 2.1.1 + resolution: "@vitest/utils@npm:2.1.1" + dependencies: + "@vitest/pretty-format": "npm:2.1.1" + loupe: "npm:^3.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/b724c7f23591860bd24cd8e6d0cd803405f4fbff746db160a948290742144463287566a05ca400deb56817603b5185c4429707947869c3d453805860b5e3a3e5 + languageName: node + linkType: hard + "@volar/language-core@npm:1.10.1, @volar/language-core@npm:~1.10.0": version: 1.10.1 resolution: "@volar/language-core@npm:1.10.1" @@ -12845,6 +12920,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.3.6": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/1471db19c3b06d485a622d62f65947a19a23fbd0dd73f7fd3eafb697eec5360cde447fb075919987899b1a2096e85d35d4eb5a4de09a57600ac9cf7e6c8e768b + languageName: node + linkType: hard + "decamelize@npm:^1.2.0": version: 1.2.0 resolution: "decamelize@npm:1.2.0" @@ -19456,7 +19543,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.0, magic-string@npm:^0.30.10, magic-string@npm:^0.30.11, magic-string@npm:^0.30.4, magic-string@npm:^0.30.5": +"magic-string@npm:^0.30.0, magic-string@npm:^0.30.11, magic-string@npm:^0.30.4, magic-string@npm:^0.30.5": version: 0.30.11 resolution: "magic-string@npm:0.30.11" dependencies: @@ -21096,22 +21183,22 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 languageName: node linkType: hard -"msw@npm:^2.3.2": - version: 2.3.5 - resolution: "msw@npm:2.3.5" +"msw@npm:^2.3.5": + version: 2.4.8 + resolution: "msw@npm:2.4.8" dependencies: "@bundled-es-modules/cookie": "npm:^2.0.0" "@bundled-es-modules/statuses": "npm:^1.0.1" "@bundled-es-modules/tough-cookie": "npm:^0.1.6" "@inquirer/confirm": "npm:^3.0.0" - "@mswjs/interceptors": "npm:^0.29.0" + "@mswjs/interceptors": "npm:^0.35.6" "@open-draft/until": "npm:^2.1.0" "@types/cookie": "npm:^0.6.0" "@types/statuses": "npm:^2.0.4" @@ -21120,18 +21207,18 @@ __metadata: headers-polyfill: "npm:^4.0.2" is-node-process: "npm:^1.2.0" outvariant: "npm:^1.4.2" - path-to-regexp: "npm:^6.2.0" + path-to-regexp: "npm:^6.3.0" strict-event-emitter: "npm:^0.5.1" type-fest: "npm:^4.9.0" yargs: "npm:^17.7.2" peerDependencies: - typescript: ">= 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 @@ -22064,7 +22151,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 @@ -22566,10 +22653,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 @@ -27080,10 +27167,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 @@ -28501,18 +28595,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 @@ -28652,34 +28745,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: @@ -28697,7 +28790,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10c0/b4e6cca00816bf967a8589111ded72faa12f92f94ccdd0dcd0698ffcfdfc52ec662753f66b387549c600ac699b993fd952efbd99dc57fcf4d1c69a2f1022b259 + checksum: 10c0/77a67092338613376dadd8f6f6872383db8409402ce400ac1de48efd87a7214183e798484a3eb2310221c03554e37a00f9fdbc91e49194e7c68e009a5589f494 languageName: node linkType: hard 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 +}