diff --git a/CHANGELOG.md b/CHANGELOG.md index e836b81e7ad8..60adcc476fb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 8.4.6 + +- Addon Test: Use pathe for better windows support - [#29676](https://github.com/storybookjs/storybook/pull/29676), thanks @yannbf! +- Angular: Default to standalone components in Angular v19 - [#29677](https://github.com/storybookjs/storybook/pull/29677), thanks @ingowagner! +- Frameworks: Add Vite 6 support - [#29710](https://github.com/storybookjs/storybook/pull/29710), thanks @yannbf! +- Portable stories: Support multiple annotation notations from preview - [#29733](https://github.com/storybookjs/storybook/pull/29733), thanks @yannbf! +- React: Upgrade react-docgen-typescript to support Vite 6 - [#29724](https://github.com/storybookjs/storybook/pull/29724), thanks @yannbf! +- Svelte: Support `@sveltejs/vite-plugin-svelte` v5 - [#29731](https://github.com/storybookjs/storybook/pull/29731), thanks @JReinhold! + ## 8.4.5 - Angular: Support v19 - [#29659](https://github.com/storybookjs/storybook/pull/29659), thanks @leosvelperez! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index ea140d28b0a1..0505eaff38af 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,8 @@ +## 8.5.0-alpha.14 + +- RNW-Vite: Add built-in Flow support - [#29756](https://github.com/storybookjs/storybook/pull/29756), thanks @dannyhw! +- Test: Add coverage feature - [#29713](https://github.com/storybookjs/storybook/pull/29713), thanks @ndelangen! + ## 8.5.0-alpha.13 - Portable stories: Support multiple annotation notations from preview - [#29733](https://github.com/storybookjs/storybook/pull/29733), thanks @yannbf! diff --git a/code/addons/test/package.json b/code/addons/test/package.json index 06ea13ddacb0..8e4db505d183 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -48,6 +48,11 @@ "import": "./dist/vitest-plugin/test-utils.mjs", "require": "./dist/vitest-plugin/test-utils.js" }, + "./internal/coverage-reporter": { + "types": "./dist/node/coverage-reporter.d.ts", + "import": "./dist/node/coverage-reporter.mjs", + "require": "./dist/node/coverage-reporter.js" + }, "./preview": { "types": "./dist/preview.d.ts", "import": "./dist/preview.mjs", @@ -87,7 +92,7 @@ }, "devDependencies": { "@devtools-ds/object-inspector": "^1.1.2", - "@storybook/icons": "^1.2.12", + "@types/istanbul-lib-report": "^3.0.3", "@types/node": "^22.0.0", "@types/semver": "^7", "@vitest/browser": "^2.1.3", @@ -98,6 +103,7 @@ "execa": "^8.0.1", "find-up": "^7.0.0", "formik": "^2.2.9", + "istanbul-lib-report": "^3.0.1", "pathe": "^1.1.2", "picocolors": "^1.1.0", "react": "^18.2.0", @@ -146,7 +152,8 @@ "./src/vitest-plugin/index.ts", "./src/vitest-plugin/global-setup.ts", "./src/postinstall.ts", - "./src/node/vitest.ts" + "./src/node/vitest.ts", + "./src/node/coverage-reporter.ts" ] }, "storybook": { diff --git a/code/addons/test/src/components/TestProviderRender.stories.tsx b/code/addons/test/src/components/TestProviderRender.stories.tsx index 13a9e9bd3f35..fbdb3d488c7c 100644 --- a/code/addons/test/src/components/TestProviderRender.stories.tsx +++ b/code/addons/test/src/components/TestProviderRender.stories.tsx @@ -124,29 +124,83 @@ export const Running: Story = { }, }; -export const EnableA11y: Story = { +export const Watching: Story = { + args: { + state: { + ...config, + ...baseState, + watching: true, + }, + }, +}; + +export const WithCoverageNegative: Story = { args: { state: { ...config, ...baseState, details: { testResults: [], + coverageSummary: { + percentage: 20, + status: 'negative', + }, }, config: { - a11y: true, - coverage: false, + a11y: false, + coverage: true, }, }, }, }; -export const EnableEditing: Story = { +export const WithCoverageWarning: Story = { args: { state: { ...config, ...baseState, + details: { + testResults: [], + coverageSummary: { + percentage: 50, + status: 'warning', + }, + }, config: { - a11y: true, + a11y: false, + coverage: true, + }, + }, + }, +}; + +export const WithCoveragePositive: Story = { + args: { + state: { + ...config, + ...baseState, + details: { + testResults: [], + coverageSummary: { + percentage: 80, + status: 'positive', + }, + }, + config: { + a11y: false, + coverage: true, + }, + }, + }, +}; + +export const Editing: Story = { + args: { + state: { + ...config, + ...baseState, + config: { + a11y: false, coverage: false, }, details: { @@ -161,3 +215,21 @@ export const EnableEditing: Story = { screen.getByLabelText(/Open settings/).click(); }, }; + +export const EditingAndWatching: Story = { + args: { + state: { + ...config, + ...baseState, + watching: true, + config: { + a11y: true, + coverage: true, // should be automatically disabled in the UI + }, + details: { + testResults: [], + }, + }, + }, + play: Editing.play, +}; diff --git a/code/addons/test/src/components/TestProviderRender.tsx b/code/addons/test/src/components/TestProviderRender.tsx index 588523f89ad9..ca6be687c88d 100644 --- a/code/addons/test/src/components/TestProviderRender.tsx +++ b/code/addons/test/src/components/TestProviderRender.tsx @@ -77,6 +77,7 @@ export const TestProviderRender: FC<{ const title = state.crashed || state.failed ? 'Local tests failed' : 'Run local tests'; const errorMessage = state.error?.message; + const coverageSummary = state.details?.coverageSummary; const [config, updateConfig] = useConfig( api, @@ -159,8 +160,8 @@ export const TestProviderRender: FC<{ right={ updateConfig({ coverage: !config.coverage })} /> } @@ -185,11 +186,27 @@ export const TestProviderRender: FC<{ title="Component tests" icon={} /> - } - right={`60%`} - /> + {coverageSummary ? ( + + } + right={`${coverageSummary.percentage}%`} + /> + ) : ( + } + /> + )} } diff --git a/code/addons/test/src/constants.ts b/code/addons/test/src/constants.ts index 838594e212a3..0453930e3758 100644 --- a/code/addons/test/src/constants.ts +++ b/code/addons/test/src/constants.ts @@ -10,6 +10,8 @@ export const DOCUMENTATION_LINK = 'writing-tests/test-addon'; export const DOCUMENTATION_DISCREPANCY_LINK = `${DOCUMENTATION_LINK}#what-happens-when-there-are-different-test-results-in-multiple-environments`; export const DOCUMENTATION_FATAL_ERROR_LINK = `${DOCUMENTATION_LINK}#what-happens-if-vitest-itself-has-an-error`; +export const COVERAGE_DIRECTORY = 'coverage'; + export interface Config { coverage: boolean; a11y: boolean; @@ -17,4 +19,8 @@ export interface Config { export type Details = { testResults: TestResult[]; + coverageSummary?: { + status: 'positive' | 'warning' | 'negative' | 'unknown'; + percentage: number; + }; }; diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts index 0771604861d9..9cf4880ec35e 100644 --- a/code/addons/test/src/node/boot-test-runner.ts +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -3,6 +3,7 @@ import { type ChildProcess } from 'node:child_process'; import type { Channel } from 'storybook/internal/channels'; import { TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, + TESTING_MODULE_CONFIG_CHANGE, TESTING_MODULE_CRASH_REPORT, TESTING_MODULE_RUN_REQUEST, TESTING_MODULE_WATCH_MODE_REQUEST, @@ -43,11 +44,14 @@ const bootTestRunner = async (channel: Channel, initEvent?: string, initArgs?: a 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 forwardConfigChange = (...args: any[]) => + child?.send({ args, from: 'server', type: TESTING_MODULE_CONFIG_CHANGE }); const killChild = () => { channel.off(TESTING_MODULE_RUN_REQUEST, forwardRun); channel.off(TESTING_MODULE_WATCH_MODE_REQUEST, forwardWatchMode); channel.off(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, forwardCancel); + channel.off(TESTING_MODULE_CONFIG_CHANGE, forwardConfigChange); child?.kill(); child = null; }; @@ -86,6 +90,7 @@ const bootTestRunner = async (channel: Channel, initEvent?: string, initArgs?: a channel.on(TESTING_MODULE_RUN_REQUEST, forwardRun); channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, forwardWatchMode); channel.on(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, forwardCancel); + channel.on(TESTING_MODULE_CONFIG_CHANGE, forwardConfigChange); resolve(); } else if (result.type === 'error') { diff --git a/code/addons/test/src/node/coverage-reporter.ts b/code/addons/test/src/node/coverage-reporter.ts new file mode 100644 index 000000000000..452643cd9d60 --- /dev/null +++ b/code/addons/test/src/node/coverage-reporter.ts @@ -0,0 +1,53 @@ +import type { ResolvedCoverageOptions } from 'vitest/node'; + +import type { ReportNode, Visitor } from 'istanbul-lib-report'; +import { ReportBase } from 'istanbul-lib-report'; + +import { type Details, TEST_PROVIDER_ID } from '../constants'; +import type { TestManager } from './test-manager'; + +export type StorybookCoverageReporterOptions = { + testManager: TestManager; + coverageOptions: ResolvedCoverageOptions<'v8'>; +}; + +export default class StorybookCoverageReporter extends ReportBase implements Partial { + #testManager: StorybookCoverageReporterOptions['testManager']; + + #coverageOptions: StorybookCoverageReporterOptions['coverageOptions']; + + constructor(opts: StorybookCoverageReporterOptions) { + super(); + this.#testManager = opts.testManager; + this.#coverageOptions = opts.coverageOptions; + } + + onSummary(node: ReportNode) { + if (!node.isRoot()) { + return; + } + const coverageSummary = node.getCoverageSummary(false); + + const percentage = Math.round(coverageSummary.data.statements.pct); + + // Fallback to Vitest's default watermarks https://vitest.dev/config/#coverage-watermarks + const [lowWatermark = 50, highWatermark = 80] = + this.#coverageOptions.watermarks?.statements ?? []; + + const coverageDetails: Details['coverageSummary'] = { + percentage, + status: + percentage < lowWatermark + ? 'negative' + : percentage < highWatermark + ? 'warning' + : 'positive', + }; + this.#testManager.sendProgressReport({ + providerId: TEST_PROVIDER_ID, + details: { + coverageSummary: coverageDetails, + }, + }); + } +} diff --git a/code/addons/test/src/node/test-manager.ts b/code/addons/test/src/node/test-manager.ts index 624916772056..3660081de58c 100644 --- a/code/addons/test/src/node/test-manager.ts +++ b/code/addons/test/src/node/test-manager.ts @@ -12,7 +12,7 @@ import { type TestingModuleWatchModeRequestPayload, } from 'storybook/internal/core-events'; -import { TEST_PROVIDER_ID } from '../constants'; +import { type Config, TEST_PROVIDER_ID } from '../constants'; import { VitestManager } from './vitest-manager'; export class TestManager { @@ -20,6 +20,8 @@ export class TestManager { watchMode = false; + coverage = false; + constructor( private channel: Channel, private options: { @@ -27,7 +29,7 @@ export class TestManager { onReady?: () => void; } = {} ) { - this.vitestManager = new VitestManager(channel, this); + this.vitestManager = new VitestManager(this); this.channel.on(TESTING_MODULE_RUN_REQUEST, this.handleRunRequest.bind(this)); this.channel.on(TESTING_MODULE_CONFIG_CHANGE, this.handleConfigChange.bind(this)); @@ -37,21 +39,29 @@ export class TestManager { this.vitestManager.startVitest().then(() => options.onReady?.()); } - async restartVitest(watchMode = false) { + async restartVitest({ watchMode, coverage }: { watchMode: boolean; coverage: boolean }) { await this.vitestManager.vitest?.runningPromise; await this.vitestManager.closeVitest(); - await this.vitestManager.startVitest(watchMode); + await this.vitestManager.startVitest({ watchMode, coverage }); } - async handleConfigChange(payload: TestingModuleConfigChangePayload) { - // TODO do something with the config - const config = payload.config; + async handleConfigChange( + payload: TestingModuleConfigChangePayload<{ coverage: boolean; a11y: boolean }> + ) { + if (payload.providerId !== TEST_PROVIDER_ID) { + return; + } + if (this.coverage !== payload.config.coverage) { + try { + this.coverage = payload.config.coverage; + await this.restartVitest({ watchMode: this.watchMode, coverage: this.coverage }); + } catch (e) { + this.reportFatalError('Failed to change coverage mode', e); + } + } } async handleWatchModeRequest(payload: TestingModuleWatchModeRequestPayload) { - // TODO do something with the config - const config = payload.config; - try { if (payload.providerId !== TEST_PROVIDER_ID) { return; @@ -59,20 +69,45 @@ export class TestManager { if (this.watchMode !== payload.watchMode) { this.watchMode = payload.watchMode; - await this.restartVitest(this.watchMode); + await this.restartVitest({ watchMode: this.watchMode, coverage: false }); } } catch (e) { this.reportFatalError('Failed to change watch mode', e); } } - async handleRunRequest(payload: TestingModuleRunRequestPayload) { + async handleRunRequest(payload: TestingModuleRunRequestPayload) { try { if (payload.providerId !== TEST_PROVIDER_ID) { return; } + if (payload.config && this.coverage !== payload.config.coverage) { + this.coverage = payload.config.coverage; + } + + const allTestsRun = (payload.storyIds ?? []).length === 0; + if (this.coverage) { + /* + If we have coverage enabled and we're running all stories, + we have to restart Vitest AND disable watch mode otherwise the coverage report will be incorrect, + Vitest behaves wonky when re-using the same Vitest instance but with watch mode disabled, + among other things it causes the coverage report to be incorrect and stale. + + If we're only running a subset of stories, we have to temporarily disable coverage, + as a coverage report for a subset of stories is not useful. + */ + await this.restartVitest({ + watchMode: allTestsRun ? false : this.watchMode, + coverage: allTestsRun, + }); + } await this.vitestManager.runTests(payload); + + if (this.coverage && !allTestsRun) { + // Re-enable coverage if it was temporarily disabled because of a subset of stories was run + await this.restartVitest({ watchMode: this.watchMode, coverage: this.coverage }); + } } catch (e) { this.reportFatalError('Failed to run tests', e); } diff --git a/code/addons/test/src/node/vitest-manager.ts b/code/addons/test/src/node/vitest-manager.ts index b14da16ecce7..37e6e0588aa1 100644 --- a/code/addons/test/src/node/vitest-manager.ts +++ b/code/addons/test/src/node/vitest-manager.ts @@ -1,8 +1,15 @@ import { existsSync } from 'node:fs'; -import type { TestProject, TestSpecification, Vitest, WorkspaceProject } from 'vitest/node'; - -import type { Channel } from 'storybook/internal/channels'; +import type { + CoverageOptions, + ResolvedCoverageOptions, + TestProject, + TestSpecification, + Vitest, + WorkspaceProject, +} from 'vitest/node'; + +import { resolvePathInStorybookCache } from 'storybook/internal/common'; import type { TestingModuleRunRequestPayload } from 'storybook/internal/core-events'; import type { DocsIndexEntry, StoryIndex, StoryIndexEntry } from '@storybook/types'; @@ -10,7 +17,9 @@ import type { DocsIndexEntry, StoryIndex, StoryIndexEntry } from '@storybook/typ import path, { normalize } from 'pathe'; import slash from 'slash'; +import { COVERAGE_DIRECTORY, type Config } from '../constants'; import { log } from '../logger'; +import type { StorybookCoverageReporterOptions } from './coverage-reporter'; import { StorybookReporter } from './reporter'; import type { TestManager } from './test-manager'; @@ -27,14 +36,31 @@ export class VitestManager { storyCountForCurrentRun: number = 0; - constructor( - private channel: Channel, - private testManager: TestManager - ) {} + constructor(private testManager: TestManager) {} - async startVitest(watchMode = false) { + async startVitest({ watchMode = false, coverage = false } = {}) { const { createVitest } = await import('vitest/node'); + const storybookCoverageReporter: [string, StorybookCoverageReporterOptions] = [ + '@storybook/experimental-addon-test/internal/coverage-reporter', + { + testManager: this.testManager, + coverageOptions: this.vitest?.config?.coverage as ResolvedCoverageOptions<'v8'>, + }, + ]; + const coverageOptions = ( + coverage + ? { + enabled: true, + clean: false, + cleanOnRerun: !watchMode, + reportOnFailure: true, + reporter: [['html', {}], storybookCoverageReporter], + reportsDirectory: resolvePathInStorybookCache(COVERAGE_DIRECTORY), + } + : { enabled: false } + ) as CoverageOptions; + this.vitest = await createVitest('test', { watch: watchMode, passWithNoTests: false, @@ -45,15 +71,12 @@ export class VitestManager { // 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)], - // @ts-expect-error we just want to disable coverage, not specify a provider - coverage: { - enabled: false, - }, + coverage: coverageOptions, }); if (this.vitest) { this.vitest.onCancel(() => { - // TODO: handle cancelation + // TODO: handle cancellation }); } @@ -110,10 +133,11 @@ export class VitestManager { return true; } - async runTests(requestPayload: TestingModuleRunRequestPayload) { + async runTests(requestPayload: TestingModuleRunRequestPayload) { if (!this.vitest) { await this.startVitest(); } + this.resetTestNamePattern(); const stories = await this.fetchStories(requestPayload.indexUrl, requestPayload.storyIds); @@ -242,7 +266,7 @@ export class VitestManager { if (triggerAffectedTests.length) { await this.vitest.cancelCurrentRun('keyboard-input'); await this.vitest.runningPromise; - await this.vitest.runFiles(triggerAffectedTests, true); + await this.vitest.runFiles(triggerAffectedTests, false); } } diff --git a/code/addons/test/src/preset.ts b/code/addons/test/src/preset.ts index a1a6e1233faf..30b0e9da7b3d 100644 --- a/code/addons/test/src/preset.ts +++ b/code/addons/test/src/preset.ts @@ -1,19 +1,26 @@ import { readFileSync } from 'node:fs'; +import { mkdir } from 'node:fs/promises'; import type { Channel } from 'storybook/internal/channels'; -import { checkAddonOrder, getFrameworkName, serverRequire } from 'storybook/internal/common'; import { + checkAddonOrder, + getFrameworkName, + resolvePathInStorybookCache, + serverRequire, +} from 'storybook/internal/common'; +import { + TESTING_MODULE_CONFIG_CHANGE, TESTING_MODULE_RUN_REQUEST, TESTING_MODULE_WATCH_MODE_REQUEST, } from 'storybook/internal/core-events'; import { oneWayHash, telemetry } from 'storybook/internal/telemetry'; -import type { Options, PresetProperty, StoryId } from 'storybook/internal/types'; +import type { Options, PresetProperty, PresetPropertyFn, StoryId } from 'storybook/internal/types'; import { isAbsolute, join } from 'pathe'; import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; -import { STORYBOOK_ADDON_TEST_CHANNEL } from './constants'; +import { COVERAGE_DIRECTORY, STORYBOOK_ADDON_TEST_CHANNEL, TEST_PROVIDER_ID } from './constants'; import { log } from './logger'; import { runTestRunner } from './node/boot-test-runner'; @@ -64,7 +71,9 @@ export const experimental_serverChannel = async (channel: Channel, options: Opti const execute = (eventName: string) => (...args: any[]) => { - runTestRunner(channel, eventName, args); + if (args[0]?.providerId === TEST_PROVIDER_ID) { + runTestRunner(channel, eventName, args); + } }; channel.on(TESTING_MODULE_RUN_REQUEST, execute(TESTING_MODULE_RUN_REQUEST)); @@ -73,6 +82,7 @@ export const experimental_serverChannel = async (channel: Channel, options: Opti execute(TESTING_MODULE_WATCH_MODE_REQUEST)(payload); } }); + channel.on(TESTING_MODULE_CONFIG_CHANGE, execute(TESTING_MODULE_CONFIG_CHANGE)); if (!core.disableTelemetry) { const packageJsonPath = require.resolve('@storybook/experimental-addon-test/package.json'); @@ -126,3 +136,19 @@ export const managerEntries: PresetProperty<'managerEntries'> = async (entry = [ // for whatever reason seems like the return type of managerEntries is not correct (it expects never instead of string[]) return entry as never; }; + +export const staticDirs: PresetPropertyFn<'staticDirs'> = async (values = [], options) => { + if (options.configType === 'PRODUCTION') { + return values; + } + + const coverageDirectory = resolvePathInStorybookCache(COVERAGE_DIRECTORY); + await mkdir(coverageDirectory, { recursive: true }); + return [ + { + from: coverageDirectory, + to: '/coverage', + }, + ...values, + ]; +}; diff --git a/code/core/src/core-events/data/testing-module.ts b/code/core/src/core-events/data/testing-module.ts index 80edea66aa64..ad843450723b 100644 --- a/code/core/src/core-events/data/testing-module.ts +++ b/code/core/src/core-events/data/testing-module.ts @@ -11,12 +11,14 @@ export type TestProviderState< export type TestProviders = Record; -export type TestingModuleRunRequestPayload = { +export type TestingModuleRunRequestPayload< + Config extends { [key: string]: any } = NonNullable, +> = { providerId: TestProviderId; // TODO: Avoid needing to do a fetch request server-side to retrieve the index indexUrl: string; // e.g. http://localhost:6006/index.json storyIds?: string[]; // ['button--primary', 'button--secondary'] - config?: TestProviderState['config']; + config?: Config; }; export type TestingModuleProgressReportPayload = @@ -37,6 +39,10 @@ export type TestingModuleProgressReportPayload = message: string; stack?: string; }; + } + | { + providerId: TestProviderId; + details: { [key: string]: any }; }; export type TestingModuleCrashReportPayload = { @@ -71,13 +77,17 @@ export type TestingModuleCancelTestRunResponsePayload = message: string; }; -export type TestingModuleWatchModeRequestPayload = { +export type TestingModuleWatchModeRequestPayload< + Config extends { [key: string]: any } = NonNullable, +> = { providerId: TestProviderId; watchMode: boolean; - config?: TestProviderState['config']; + config?: Config; }; -export type TestingModuleConfigChangePayload = { +export type TestingModuleConfigChangePayload< + Config extends { [key: string]: any } = NonNullable, +> = { providerId: TestProviderId; - config: TestProviderState['config']; + config: Config; }; diff --git a/code/core/src/core-server/presets/common-preset.ts b/code/core/src/core-server/presets/common-preset.ts index 7bd893d0daf1..fe5f7db4f53a 100644 --- a/code/core/src/core-server/presets/common-preset.ts +++ b/code/core/src/core-server/presets/common-preset.ts @@ -303,26 +303,27 @@ export const experimental_serverChannel = async ( channel.on( TESTING_MODULE_PROGRESS_REPORT, async (payload: TestingModuleProgressReportPayload) => { - if ( - (payload.status === 'success' || payload.status === 'cancelled') && - payload.progress?.finishedAt - ) { + const status = 'status' in payload ? payload.status : undefined; + const progress = 'progress' in payload ? payload.progress : undefined; + const error = 'error' in payload ? payload.error : undefined; + + if ((status === 'success' || status === 'cancelled') && progress?.finishedAt) { await telemetry('testing-module-completed-report', { provider: payload.providerId, - duration: payload.progress.finishedAt - payload.progress.startedAt, - numTotalTests: payload.progress.numTotalTests, - numFailedTests: payload.progress.numFailedTests, - numPassedTests: payload.progress.numPassedTests, - status: payload.status, + duration: progress?.finishedAt - progress?.startedAt, + numTotalTests: progress?.numTotalTests, + numFailedTests: progress?.numFailedTests, + numPassedTests: progress?.numPassedTests, + status, }); } - if (payload.status === 'failed') { + if (status === 'failed') { await telemetry('testing-module-completed-report', { provider: payload.providerId, status: 'failed', ...(options.enableCrashReports && { - error: sanitizeError(payload.error), + error: error && sanitizeError(error), }), }); } diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts index 289ffe51c81d..6b33f04c2e4f 100644 --- a/code/core/src/manager-api/modules/experimental_testmodule.ts +++ b/code/core/src/manager-api/modules/experimental_testmodule.ts @@ -57,7 +57,19 @@ export const init: ModuleFn = ({ store, fullAPI }) => { updateTestProviderState(id, update) { return store.setState( ({ testProviders }) => { - return { testProviders: { ...testProviders, [id]: { ...testProviders[id], ...update } } }; + return { + testProviders: { + ...testProviders, + [id]: { + ...testProviders[id], + ...update, + details: { + ...(testProviders[id].details || {}), + ...(update.details || {}), + }, + }, + }, + }; }, { persistence: 'session' } ); diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index ba64b4e500a5..e39f7595188d 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -130,10 +130,12 @@ export const SidebarBottomBase = ({ }; const onProgressReport = ({ providerId, ...result }: TestingModuleProgressReportPayload) => { - if (result.status === 'failed') { + const statusResult = 'status' in result ? result.status : undefined; + + if (statusResult === 'failed') { api.updateTestProviderState(providerId, { ...result, running: false, failed: true }); } else { - const update = { ...result, running: result.status === 'pending' }; + const update = { ...result, running: statusResult === 'pending' }; api.updateTestProviderState(providerId, update); const { mapStatusUpdate, ...state } = testProviders[providerId]; diff --git a/code/frameworks/react-native-web-vite/package.json b/code/frameworks/react-native-web-vite/package.json index 0aba8948e71c..91e9238a97e7 100644 --- a/code/frameworks/react-native-web-vite/package.json +++ b/code/frameworks/react-native-web-vite/package.json @@ -53,6 +53,7 @@ "prep": "jiti ../../../scripts/prepare/bundle.ts" }, "dependencies": { + "@bunchtogether/vite-plugin-flow": "^1.0.2", "@joshwooding/vite-plugin-react-docgen-typescript": "0.4.2", "@storybook/builder-vite": "workspace:*", "@storybook/react": "workspace:*", diff --git a/code/frameworks/react-native-web-vite/src/preset.ts b/code/frameworks/react-native-web-vite/src/preset.ts index 852d69af2ce3..8e3d7d6b58a8 100644 --- a/code/frameworks/react-native-web-vite/src/preset.ts +++ b/code/frameworks/react-native-web-vite/src/preset.ts @@ -1,8 +1,9 @@ // @ts-expect-error FIXME import { viteFinal as reactViteFinal } from '@storybook/react-vite/preset'; +import { esbuildFlowPlugin, flowPlugin } from '@bunchtogether/vite-plugin-flow'; import react from '@vitejs/plugin-react'; -import type { PluginOption } from 'vite'; +import type { InlineConfig, PluginOption } from 'vite'; import type { FrameworkOptions, StorybookConfig } from './types'; @@ -61,13 +62,19 @@ export function reactNativeWeb(): PluginOption { } export const viteFinal: StorybookConfig['viteFinal'] = async (config, options) => { + const { mergeConfig } = await import('vite'); + const { pluginReactOptions = {} } = await options.presets.apply('frameworkOptions'); const reactConfig = await reactViteFinal(config, options); + const { plugins = [] } = reactConfig; plugins.unshift( + flowPlugin({ + exclude: [], + }), react({ babel: { babelrc: false, @@ -77,9 +84,16 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config, options) = ...pluginReactOptions, }) ); + plugins.push(reactNativeWeb()); - return reactConfig; + return mergeConfig(reactConfig, { + optimizeDeps: { + esbuildOptions: { + plugins: [esbuildFlowPlugin(new RegExp(/\.(flow|jsx?)$/), (_path: string) => 'jsx')], + }, + }, + } satisfies InlineConfig); }; export const core = { diff --git a/code/frameworks/react-native-web-vite/src/typings.d.ts b/code/frameworks/react-native-web-vite/src/typings.d.ts new file mode 100644 index 000000000000..8aed0ac76f1a --- /dev/null +++ b/code/frameworks/react-native-web-vite/src/typings.d.ts @@ -0,0 +1 @@ +declare module '@bunchtogether/vite-plugin-flow'; diff --git a/code/package.json b/code/package.json index 24b54cd51688..389b0893dea6 100644 --- a/code/package.json +++ b/code/package.json @@ -293,5 +293,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.5.0-alpha.14" } diff --git a/code/yarn.lock b/code/yarn.lock index 6623d2309c32..42fd23469029 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -2501,6 +2501,16 @@ __metadata: languageName: node linkType: hard +"@bunchtogether/vite-plugin-flow@npm:^1.0.2": + version: 1.0.2 + resolution: "@bunchtogether/vite-plugin-flow@npm:1.0.2" + dependencies: + flow-remove-types: "npm:^2.158.0" + rollup-pluginutils: "npm:^2.8.2" + checksum: 10c0/84faf014977196470bbeae686b4e167de2805777389f8a0da88647484df7cf39db3da91907d75ea810ea77175c0cdd40a9a3ad92b7c7c44681b0cd1f4156c7b8 + languageName: node + linkType: hard + "@bundled-es-modules/cookie@npm:^2.0.0": version: 2.0.0 resolution: "@bundled-es-modules/cookie@npm:2.0.0" @@ -6600,6 +6610,7 @@ __metadata: "@storybook/instrumenter": "workspace:*" "@storybook/test": "workspace:*" "@storybook/theming": "workspace:*" + "@types/istanbul-lib-report": "npm:^3.0.3" "@types/node": "npm:^22.0.0" "@types/semver": "npm:^7" "@vitest/browser": "npm:^2.1.3" @@ -6610,6 +6621,7 @@ __metadata: execa: "npm:^8.0.1" find-up: "npm:^7.0.0" formik: "npm:^2.2.9" + istanbul-lib-report: "npm:^3.0.1" pathe: "npm:^1.1.2" picocolors: "npm:^1.1.0" polished: "npm:^4.2.2" @@ -7097,6 +7109,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/react-native-web-vite@workspace:frameworks/react-native-web-vite" dependencies: + "@bunchtogether/vite-plugin-flow": "npm:^1.0.2" "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.4.2" "@storybook/builder-vite": "workspace:*" "@storybook/react": "workspace:*" @@ -8358,6 +8371,13 @@ __metadata: languageName: node linkType: hard +"@types/istanbul-lib-coverage@npm:*": + version: 2.0.6 + resolution: "@types/istanbul-lib-coverage@npm:2.0.6" + checksum: 10c0/3948088654f3eeb45363f1db158354fb013b362dba2a5c2c18c559484d5eb9f6fd85b23d66c0a7c2fcfab7308d0a585b14dadaca6cc8bf89ebfdc7f8f5102fb7 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.4 resolution: "@types/istanbul-lib-coverage@npm:2.0.4" @@ -8365,6 +8385,15 @@ __metadata: languageName: node linkType: hard +"@types/istanbul-lib-report@npm:^3.0.3": + version: 3.0.3 + resolution: "@types/istanbul-lib-report@npm:3.0.3" + dependencies: + "@types/istanbul-lib-coverage": "npm:*" + checksum: 10c0/247e477bbc1a77248f3c6de5dadaae85ff86ac2d76c5fc6ab1776f54512a745ff2a5f791d22b942e3990ddbd40f3ef5289317c4fca5741bedfaa4f01df89051c + languageName: node + linkType: hard + "@types/js-yaml@npm:^4.0.5": version: 4.0.9 resolution: "@types/js-yaml@npm:4.0.9" @@ -15299,6 +15328,13 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^0.6.1": + version: 0.6.1 + resolution: "estree-walker@npm:0.6.1" + checksum: 10c0/6dabc855faa04a1ffb17b6a9121b6008ba75ab5a163ad9dc3d7fca05cfda374c5f5e91418d783496620ca75e99a73c40874d8b75f23b4117508cc8bde78e7b41 + languageName: node + linkType: hard + "estree-walker@npm:^2.0.1, estree-walker@npm:^2.0.2": version: 2.0.2 resolution: "estree-walker@npm:2.0.2" @@ -16010,6 +16046,20 @@ __metadata: languageName: node linkType: hard +"flow-remove-types@npm:^2.158.0": + version: 2.255.0 + resolution: "flow-remove-types@npm:2.255.0" + dependencies: + hermes-parser: "npm:0.25.1" + pirates: "npm:^3.0.2" + vlq: "npm:^0.2.1" + bin: + flow-node: flow-node + flow-remove-types: flow-remove-types + checksum: 10c0/080cfab76259e313ac77ebd911528fdc9423446d0c4503b95c9f0beb57fde1143657ac3388110382ddbfdb9b7e8ebc20fe903b14a7de374f53d44d6365e26ae1 + languageName: node + linkType: hard + "flush-promises@npm:^1.0.2": version: 1.0.2 resolution: "flush-promises@npm:1.0.2" @@ -17233,6 +17283,22 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.25.1": + version: 0.25.1 + resolution: "hermes-estree@npm:0.25.1" + checksum: 10c0/48be3b2fa37a0cbc77a112a89096fa212f25d06de92781b163d67853d210a8a5c3784fac23d7d48335058f7ed283115c87b4332c2a2abaaccc76d0ead1a282ac + languageName: node + linkType: hard + +"hermes-parser@npm:0.25.1": + version: 0.25.1 + resolution: "hermes-parser@npm:0.25.1" + dependencies: + hermes-estree: "npm:0.25.1" + checksum: 10c0/3abaa4c6f1bcc25273f267297a89a4904963ea29af19b8e4f6eabe04f1c2c7e9abd7bfc4730ddb1d58f2ea04b6fee74053d8bddb5656ec6ebf6c79cc8d14202c + languageName: node + linkType: hard + "highlight.js@npm:^10.4.1, highlight.js@npm:~10.7.0": version: 10.7.3 resolution: "highlight.js@npm:10.7.3" @@ -21879,6 +21945,13 @@ __metadata: languageName: node linkType: hard +"node-modules-regexp@npm:^1.0.0": + version: 1.0.0 + resolution: "node-modules-regexp@npm:1.0.0" + checksum: 10c0/d4a9b6425a18e9fadd38f21a7f7820b3bfda4663c7d3b9f80043e3f5f7e27a0a1e04f524077b00a15ae77148cd81319da5772900229d89541062f7e876b36763 + languageName: node + linkType: hard + "node-polyfill-webpack-plugin@npm:^2.0.1": version: 2.0.1 resolution: "node-polyfill-webpack-plugin@npm:2.0.1" @@ -23138,6 +23211,15 @@ __metadata: languageName: node linkType: hard +"pirates@npm:^3.0.2": + version: 3.0.2 + resolution: "pirates@npm:3.0.2" + dependencies: + node-modules-regexp: "npm:^1.0.0" + checksum: 10c0/f71519f64abff750ad00398e7a0f724e7d3af0ce14c0cf149a47dd9e1fae5e9aea24cb3a63b4ce8dce8b051f7d44531af6743078e33f72cb8602c5a7365185d1 + languageName: node + linkType: hard + "pirates@npm:^4.0.5": version: 4.0.6 resolution: "pirates@npm:4.0.6" @@ -25514,6 +25596,15 @@ __metadata: languageName: node linkType: hard +"rollup-pluginutils@npm:^2.8.2": + version: 2.8.2 + resolution: "rollup-pluginutils@npm:2.8.2" + dependencies: + estree-walker: "npm:^0.6.1" + checksum: 10c0/20947bec5a5dd68b5c5c8423911e6e7c0ad834c451f1a929b1f4e2bc08836ad3f1a722ef2bfcbeca921870a0a283f13f064a317dc7a6768496e98c9a641ba290 + languageName: node + linkType: hard + "rollup@npm:^3.27.1": version: 3.29.4 resolution: "rollup@npm:3.29.4" @@ -29203,6 +29294,13 @@ __metadata: languageName: node linkType: hard +"vlq@npm:^0.2.1": + version: 0.2.3 + resolution: "vlq@npm:0.2.3" + checksum: 10c0/d1557b404353ca75c7affaaf403d245a3273a7d1c6b3380ed7f04ae3f080e4658f41ac700d6f48acb3cd4875fe7bc7da4924b3572cd5584a5de83b35b1de5e12 + languageName: node + linkType: hard + "vm-browserify@npm:^1.1.2": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" diff --git a/docs/versions/next.json b/docs/versions/next.json index 27ef6e4d3c3f..36d35a6a292f 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.5.0-alpha.13","info":{"plain":"- Portable stories: Support multiple annotation notations from preview - [#29733](https://github.com/storybookjs/storybook/pull/29733), thanks @yannbf!\n- React: Upgrade react-docgen-typescript to support Vite 6 - [#29724](https://github.com/storybookjs/storybook/pull/29724), thanks @yannbf!"}} +{"version":"8.5.0-alpha.14","info":{"plain":"- RNW-Vite: Add built-in Flow support - [#29756](https://github.com/storybookjs/storybook/pull/29756), thanks @dannyhw!\n- Test: Add coverage feature - [#29713](https://github.com/storybookjs/storybook/pull/29713), thanks @ndelangen!"}} diff --git a/test-storybooks/portable-stories-kitchen-sink/nextjs/jest.config.js b/test-storybooks/portable-stories-kitchen-sink/nextjs/jest.config.js index 3867a50e7bc7..f427b4763dd6 100644 --- a/test-storybooks/portable-stories-kitchen-sink/nextjs/jest.config.js +++ b/test-storybooks/portable-stories-kitchen-sink/nextjs/jest.config.js @@ -9,7 +9,7 @@ const createJestConfig = nextJest({ /** @type {import('jest').Config} */ const customJestConfig = { coverageProvider: 'v8', - testEnvironment: 'jsdom', + testEnvironment: '@happy-dom/jest-environment', // Add more setup options before each test is run setupFilesAfterEnv: ['./jest.setup.ts'], moduleNameMapper: { diff --git a/test-storybooks/portable-stories-kitchen-sink/nextjs/package.json b/test-storybooks/portable-stories-kitchen-sink/nextjs/package.json index 9e9b8e0285f8..69722cabd5db 100644 --- a/test-storybooks/portable-stories-kitchen-sink/nextjs/package.json +++ b/test-storybooks/portable-stories-kitchen-sink/nextjs/package.json @@ -84,6 +84,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@happy-dom/jest-environment": "^15.11.7", "@jest/globals": "^29.7.0", "@storybook/addon-actions": "^8.0.0", "@storybook/addon-essentials": "^8.0.0", @@ -101,11 +102,10 @@ "eslint": "^8.56.0", "eslint-plugin-storybook": "^0.6.15", "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", "storybook": "^8.0.0", "typescript": "^5.2.2" }, "maintainer_please_read_this": { "_": "we use file protocol to make this setup close to real life scenarios as well as avoid issues with duplicated React instances. When you recompile the SB packages, you need to rerun install." } -} \ No newline at end of file +} diff --git a/test-storybooks/portable-stories-kitchen-sink/nextjs/stories/__snapshots__/portable-stories.test.tsx.snap b/test-storybooks/portable-stories-kitchen-sink/nextjs/stories/__snapshots__/portable-stories.test.tsx.snap index e65b31476609..677e011bb38c 100644 --- a/test-storybooks/portable-stories-kitchen-sink/nextjs/stories/__snapshots__/portable-stories.test.tsx.snap +++ b/test-storybooks/portable-stories-kitchen-sink/nextjs/stories/__snapshots__/portable-stories.test.tsx.snap @@ -158,16 +158,16 @@ exports[`renders imageLegacyStories stories renders BlurredAbsolutePlaceholder 1 Global Decorator