diff --git a/.circleci/config.yml b/.circleci/config.yml index d89b0e260576..550dc9e6b994 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -165,17 +165,15 @@ jobs: root: . paths: - code/node_modules + - code/addons - scripts/node_modules - code/bench - code/examples - - code/node_modules - - code/addons - code/frameworks - code/deprecated - code/lib - code/core - code/builders - - code/ui - code/renderers - code/presets - .verdaccio-cache @@ -269,16 +267,30 @@ jobs: steps: - git-shallow-clone/checkout_advanced: clone_options: "--depth 1 --verbose" - - attach_workspace: - at: . - nx/set-shas: main-branch-name: "next" workflow-name: << pipeline.parameters.workflow >> + - run: + name: install in scripts + command: | + cd scripts + yarn install + - run: + name: install in code + command: | + cd code + yarn install + - run: + name: Compile + command: | + yarn task --task compile --start-from=compile --no-link --debug - run: name: Check command: | - yarn task --task compile --start-from=auto --no-link --debug - yarn task --task check --start-from=auto --no-link --debug + yarn task --task check --start-from=check --no-link --debug + - run: + name: Ensure no changes pending + command: | git diff --exit-code - report-workflow-on-failure - cancel-workflow-on-failure @@ -807,9 +819,7 @@ workflows: - bench-packages: requires: - build - - check: - requires: - - build + - check - unit-tests: requires: - build @@ -885,9 +895,7 @@ workflows: - bench-packages: requires: - build - - check: - requires: - - build + - check - unit-tests: requires: - build @@ -964,9 +972,7 @@ workflows: - bench-packages: requires: - build - - check: - requires: - - build + - check - unit-tests: requires: - build diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index a1c1ca62d0a3..9e4b54c8bd39 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,11 @@ +## 8.5.0-alpha.11 + +- Core + Addon Test: Refactor test API and fix total test count - [#29656](https://github.com/storybookjs/storybook/pull/29656), thanks @ghengeveld! +- Core: Emit deprecated `TESTING_MODULE_RUN_ALL_REQUEST` for backward compatibility - [#29711](https://github.com/storybookjs/storybook/pull/29711), thanks @ghengeveld! +- Frameworks: Add Vite 6 support - [#29710](https://github.com/storybookjs/storybook/pull/29710), thanks @yannbf! +- TestAddon: Refactor UI & add config options - [#29662](https://github.com/storybookjs/storybook/pull/29662), thanks @ndelangen! +- Vue: Fix `vue-component-meta` docgen HMR not working - [#29518](https://github.com/storybookjs/storybook/pull/29518), thanks @IonianPlayboy! + ## 8.5.0-alpha.10 - Addon Test: Use pathe for better windows support - [#29676](https://github.com/storybookjs/storybook/pull/29676), thanks @yannbf! diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx index 5cc6b58297a1..8fa63f84819a 100644 --- a/code/addons/test/src/components/ContextMenuItem.tsx +++ b/code/addons/test/src/components/ContextMenuItem.tsx @@ -7,7 +7,7 @@ import React, { useState, } from 'react'; -import { Button, type ListItem } from 'storybook/internal/components'; +import { Button, ListItem } from 'storybook/internal/components'; import { useStorybookApi } from 'storybook/internal/manager-api'; import { useTheme } from 'storybook/internal/theming'; import { type API_HashEntry, type Addon_TestProviderState } from 'storybook/internal/types'; @@ -23,8 +23,7 @@ export const ContextMenuItem: FC<{ state: Addon_TestProviderState<{ testResults: TestResult[]; }>; - ListItem: typeof ListItem; -}> = ({ context, state, ListItem }) => { +}> = ({ context, state }) => { const api = useStorybookApi(); const [isDisabled, setDisabled] = useState(false); diff --git a/code/addons/test/src/components/Description.tsx b/code/addons/test/src/components/Description.tsx new file mode 100644 index 000000000000..aa7365a007e8 --- /dev/null +++ b/code/addons/test/src/components/Description.tsx @@ -0,0 +1,55 @@ +import React from 'react'; + +import { Link as LinkComponent } from 'storybook/internal/components'; +import { type TestProviderConfig, type TestProviderState } from 'storybook/internal/core-events'; +import { styled } from 'storybook/internal/theming'; + +import { RelativeTime } from './RelativeTime'; + +export const DescriptionStyle = styled.div(({ theme }) => ({ + fontSize: theme.typography.size.s1, + color: theme.barTextColor, +})); + +export function Description({ + errorMessage, + setIsModalOpen, + state, +}: { + state: TestProviderConfig & TestProviderState; + errorMessage: string; + setIsModalOpen: React.Dispatch>; +}) { + let description: string | React.ReactNode = 'Not run'; + + if (state.running) { + description = state.progress + ? `Testing... ${state.progress.numPassedTests}/${state.progress.numTotalTests}` + : 'Starting...'; + } else if (state.failed && !errorMessage) { + description = ''; + } else if (state.crashed || (state.failed && errorMessage)) { + description = ( + <> + { + setIsModalOpen(true); + }} + > + {state.error?.name || 'View full error'} + + + ); + } else if (state.progress?.finishedAt) { + description = ( + + ); + } else if (state.watching) { + description = 'Watching for file changes'; + } + return {description}; +} diff --git a/code/addons/test/src/components/RelativeTime.tsx b/code/addons/test/src/components/RelativeTime.tsx index d643960b06ed..fa9e7cf6d549 100644 --- a/code/addons/test/src/components/RelativeTime.tsx +++ b/code/addons/test/src/components/RelativeTime.tsx @@ -1,6 +1,23 @@ import { useEffect, useState } from 'react'; -import { getRelativeTimeString } from '../manager'; +export function getRelativeTimeString(date: Date): string { + const delta = Math.round((date.getTime() - Date.now()) / 1000); + const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]; + const units: Intl.RelativeTimeFormatUnit[] = [ + 'second', + 'minute', + 'hour', + 'day', + 'week', + 'month', + 'year', + ]; + + const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(delta)); + const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1; + const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); + return rtf.format(Math.floor(delta / divisor), units[unitIndex]); +} export const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: number }) => { const [relativeTimeString, setRelativeTimeString] = useState(null); diff --git a/code/addons/test/src/components/Subnav.tsx b/code/addons/test/src/components/Subnav.tsx index 59564dab2b67..bf9d8436cee0 100644 --- a/code/addons/test/src/components/Subnav.tsx +++ b/code/addons/test/src/components/Subnav.tsx @@ -41,7 +41,7 @@ const StyledSubnav = styled.nav(({ theme }) => ({ paddingLeft: 15, })); -export interface SubnavProps { +interface SubnavProps { controls: Controls; controlStates: ControlStates; status: Call['status']; @@ -64,7 +64,7 @@ const Note = styled(TooltipNote)(({ theme }) => ({ fontFamily: theme.typography.fonts.base, })); -export const StyledIconButton = styled(IconButton as any)(({ theme }) => ({ +const StyledIconButton = styled(IconButton)(({ theme }) => ({ color: theme.textMutedColor, margin: '0 3px', })); diff --git a/code/addons/test/src/components/TestProviderRender.stories.tsx b/code/addons/test/src/components/TestProviderRender.stories.tsx new file mode 100644 index 000000000000..4182152ecefe --- /dev/null +++ b/code/addons/test/src/components/TestProviderRender.stories.tsx @@ -0,0 +1,158 @@ +import React from 'react'; + +import type { TestProviderConfig, TestProviderState } from 'storybook/internal/core-events'; +import { ManagerContext } from 'storybook/internal/manager-api'; +import { styled } from 'storybook/internal/theming'; +import { Addon_TypesEnum } from 'storybook/internal/types'; + +import type { Meta, StoryObj } from '@storybook/react'; +import { fn, within } from '@storybook/test'; + +import type { Config, Details } from '../constants'; +import { TestProviderRender } from './TestProviderRender'; + +type Story = StoryObj; +const managerContext: any = { + state: { + testProviders: { + 'test-provider-id': { + id: 'test-provider-id', + name: 'Test Provider', + type: Addon_TypesEnum.experimental_TEST_PROVIDER, + }, + }, + }, + api: { + getDocsUrl: fn().mockName('api::getDocsUrl'), + emit: fn().mockName('api::emit'), + updateTestProviderState: fn().mockName('api::updateTestProviderState'), + }, +}; + +const config: TestProviderConfig = { + id: 'test-provider-id', + name: 'Test Provider', + type: Addon_TypesEnum.experimental_TEST_PROVIDER, + runnable: true, + watchable: true, +}; + +const baseState: TestProviderState = { + cancellable: true, + cancelling: false, + crashed: false, + error: null, + failed: false, + running: false, + watching: false, + config: { + a11y: false, + coverage: false, + }, + details: { + testResults: [ + { + endTime: 0, + startTime: 0, + status: 'passed', + message: 'All tests passed', + results: [ + { + storyId: 'story-id', + status: 'success', + duration: 100, + testRunId: 'test-run-id', + }, + ], + }, + ], + }, +}; + +const Content = styled.div({ + padding: '12px 6px', + display: 'flex', + flexDirection: 'column', + gap: '12px', +}); + +export default { + title: 'TestProviderRender', + component: TestProviderRender, + args: { + state: { + ...config, + ...baseState, + }, + api: managerContext.api, + }, + decorators: [ + (StoryFn) => ( + + + + ), + (StoryFn) => ( + + + + ), + ], +} as Meta; + +export const Default: Story = { + args: { + state: { + ...config, + ...baseState, + }, + }, +}; + +export const Running: Story = { + args: { + state: { + ...config, + ...baseState, + running: true, + }, + }, +}; + +export const EnableA11y: Story = { + args: { + state: { + ...config, + ...baseState, + details: { + testResults: [], + }, + config: { + a11y: true, + coverage: false, + }, + }, + }, +}; + +export const EnableEditing: Story = { + args: { + state: { + ...config, + ...baseState, + config: { + a11y: true, + coverage: false, + }, + details: { + testResults: [], + }, + }, + }, + + play: async ({ canvasElement }) => { + const screen = within(canvasElement); + + screen.getByLabelText('Edit').click(); + }, +}; diff --git a/code/addons/test/src/components/TestProviderRender.tsx b/code/addons/test/src/components/TestProviderRender.tsx new file mode 100644 index 000000000000..9e7534472774 --- /dev/null +++ b/code/addons/test/src/components/TestProviderRender.tsx @@ -0,0 +1,182 @@ +import React, { type FC, Fragment, useCallback, useRef, useState } from 'react'; + +import { Button } from 'storybook/internal/components'; +import { + TESTING_MODULE_CONFIG_CHANGE, + type TestProviderConfig, + type TestProviderState, + type TestingModuleConfigChangePayload, +} from 'storybook/internal/core-events'; +import type { API } from 'storybook/internal/manager-api'; +import { styled } from 'storybook/internal/theming'; + +import { EditIcon, EyeIcon, PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons'; + +import { type Config, type Details, TEST_PROVIDER_ID } from '../constants'; +import { Description } from './Description'; +import { GlobalErrorModal } from './GlobalErrorModal'; + +const Info = styled.div({ + display: 'flex', + flexDirection: 'column', + marginLeft: 6, +}); + +const Title = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({ + fontSize: theme.typography.size.s1, + fontWeight: crashed ? 'bold' : 'normal', + color: crashed ? theme.color.negativeText : theme.color.defaultText, +})); + +const Actions = styled.div({ + display: 'flex', + gap: 6, +}); + +const Head = styled.div({ + display: 'flex', + justifyContent: 'space-between', + gap: 6, +}); + +export const TestProviderRender: FC<{ + api: API; + state: TestProviderConfig & TestProviderState; +}> = ({ state, api }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const title = state.crashed || state.failed ? 'Component tests failed' : 'Component tests'; + const errorMessage = state.error?.message; + + const [config, changeConfig] = useConfig( + state.id, + state.config || { a11y: false, coverage: false }, + api + ); + + const [isEditing, setIsEditing] = useState(false); + + return ( + + + + + {title} + + + + + + + {state.watchable && ( + + )} + {state.runnable && ( + <> + {state.running && state.cancellable ? ( + + ) : ( + + )} + + )} + + + + {!isEditing ? ( + + {Object.entries(config).map(([key, value]) => ( +
+ {key}: {value ? 'ON' : 'OFF'} +
+ ))} +
+ ) : ( + + {Object.entries(config).map(([key, value]) => ( +
{ + changeConfig({ [key]: !value }); + }} + > + {key}: {value ? 'ON' : 'OFF'} +
+ ))} +
+ )} + + { + setIsModalOpen(false); + }} + onRerun={() => { + setIsModalOpen(false); + api.runTestProvider(TEST_PROVIDER_ID); + }} + /> +
+ ); +}; + +function useConfig(id: string, config: Config, api: API) { + const data = useRef(config); + data.current = config || { + a11y: false, + coverage: false, + }; + + const changeConfig = useCallback( + (update: Partial) => { + const newConfig = { + ...data.current, + ...update, + }; + api.updateTestProviderState(id, { + config: newConfig, + }); + api.emit(TESTING_MODULE_CONFIG_CHANGE, { + providerId: id, + config: newConfig, + } as TestingModuleConfigChangePayload); + }, + [api, id] + ); + + return [data.current, changeConfig] as const; +} diff --git a/code/addons/test/src/constants.ts b/code/addons/test/src/constants.ts index b1c1808db1a4..838594e212a3 100644 --- a/code/addons/test/src/constants.ts +++ b/code/addons/test/src/constants.ts @@ -1,3 +1,5 @@ +import type { TestResult } from './node/reporter'; + export const ADDON_ID = 'storybook/test'; export const TEST_PROVIDER_ID = `${ADDON_ID}/test-provider`; export const PANEL_ID = `${ADDON_ID}/panel`; @@ -7,3 +9,12 @@ export const TUTORIAL_VIDEO_LINK = 'https://youtu.be/Waht9qq7AoA'; 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 interface Config { + coverage: boolean; + a11y: boolean; +} + +export type Details = { + testResults: TestResult[]; +}; diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 9b560cdd8cee..dfe729688dd7 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -1,10 +1,8 @@ -import React, { useState } from 'react'; +import React from 'react'; -import { AddonPanel, Button, Link as LinkComponent } from 'storybook/internal/components'; -import { TESTING_MODULE_RUN_ALL_REQUEST } from 'storybook/internal/core-events'; +import { AddonPanel } from 'storybook/internal/components'; import type { Combo } from 'storybook/internal/manager-api'; import { Consumer, addons, types } from 'storybook/internal/manager-api'; -import { styled } from 'storybook/internal/theming'; import { type API_StatusObject, type API_StatusValue, @@ -12,15 +10,11 @@ import { Addon_TypesEnum, } from 'storybook/internal/types'; -import { EyeIcon, PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons'; - import { ContextMenuItem } from './components/ContextMenuItem'; -import { GlobalErrorModal } from './components/GlobalErrorModal'; import { Panel } from './components/Panel'; import { PanelTitle } from './components/PanelTitle'; -import { RelativeTime } from './components/RelativeTime'; -import { ADDON_ID, PANEL_ID, TEST_PROVIDER_ID } from './constants'; -import type { TestResult } from './node/reporter'; +import { TestProviderRender } from './components/TestProviderRender'; +import { ADDON_ID, type Config, type Details, PANEL_ID, TEST_PROVIDER_ID } from './constants'; const statusMap: Record = { failed: 'error', @@ -28,47 +22,6 @@ const statusMap: Record = { pending: 'pending', }; -export function getRelativeTimeString(date: Date): string { - const delta = Math.round((date.getTime() - Date.now()) / 1000); - const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]; - const units: Intl.RelativeTimeFormatUnit[] = [ - 'second', - 'minute', - 'hour', - 'day', - 'week', - 'month', - 'year', - ]; - - const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(delta)); - const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1; - const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); - return rtf.format(Math.floor(delta / divisor), units[unitIndex]); -} - -const Info = styled.div({ - display: 'flex', - flexDirection: 'column', - marginLeft: 6, -}); - -const SidebarContextMenuTitle = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({ - fontSize: theme.typography.size.s1, - fontWeight: crashed ? 'bold' : 'normal', - color: crashed ? theme.color.negativeText : theme.color.defaultText, -})); - -const Description = styled.div(({ theme }) => ({ - fontSize: theme.typography.size.s1, - color: theme.barTextColor, -})); - -const Actions = styled.div({ - display: 'flex', - gap: 6, -}); - addons.register(ADDON_ID, (api) => { const storybookBuilder = (globalThis as any).STORYBOOK_BUILDER || ''; if (storybookBuilder.includes('vite')) { @@ -83,7 +36,7 @@ addons.register(ADDON_ID, (api) => { watchable: true, name: 'Component tests', - sidebarContextMenu: ({ context, state }, { ListItem }) => { + sidebarContextMenu: ({ context, state }) => { if (context.type === 'docs') { return null; } @@ -91,111 +44,10 @@ addons.register(ADDON_ID, (api) => { return null; } - return ; + return ; }, - render: (state) => { - const [isModalOpen, setIsModalOpen] = useState(false); - - const title = state.crashed || state.failed ? 'Component tests failed' : 'Component tests'; - const errorMessage = state.error?.message; - let description: string | React.ReactNode = 'Not run'; - - if (state.running) { - description = state.progress - ? `Testing... ${state.progress.numPassedTests}/${state.progress.numTotalTests}` - : 'Starting...'; - } else if (state.failed && !errorMessage) { - description = ''; - } else if (state.crashed || (state.failed && errorMessage)) { - description = ( - <> - { - setIsModalOpen(true); - }} - > - {state.error?.name || 'View full error'} - - - ); - } else if (state.progress?.finishedAt) { - description = ( - - ); - } else if (state.watching) { - description = 'Watching for file changes'; - } - - return ( - <> - - - {title} - - {description} - - - - {state.watchable && ( - - )} - {state.runnable && ( - <> - {state.running && state.cancellable ? ( - - ) : ( - - )} - - )} - - - { - setIsModalOpen(false); - }} - onRerun={() => { - setIsModalOpen(false); - api - .getChannel() - .emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: TEST_PROVIDER_ID }); - }} - /> - - ); - }, + render: (state) => , mapStatusUpdate: (state) => Object.fromEntries( @@ -221,9 +73,7 @@ addons.register(ADDON_ID, (api) => { .filter(Boolean) ) ), - } as Addon_TestProviderType<{ - testResults: TestResult[]; - }>); + } as Addon_TestProviderType); } const filter = ({ state }: Combo) => { diff --git a/code/addons/test/src/node/boot-test-runner.test.ts b/code/addons/test/src/node/boot-test-runner.test.ts index 7792bffdc9b3..f5d9fde5bf28 100644 --- a/code/addons/test/src/node/boot-test-runner.test.ts +++ b/code/addons/test/src/node/boot-test-runner.test.ts @@ -5,7 +5,6 @@ import { Channel, type ChannelTransport } from '@storybook/core/channels'; import { TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, TESTING_MODULE_PROGRESS_REPORT, - TESTING_MODULE_RUN_ALL_REQUEST, TESTING_MODULE_RUN_REQUEST, TESTING_MODULE_WATCH_MODE_REQUEST, } from '@storybook/core/core-events'; @@ -103,13 +102,6 @@ describe('bootTestRunner', () => { 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'], diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts index 4acc4995919b..0771604861d9 100644 --- a/code/addons/test/src/node/boot-test-runner.ts +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -4,7 +4,6 @@ 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, type TestingModuleCrashReportPayload, @@ -40,8 +39,6 @@ const bootTestRunner = async (channel: Channel, initEvent?: string, initArgs?: a 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[]) => @@ -49,7 +46,6 @@ const bootTestRunner = async (channel: Channel, initEvent?: string, initArgs?: a 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(); @@ -88,7 +84,6 @@ const bootTestRunner = async (channel: Channel, initEvent?: string, initArgs?: a // 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); diff --git a/code/addons/test/src/node/reporter.ts b/code/addons/test/src/node/reporter.ts index 833ab0bf3823..51d2dc010cff 100644 --- a/code/addons/test/src/node/reporter.ts +++ b/code/addons/test/src/node/reporter.ts @@ -64,7 +64,7 @@ export class StorybookReporter implements Reporter { sendReport: (payload: TestingModuleProgressReportPayload) => void; - constructor(private testManager: TestManager) { + constructor(public testManager: TestManager) { this.sendReport = throttle((payload) => this.testManager.sendProgressReport(payload), 1000); } @@ -75,18 +75,19 @@ export class StorybookReporter implements Reporter { getProgressReport(finishedAt?: number) { 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 fileTests = getTests(files).filter((t) => t.mode === 'run' || t.mode === 'only'); + + // The total number of tests reported by Vitest is dynamic and can change during the run, so we + // use `storyCountForCurrentRun` instead, based on the list of stories provided in the run request. + const numTotalTests = finishedAt + ? fileTests.length + : Math.max(fileTests.length, this.testManager.vitestManager.storyCountForCurrentRun); 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: TestResult[] = []; + const numPendingTests = fileTests.filter((t) => t.result?.state === 'run').length; - for (const file of files) { + const testResults: TestResult[] = files.map((file) => { const tests = getTests([file]); let startTime = tests.reduce( (prev, next) => Math.min(prev, next.result?.startTime ?? Number.POSITIVE_INFINITY), @@ -102,7 +103,7 @@ export class StorybookReporter implements Reporter { startTime ); - const assertionResults = tests.flatMap((t) => { + const results = tests.flatMap((t) => { const ancestorTitles: string[] = []; let iter: Suite | undefined = t.suite; while (iter) { @@ -129,15 +130,14 @@ export class StorybookReporter implements Reporter { }); const hasFailedTests = tests.some((t) => t.result?.state === 'fail'); - - testResults.push({ - results: assertionResults, + return { + results, startTime, endTime, status: file.result?.state === 'fail' || hasFailedTests ? 'failed' : 'passed', message: file.result?.errors?.[0]?.stack || file.result?.errors?.[0]?.message, - }); - } + }; + }); return { cancellable: !finishedAt, diff --git a/code/addons/test/src/node/test-manager.test.ts b/code/addons/test/src/node/test-manager.test.ts index 826d71a877b9..b674c1d786f7 100644 --- a/code/addons/test/src/node/test-manager.test.ts +++ b/code/addons/test/src/node/test-manager.test.ts @@ -2,12 +2,14 @@ import { describe, expect, it, vi } from 'vitest'; import { createVitest } from 'vitest/node'; import { Channel, type ChannelTransport } from '@storybook/core/channels'; +import type { StoryIndex } from '@storybook/types'; import path from 'pathe'; import { TEST_PROVIDER_ID } from '../constants'; import { TestManager } from './test-manager'; +const setTestNamePattern = vi.hoisted(() => vi.fn()); const vitest = vi.hoisted(() => ({ projects: [{}], init: vi.fn(), @@ -18,7 +20,14 @@ const vitest = vi.hoisted(() => ({ globTestSpecs: vi.fn(), getModuleProjects: vi.fn(() => []), configOverride: { - testNamePattern: undefined, + actualTestNamePattern: undefined, + get testNamePattern() { + return this.actualTestNamePattern; + }, + set testNamePattern(value: string) { + setTestNamePattern(value); + this.actualTestNamePattern = value; + }, }, })); @@ -40,6 +49,33 @@ const tests = [ }, ]; +global.fetch = vi.fn().mockResolvedValue({ + json: () => + new Promise((resolve) => + resolve({ + v: 5, + entries: { + 'story--one': { + type: 'story', + id: 'story--one', + name: 'One', + title: 'story/one', + importPath: 'path/to/file', + tags: ['test'], + }, + 'another--one': { + type: 'story', + id: 'another--one', + name: 'One', + title: 'another/one', + importPath: 'path/to/another/file', + tags: ['test'], + }, + }, + } as StoryIndex) + ), +}); + const options: ConstructorParameters[1] = { onError: (message, error) => { throw error; @@ -83,16 +119,7 @@ describe('TestManager', () => { await testManager.handleRunRequest({ providerId: TEST_PROVIDER_ID, - payload: [ - { - stories: [], - importPath: 'path/to/file', - }, - { - stories: [], - importPath: 'path/to/another/file', - }, - ], + indexUrl: 'http://localhost:6006/index.json', }); expect(createVitest).toHaveBeenCalledTimes(1); expect(vitest.runFiles).toHaveBeenCalledWith(tests, true); @@ -104,33 +131,18 @@ describe('TestManager', () => { await testManager.handleRunRequest({ providerId: TEST_PROVIDER_ID, - payload: [ - { - stories: [], - importPath: 'path/to/unknown/file', - }, - ], + indexUrl: 'http://localhost:6006/index.json', + storyIds: [], }); expect(vitest.runFiles).toHaveBeenCalledWith([], true); + expect(vitest.configOverride.testNamePattern).toBeUndefined(); await testManager.handleRunRequest({ providerId: TEST_PROVIDER_ID, - payload: [ - { - stories: [], - importPath: 'path/to/file', - }, - ], + indexUrl: 'http://localhost:6006/index.json', + storyIds: ['story--one'], }); + expect(setTestNamePattern).toHaveBeenCalledWith(/^One$/); expect(vitest.runFiles).toHaveBeenCalledWith(tests.slice(0, 1), true); }); - - it('should handle run all request', async () => { - const testManager = await TestManager.start(mockChannel, options); - expect(createVitest).toHaveBeenCalledTimes(1); - - await testManager.handleRunAllRequest({ providerId: TEST_PROVIDER_ID }); - expect(createVitest).toHaveBeenCalledTimes(1); - expect(vitest.runFiles).toHaveBeenCalledWith(tests, true); - }); }); diff --git a/code/addons/test/src/node/test-manager.ts b/code/addons/test/src/node/test-manager.ts index a462fb1f7371..624916772056 100644 --- a/code/addons/test/src/node/test-manager.ts +++ b/code/addons/test/src/node/test-manager.ts @@ -1,13 +1,13 @@ import type { Channel } from 'storybook/internal/channels'; import { TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, + TESTING_MODULE_CONFIG_CHANGE, TESTING_MODULE_PROGRESS_REPORT, - TESTING_MODULE_RUN_ALL_REQUEST, TESTING_MODULE_RUN_REQUEST, TESTING_MODULE_WATCH_MODE_REQUEST, type TestingModuleCancelTestRunRequestPayload, + type TestingModuleConfigChangePayload, type TestingModuleProgressReportPayload, - type TestingModuleRunAllRequestPayload, type TestingModuleRunRequestPayload, type TestingModuleWatchModeRequestPayload, } from 'storybook/internal/core-events'; @@ -16,7 +16,7 @@ import { TEST_PROVIDER_ID } from '../constants'; import { VitestManager } from './vitest-manager'; export class TestManager { - private vitestManager: VitestManager; + vitestManager: VitestManager; watchMode = false; @@ -30,7 +30,7 @@ export class TestManager { 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_CONFIG_CHANGE, this.handleConfigChange.bind(this)); this.channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, this.handleWatchModeRequest.bind(this)); this.channel.on(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, this.handleCancelRequest.bind(this)); @@ -43,14 +43,22 @@ export class TestManager { await this.vitestManager.startVitest(watchMode); } - async handleWatchModeRequest(request: TestingModuleWatchModeRequestPayload) { + async handleConfigChange(payload: TestingModuleConfigChangePayload) { + // TODO do something with the config + const config = payload.config; + } + + async handleWatchModeRequest(payload: TestingModuleWatchModeRequestPayload) { + // TODO do something with the config + const config = payload.config; + try { - if (request.providerId !== TEST_PROVIDER_ID) { + if (payload.providerId !== TEST_PROVIDER_ID) { return; } - if (this.watchMode !== request.watchMode) { - this.watchMode = request.watchMode; + if (this.watchMode !== payload.watchMode) { + this.watchMode = payload.watchMode; await this.restartVitest(this.watchMode); } } catch (e) { @@ -58,33 +66,21 @@ export class TestManager { } } - async handleRunRequest(request: TestingModuleRunRequestPayload) { + async handleRunRequest(payload: TestingModuleRunRequestPayload) { try { - if (request.providerId !== TEST_PROVIDER_ID) { + if (payload.providerId !== TEST_PROVIDER_ID) { return; } - await this.vitestManager.runTests(request.payload); + await this.vitestManager.runTests(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 handleCancelRequest(request: TestingModuleCancelTestRunRequestPayload) { + async handleCancelRequest(payload: TestingModuleCancelTestRunRequestPayload) { try { - if (request.providerId !== TEST_PROVIDER_ID) { + if (payload.providerId !== TEST_PROVIDER_ID) { return; } diff --git a/code/addons/test/src/node/vitest-manager.ts b/code/addons/test/src/node/vitest-manager.ts index 4441a66371f4..b14da16ecce7 100644 --- a/code/addons/test/src/node/vitest-manager.ts +++ b/code/addons/test/src/node/vitest-manager.ts @@ -5,6 +5,8 @@ import type { TestProject, TestSpecification, Vitest, WorkspaceProject } from 'v import type { Channel } from 'storybook/internal/channels'; import type { TestingModuleRunRequestPayload } from 'storybook/internal/core-events'; +import type { DocsIndexEntry, StoryIndex, StoryIndexEntry } from '@storybook/types'; + import path, { normalize } from 'pathe'; import slash from 'slash'; @@ -12,11 +14,19 @@ import { log } from '../logger'; import { StorybookReporter } from './reporter'; import type { TestManager } from './test-manager'; +type TagsFilter = { + include: string[]; + exclude: string[]; + skip: string[]; +}; + export class VitestManager { vitest: Vitest | null = null; vitestStartupCounter = 0; + storyCountForCurrentRun: number = 0; + constructor( private channel: Channel, private testManager: TestManager @@ -54,23 +64,6 @@ export class VitestManager { } } - async runAllTests() { - if (!this.vitest) { - await this.startVitest(); - } - this.resetTestNamePattern(); - - const storybookTests = await this.getStorybookTestSpecs(); - for (const storybookTest of storybookTests) { - // make sure to clear the file cache so test results are updated even if watch mode is not enabled - if (!this.testManager.watchMode) { - this.updateLastChanged(storybookTest.moduleId); - } - } - await this.cancelCurrentRun(); - await this.vitest!.runFiles(storybookTests, true); - } - private updateLastChanged(filepath: string) { const projects = this.vitest!.getModuleProjects(filepath); projects.forEach(({ server, browser }) => { @@ -84,55 +77,88 @@ export class VitestManager { }); } - async runTests(testPayload: TestingModuleRunRequestPayload['payload']) { + private async fetchStories(indexUrl: string, requestStoryIds?: string[]) { + try { + const index = (await Promise.race([ + fetch(indexUrl).then((res) => res.json()), + new Promise((_, reject) => setTimeout(reject, 3000, new Error('Request took too long'))), + ])) as StoryIndex; + const storyIds = requestStoryIds || Object.keys(index.entries); + return storyIds.map((id) => index.entries[id]).filter((story) => story.type === 'story'); + } catch (e) { + log('Failed to fetch story index: ' + e.message); + return []; + } + } + + private filterStories( + story: StoryIndexEntry | DocsIndexEntry, + moduleId: string, + tagsFilter: TagsFilter + ) { + const absoluteImportPath = path.join(process.cwd(), story.importPath); + if (absoluteImportPath !== moduleId) { + return false; + } + if (tagsFilter.include.length && !tagsFilter.include.some((tag) => story.tags?.includes(tag))) { + return false; + } + if (tagsFilter.exclude.some((tag) => story.tags?.includes(tag))) { + return false; + } + // Skipped tests are intentionally included here + return true; + } + + async runTests(requestPayload: TestingModuleRunRequestPayload) { if (!this.vitest) { await this.startVitest(); } this.resetTestNamePattern(); - // 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(); - - const filteredStoryNames: string[] = []; - - for (const storybookTest of storybookTests) { - const match = testPayload.find((test) => { - const absoluteImportPath = path.join(process.cwd(), test.importPath); - return absoluteImportPath === storybookTest.moduleId; - }); - if (match) { - // make sure to clear the file cache so test results are updated even if watch mode is not enabled - if (!this.testManager.watchMode) { - this.updateLastChanged(storybookTest.moduleId); - } - - if (match.stories?.length) { - filteredStoryNames.push(...match.stories.map((story) => story.name)); + const stories = await this.fetchStories(requestPayload.indexUrl, requestPayload.storyIds); + const vitestTestSpecs = await this.getStorybookTestSpecs(); + const isSingleStoryRun = requestPayload.storyIds?.length === 1; + + const { filteredTestFiles, totalTestCount } = vitestTestSpecs.reduce( + (acc, spec) => { + /* eslint-disable no-underscore-dangle */ + const { env = {} } = spec.project.config; + const include = env.__VITEST_INCLUDE_TAGS__?.split(',').filter(Boolean) ?? ['test']; + const exclude = env.__VITEST_EXCLUDE_TAGS__?.split(',').filter(Boolean) ?? []; + const skip = env.__VITEST_SKIP_TAGS__?.split(',').filter(Boolean) ?? []; + /* eslint-enable no-underscore-dangle */ + + const matches = stories.filter((story) => + this.filterStories(story, spec.moduleId, { include, exclude, skip }) + ); + if (matches.length) { + if (!this.testManager.watchMode) { + // Clear the file cache if watch mode is not enabled + this.updateLastChanged(spec.moduleId); + } + acc.filteredTestFiles.push(spec); + acc.totalTestCount += matches.filter( + // Don't count skipped stories, because StorybookReporter doesn't include them either + (story) => !skip.some((tag) => story.tags?.includes(tag)) + ).length; } - testList.push(storybookTest); - } - } + return acc; + }, + { filteredTestFiles: [] as TestSpecification[], totalTestCount: 0 } + ); await this.cancelCurrentRun(); + this.storyCountForCurrentRun = totalTestCount; - if (filteredStoryNames.length > 0) { - // temporarily set the test name pattern to only run the selected stories - // converting a list of story names to a single regex pattern - // ie. ['My Story', 'Other Story'] => /^(My Story|Other Story)$/ - const testNamePattern = new RegExp( - `^(${filteredStoryNames - .map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - .join('|')})$` + if (isSingleStoryRun) { + const storyName = stories[0].name; + this.vitest!.configOverride.testNamePattern = new RegExp( + `^${storyName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$` ); - this.vitest!.configOverride.testNamePattern = testNamePattern; } - await this.vitest!.runFiles(testList, true); + await this.vitest!.runFiles(filteredTestFiles, true); this.resetTestNamePattern(); } @@ -224,6 +250,7 @@ export class VitestManager { const id = slash(file); this.vitest?.logger.clearHighlightCache(id); this.updateLastChanged(id); + this.storyCountForCurrentRun = 0; await this.runAffectedTests(file); } diff --git a/code/addons/test/src/preset.ts b/code/addons/test/src/preset.ts index 41808c55d21b..a1a6e1233faf 100644 --- a/code/addons/test/src/preset.ts +++ b/code/addons/test/src/preset.ts @@ -3,7 +3,6 @@ import { readFileSync } from 'node:fs'; import type { Channel } from 'storybook/internal/channels'; import { checkAddonOrder, getFrameworkName, serverRequire } from 'storybook/internal/common'; import { - TESTING_MODULE_RUN_ALL_REQUEST, TESTING_MODULE_RUN_REQUEST, TESTING_MODULE_WATCH_MODE_REQUEST, } from 'storybook/internal/core-events'; @@ -68,7 +67,6 @@ export const experimental_serverChannel = async (channel: Channel, options: Opti runTestRunner(channel, eventName, args); }; - channel.on(TESTING_MODULE_RUN_ALL_REQUEST, execute(TESTING_MODULE_RUN_ALL_REQUEST)); channel.on(TESTING_MODULE_RUN_REQUEST, execute(TESTING_MODULE_RUN_REQUEST)); channel.on(TESTING_MODULE_WATCH_MODE_REQUEST, (payload) => { if (payload.watchMode) { diff --git a/code/addons/test/src/vitest-plugin/index.ts b/code/addons/test/src/vitest-plugin/index.ts index ac2c65b4f860..b93eed96ddcf 100644 --- a/code/addons/test/src/vitest-plugin/index.ts +++ b/code/addons/test/src/vitest-plugin/index.ts @@ -99,15 +99,27 @@ export const storybookTest = (options?: UserOptions): Plugin => { ...config.test.env, // To be accessed by the setup file __STORYBOOK_URL__: storybookUrl, + __VITEST_INCLUDE_TAGS__: finalOptions.tags.include.join(','), + __VITEST_EXCLUDE_TAGS__: finalOptions.tags.exclude.join(','), + __VITEST_SKIP_TAGS__: finalOptions.tags.skip.join(','), }; if (config.test.browser) { config.test.browser.screenshotFailures ??= false; } + // copying straight from https://github.com/vitejs/vite/blob/main/packages/vite/src/node/constants.ts#L60 + // to avoid having to maintain Vite as a dependency just for this + const viteDefaultClientConditions = ['module', 'browser', 'development|production']; + config.resolve ??= {}; config.resolve.conditions ??= []; - config.resolve.conditions.push('storybook', 'stories', 'test'); + config.resolve.conditions.push( + 'storybook', + 'stories', + 'test', + ...viteDefaultClientConditions + ); config.test.setupFiles ??= []; if (typeof config.test.setupFiles === 'string') { diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json index b69d9f3ecfb1..15b210dcf24e 100644 --- a/code/builders/builder-vite/package.json +++ b/code/builders/builder-vite/package.json @@ -61,7 +61,7 @@ }, "peerDependencies": { "storybook": "workspace:^", - "vite": "^4.0.0 || ^5.0.0" + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" }, "publishConfig": { "access": "public" diff --git a/code/builders/builder-vite/src/vite-config.test.ts b/code/builders/builder-vite/src/vite-config.test.ts index d88da0305397..973746cfbbe6 100644 --- a/code/builders/builder-vite/src/vite-config.test.ts +++ b/code/builders/builder-vite/src/vite-config.test.ts @@ -10,6 +10,7 @@ import { commonConfig } from './vite-config'; vi.mock('vite', async (importOriginal) => ({ ...(await importOriginal()), loadConfigFromFile: vi.fn(async () => ({})), + defaultClientConditions: undefined, })); const loadConfigFromFileMock = vi.mocked(loadConfigFromFile); diff --git a/code/builders/builder-vite/src/vite-config.ts b/code/builders/builder-vite/src/vite-config.ts index 2ab46f81de8c..8983dfc137b3 100644 --- a/code/builders/builder-vite/src/vite-config.ts +++ b/code/builders/builder-vite/src/vite-config.ts @@ -47,7 +47,9 @@ export async function commonConfig( _type: PluginConfigType ): Promise { const configEnv = _type === 'development' ? configEnvServe : configEnvBuild; - const { loadConfigFromFile, mergeConfig } = await import('vite'); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this property only exists in Vite 6 + const { loadConfigFromFile, mergeConfig, defaultClientConditions = [] } = await import('vite'); const { viteConfigPath } = await getBuilderOptions(options); @@ -67,7 +69,7 @@ export async function commonConfig( base: './', plugins: await pluginConfig(options), resolve: { - conditions: ['storybook', 'stories', 'test'], + conditions: ['storybook', 'stories', 'test', ...defaultClientConditions], preserveSymlinks: isPreservingSymlinks(), alias: { assert: require.resolve('browser-assert'), diff --git a/code/core/src/core-events/data/testing-module.ts b/code/core/src/core-events/data/testing-module.ts index ee483364d93e..80edea66aa64 100644 --- a/code/core/src/core-events/data/testing-module.ts +++ b/code/core/src/core-events/data/testing-module.ts @@ -4,26 +4,19 @@ type DateNow = number; export type TestProviderId = Addon_TestProviderType['id']; export type TestProviderConfig = Addon_TestProviderType; -export type TestProviderState = Addon_TestProviderState; +export type TestProviderState< + Details extends { [key: string]: any } = NonNullable, + Config extends { [key: string]: any } = NonNullable, +> = Addon_TestProviderState; export type TestProviders = Record; -export type TestingModuleRunRequestStory = { - id: string; // button--primary - name: string; // Primary -}; - export type TestingModuleRunRequestPayload = { providerId: TestProviderId; - payload: { - importPath: string; // ./.../button.stories.tsx - stories?: TestingModuleRunRequestStory[]; - componentPath?: string; // ./.../button.tsx - }[]; -}; - -export type TestingModuleRunAllRequestPayload = { - 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']; }; export type TestingModuleProgressReportPayload = @@ -81,4 +74,10 @@ export type TestingModuleCancelTestRunResponsePayload = export type TestingModuleWatchModeRequestPayload = { providerId: TestProviderId; watchMode: boolean; + config?: TestProviderState['config']; +}; + +export type TestingModuleConfigChangePayload = { + providerId: TestProviderId; + config: TestProviderState['config']; }; diff --git a/code/core/src/core-events/index.ts b/code/core/src/core-events/index.ts index d5d51eeecdf4..8a1e0a238b8f 100644 --- a/code/core/src/core-events/index.ts +++ b/code/core/src/core-events/index.ts @@ -88,10 +88,12 @@ enum events { TESTING_MODULE_CRASH_REPORT = 'testingModuleCrashReport', TESTING_MODULE_PROGRESS_REPORT = 'testingModuleProgressReport', TESTING_MODULE_RUN_REQUEST = 'testingModuleRunRequest', + /** @deprecated Use TESTING_MODULE_RUN_REQUEST instead */ 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', + TESTING_MODULE_CONFIG_CHANGE = 'testingModuleConfigChange', } // Enables: `import Events from ...` @@ -162,6 +164,7 @@ export const { TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, TESTING_MODULE_CANCEL_TEST_RUN_RESPONSE, TESTING_MODULE_WATCH_MODE_REQUEST, + TESTING_MODULE_CONFIG_CHANGE, } = events; export * from './data/create-new-story'; diff --git a/code/core/src/csf-tools/vitest-plugin/transformer.ts b/code/core/src/csf-tools/vitest-plugin/transformer.ts index 677235975cb1..c318faa1d29e 100644 --- a/code/core/src/csf-tools/vitest-plugin/transformer.ts +++ b/code/core/src/csf-tools/vitest-plugin/transformer.ts @@ -19,11 +19,14 @@ type TagsFilter = { }; const isValidTest = (storyTags: string[], tagsFilter: TagsFilter) => { - const isIncluded = - tagsFilter?.include.length === 0 || tagsFilter?.include.some((tag) => storyTags.includes(tag)); - const isNotExcluded = tagsFilter?.exclude.every((tag) => !storyTags.includes(tag)); - - return isIncluded && isNotExcluded; + if (tagsFilter.include.length && !tagsFilter.include.some((tag) => storyTags?.includes(tag))) { + return false; + } + if (tagsFilter.exclude.some((tag) => storyTags?.includes(tag))) { + return false; + } + // Skipped tests are intentionally included here + return true; }; /** * TODO: the functionality in this file can be moved back to the vitest plugin itself It can use diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts index 54bfdc437da2..15aec353b32a 100644 --- a/code/core/src/manager-api/modules/experimental_testmodule.ts +++ b/code/core/src/manager-api/modules/experimental_testmodule.ts @@ -1,4 +1,4 @@ -import { type API_StoryEntry, Addon_TypesEnum, type StoryId } from '@storybook/core/types'; +import { Addon_TypesEnum, type StoryId } from '@storybook/core/types'; import { TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, @@ -8,8 +8,8 @@ import { type TestProviderId, type TestProviderState, type TestProviders, - type TestingModuleRunAllRequestPayload, type TestingModuleRunRequestPayload, + type TestingModuleWatchModeRequestPayload, } from '@storybook/core/core-events'; import invariant from 'tiny-invariant'; @@ -21,6 +21,7 @@ export type SubState = { }; const initialTestProviderState: TestProviderState = { + config: {} as { [key: string]: any }, details: {} as { [key: string]: any }, cancellable: false, cancelling: false, @@ -78,87 +79,58 @@ export const init: ModuleFn = ({ store, fullAPI }) => { ); }, runTestProvider(id, options) { - if (!options?.entryId) { - const payload: TestingModuleRunAllRequestPayload = { providerId: id }; - fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, payload); - return () => api.cancelTestProvider(id); - } - const index = store.getState().index; invariant(index, 'The index is currently unavailable'); - const entry = index[options.entryId]; + const provider = store.getState().testProviders[id]; - invariant(entry, `No entry found in the index for id '${options.entryId}'`); + const indexUrl = new URL('index.json', window.location.href).toString(); - if (entry.type === 'story') { + if (!options?.entryId) { const payload: TestingModuleRunRequestPayload = { providerId: id, - payload: [ - { - importPath: entry.importPath, - stories: [ - { - id: entry.id, - name: entry.name, - }, - ], - }, - ], + indexUrl, + config: provider.config, }; + fullAPI.emit(TESTING_MODULE_RUN_REQUEST, payload); + + // For backwards compatibility: + fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id }); + return () => api.cancelTestProvider(id); } - const payloads = new Set(); - - const findComponents = (entryId: StoryId) => { - const foundEntry = index[entryId]; - switch (foundEntry.type) { - case 'component': - const firstStoryId = foundEntry.children.find( - (childId) => index[childId].type === 'story' - ); - if (!firstStoryId) { - // happens when there are only docs in the component - return; - } - payloads.add({ importPath: (index[firstStoryId] as API_StoryEntry).importPath }); - return; - case 'story': { - // this shouldn't happen because we don't visit components' children. - // so we never get to a story directly. - payloads.add({ - importPath: foundEntry.importPath, - stories: [ - { - id: foundEntry.id, - name: foundEntry.name, - }, - ], - }); - return; - } - case 'docs': { - return; - } - default: - foundEntry.children.forEach(findComponents); + const entry = index[options.entryId]; + invariant(entry, `No entry found in the index for id '${options.entryId}'`); + + const findStories = (entryId: StoryId, results: StoryId[] = []): StoryId[] => { + const node = index[entryId]; + if (node.type === 'story') { + results.push(node.id); + } else if ('children' in node) { + node.children.forEach((childId) => findStories(childId, results)); } + return results; }; - findComponents(options.entryId); const payload: TestingModuleRunRequestPayload = { providerId: id, - payload: Array.from(payloads), + indexUrl, + storyIds: findStories(options.entryId), + config: provider.config, }; fullAPI.emit(TESTING_MODULE_RUN_REQUEST, payload); - return () => api.cancelTestProvider(id); }, setTestProviderWatchMode(id, watchMode) { api.updateTestProviderState(id, { watching: watchMode }); - fullAPI.emit(TESTING_MODULE_WATCH_MODE_REQUEST, { providerId: id, watchMode }); + const config = store.getState().testProviders[id].config; + fullAPI.emit(TESTING_MODULE_WATCH_MODE_REQUEST, { + providerId: id, + watchMode, + config, + } as TestingModuleWatchModeRequestPayload); }, cancelTestProvider(id) { api.updateTestProviderState(id, { cancelling: true }); diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index 76118ffc8711..867ed8da72b7 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -106,7 +106,7 @@ export function generateTestProviderLinks( if (!state) { return null; } - const content = state.sidebarContextMenu?.({ context, state }, ContextMenu); + const content = state.sidebarContextMenu?.({ context, state }); if (!content) { return null; diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx index e09d0cafb62b..898b02f28945 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx @@ -1,13 +1,40 @@ -import React from 'react'; +import React, { type FC, Fragment, useEffect, useState } from 'react'; import { Addon_TypesEnum } from '@storybook/core/types'; -import type { Meta } from '@storybook/react'; -import { fn } from '@storybook/test'; +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, fn, waitFor, within } from '@storybook/test'; import { type API, ManagerContext } from '@storybook/core/manager-api'; +import { userEvent } from '@storybook/testing-library'; import { SidebarBottomBase } from './SidebarBottom'; +const DynamicHeightDemo: FC = () => { + const [height, setHeight] = useState(100); + + useEffect(() => { + const interval = setInterval(() => { + setHeight((h) => (h === 100 ? 200 : 100)); + }, 2000); + return () => clearInterval(interval); + }, []); + + return ( +
+ CUSTOM CONTENT WITH DYNAMIC HEIGHT +
+ ); +}; + const managerContext: any = { state: { docsOptions: { @@ -57,7 +84,16 @@ export default { getElements: fn(() => ({})), } as any as API, }, + parameters: { + layout: 'fullscreen', + }, decorators: [ + (storyFn) => ( +
+
+ {storyFn()} +
+ ), (storyFn) => ( {storyFn()} ), @@ -92,3 +128,45 @@ export const Both = { }, }, }; + +export const DynamicHeight: StoryObj = { + decorators: [ + (storyFn) => ( + , + runnable: true, + }, + }, + }, + }} + > + {storyFn()} + + ), + ], + play: async ({ canvasElement }) => { + const screen = await within(canvasElement); + + const toggleButton = await screen.getByLabelText('Collapse testing module'); + const content = await screen.findByText('CUSTOM CONTENT WITH DYNAMIC HEIGHT'); + const collapse = await screen.getByTestId('collapse'); + + await expect(content).toBeVisible(); + + await userEvent.click(toggleButton); + + await waitFor(() => expect(collapse.getBoundingClientRect()).toHaveProperty('height', 0)); + + await userEvent.click(toggleButton); + + await waitFor(() => expect(collapse.getBoundingClientRect()).not.toHaveProperty('height', 0)); + }, +}; diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index 9eea4a4855b8..ba64b4e500a5 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -1,17 +1,11 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { Fragment, useEffect, useRef, useState } from 'react'; import { styled } from '@storybook/core/theming'; -import { type API_FilterFunction, Addon_TypesEnum } from '@storybook/core/types'; +import { type API_FilterFunction } from '@storybook/core/types'; import { - TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, TESTING_MODULE_CRASH_REPORT, TESTING_MODULE_PROGRESS_REPORT, - TESTING_MODULE_RUN_ALL_REQUEST, - TESTING_MODULE_WATCH_MODE_REQUEST, - type TestProviderId, - type TestProviderState, - type TestProviders, type TestingModuleCrashReportPayload, type TestingModuleProgressReportPayload, } from '@storybook/core/core-events'; @@ -30,18 +24,6 @@ const SIDEBAR_BOTTOM_SPACER_ID = 'sidebar-bottom-spacer'; // This ID is used by some integrators to target the (fixed position) sidebar bottom element so it should remain stable. const SIDEBAR_BOTTOM_WRAPPER_ID = 'sidebar-bottom-wrapper'; -const STORAGE_KEY = '@storybook/manager/test-providers'; - -const initialTestProviderState: TestProviderState = { - details: {} as { [key: string]: any }, - cancellable: false, - cancelling: false, - running: false, - watching: false, - failed: false, - crashed: false, -}; - const filterNone: API_FilterFunction = () => true; const filterWarn: API_FilterFunction = ({ status = {} }) => Object.values(status).some((value) => value?.status === 'warn'); @@ -65,6 +47,10 @@ const getFilter = (warningsActive = false, errorsActive = false) => { return filterNone; }; +const Spacer = styled.div({ + pointerEvents: 'none', +}); + const Content = styled.div(({ theme }) => ({ position: 'absolute', bottom: 0, @@ -117,15 +103,13 @@ export const SidebarBottomBase = ({ const hasErrors = errors.length > 0; useEffect(() => { - const spacer = spacerRef.current; - const wrapper = wrapperRef.current; - if (spacer && wrapper) { + if (spacerRef.current && wrapperRef.current) { const resizeObserver = new ResizeObserver(() => { - if (spacer && wrapper) { - spacer.style.height = `${wrapper.clientHeight}px`; + if (spacerRef.current && wrapperRef.current) { + spacerRef.current.style.height = `${wrapperRef.current.scrollHeight}px`; } }); - resizeObserver.observe(wrapper); + resizeObserver.observe(wrapperRef.current); return () => resizeObserver.disconnect(); } }, []); @@ -145,13 +129,6 @@ export const SidebarBottomBase = ({ }); }; - const clearState = ({ providerId }: { providerId: TestProviderId }) => { - api.clearTestProviderState(providerId); - api.experimental_updateStatus(providerId, (state = {}) => - Object.fromEntries(Object.keys(state).map((key) => [key, null])) - ); - }; - const onProgressReport = ({ providerId, ...result }: TestingModuleProgressReportPayload) => { if (result.status === 'failed') { api.updateTestProviderState(providerId, { ...result, running: false, failed: true }); @@ -168,13 +145,11 @@ export const SidebarBottomBase = ({ }; api.on(TESTING_MODULE_CRASH_REPORT, onCrashReport); - api.on(TESTING_MODULE_RUN_ALL_REQUEST, clearState); api.on(TESTING_MODULE_PROGRESS_REPORT, onProgressReport); return () => { api.off(TESTING_MODULE_CRASH_REPORT, onCrashReport); api.off(TESTING_MODULE_PROGRESS_REPORT, onProgressReport); - api.off(TESTING_MODULE_RUN_ALL_REQUEST, clearState); }; }, [api, testProviders]); @@ -184,7 +159,8 @@ export const SidebarBottomBase = ({ } return ( -
+ + {isDevelopment && ( @@ -201,13 +177,14 @@ export const SidebarBottomBase = ({ /> )} -
+ ); }; export const SidebarBottom = ({ isDevelopment }: { isDevelopment?: boolean }) => { const api = useStorybookApi(); const { notifications, status } = useStorybookState(); + return ( ({ const Collapsible = styled.div(({ theme }) => ({ overflow: 'hidden', - transition: 'max-height 250ms', + willChange: 'auto', boxShadow: `inset 0 -1px 0 ${theme.appBorderColor}`, })); @@ -164,52 +171,89 @@ export const TestingModule = ({ setWarningsActive, }: TestingModuleProps) => { const api = useStorybookApi(); + const contentRef = useRef(null); - const [collapsed, setCollapsed] = useState(false); + const timeoutRef = useRef>(null); + const [maxHeight, setMaxHeight] = useState(DEFAULT_HEIGHT); + const [isCollapsed, setCollapsed] = useState(false); + const [isChangingCollapse, setChangingCollapse] = useState(false); useEffect(() => { - setMaxHeight(contentRef.current?.offsetHeight || DEFAULT_HEIGHT); + if (contentRef.current) { + setMaxHeight(contentRef.current?.getBoundingClientRect().height || DEFAULT_HEIGHT); + + const resizeObserver = new ResizeObserver(() => { + requestAnimationFrame(() => { + if (contentRef.current && !isCollapsed) { + const height = contentRef.current?.getBoundingClientRect().height || DEFAULT_HEIGHT; + + setMaxHeight(height); + } + }); + }); + resizeObserver.observe(contentRef.current); + return () => resizeObserver.disconnect(); + } + }, [isCollapsed]); + + const toggleCollapsed = useCallback((event: SyntheticEvent) => { + event.stopPropagation(); + setChangingCollapse(true); + setCollapsed((s) => !s); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + setChangingCollapse(false); + }, 250); }, []); - const toggleCollapsed = () => { - setMaxHeight(contentRef.current?.offsetHeight || DEFAULT_HEIGHT); - setCollapsed(!collapsed); - }; + const isRunning = testProviders.some((tp) => tp.running); + const isCrashed = testProviders.some((tp) => tp.crashed); + const isFailed = testProviders.some((tp) => tp.failed); + const hasTestProviders = testProviders.length > 0; - const running = testProviders.some((tp) => tp.running); - const crashed = testProviders.some((tp) => tp.crashed); - const failed = testProviders.some((tp) => tp.failed); - const testing = testProviders.length > 0; + if (!hasTestProviders && (!errorCount || !warningCount)) { + return null; + } return ( 0} + running={isRunning} + crashed={isCrashed} + failed={isFailed || errorCount > 0} > - - - {testProviders.map((state) => { - const { render: Render } = state; - return ( - - {Render ? : } - - ); - })} - - + {hasTestProviders && ( + + + {testProviders.map((state) => { + const { render: Render } = state; + return Render ? ( + + + + ) : ( + + + + ); + })} + + + )} - - {testing && ( + + {hasTestProviders && ( )} - {testing && ( + {hasTestProviders && ( , + Config extends { [key: string]: any } = NonNullable, > { type: Addon_TypesEnum.experimental_TEST_PROVIDER; /** The unique id of the test provider. */ id: string; name: string; /** @deprecated Use render instead */ - title?: (state: TestProviderConfig & Addon_TestProviderState
) => ReactNode; + title?: (state: TestProviderConfig & Addon_TestProviderState) => ReactNode; /** @deprecated Use render instead */ - description?: (state: TestProviderConfig & Addon_TestProviderState
) => ReactNode; - render?: (state: TestProviderConfig & Addon_TestProviderState
) => ReactNode; - sidebarContextMenu?: ( - options: { - context: API_HashEntry; - state: Addon_TestProviderState
; - }, - components: { ListItem: typeof ListItem } - ) => ReactNode; + description?: (state: TestProviderConfig & Addon_TestProviderState) => ReactNode; + render?: (state: TestProviderConfig & Addon_TestProviderState) => ReactNode; + sidebarContextMenu?: (options: { + context: API_HashEntry; + state: Addon_TestProviderState
; + }) => ReactNode; mapStatusUpdate?: ( state: Addon_TestProviderState
) => API_StatusUpdate | ((state: API_StatusState) => API_StatusUpdate); @@ -493,21 +491,24 @@ export interface Addon_TestProviderType< watchable?: boolean; } -export type Addon_TestProviderState
> = - Pick & { - progress?: TestingModuleProgressReportProgress; - details: Details; - cancellable: boolean; - cancelling: boolean; - running: boolean; - watching: boolean; - failed: boolean; - crashed: boolean; - error?: { - name: string; - message?: string; - }; +export type Addon_TestProviderState< + Details extends { [key: string]: any } = NonNullable, + Config extends { [key: string]: any } = NonNullable, +> = Pick & { + progress?: TestingModuleProgressReportProgress; + details: Details; + cancellable: boolean; + cancelling: boolean; + running: boolean; + watching: boolean; + failed: boolean; + crashed: boolean; + error?: { + name: string; + message?: string; }; + config?: Config; +}; type Addon_TypeBaseNames = Exclude< Addon_TypesEnum, diff --git a/code/frameworks/experimental-nextjs-vite/package.json b/code/frameworks/experimental-nextjs-vite/package.json index 17b6bda9bfd3..12f77a09cdf4 100644 --- a/code/frameworks/experimental-nextjs-vite/package.json +++ b/code/frameworks/experimental-nextjs-vite/package.json @@ -112,7 +112,7 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "workspace:^", - "vite": "^5.0.0" + "vite": "^5.0.0 || ^6.0.0" }, "peerDependenciesMeta": { "typescript": { diff --git a/code/frameworks/react-native-web-vite/package.json b/code/frameworks/react-native-web-vite/package.json index 01c75647ee4b..a55da9586272 100644 --- a/code/frameworks/react-native-web-vite/package.json +++ b/code/frameworks/react-native-web-vite/package.json @@ -69,7 +69,7 @@ "react-native": ">=0.74.5", "react-native-web": "^0.19.12", "storybook": "workspace:^", - "vite": "^5.0.0" + "vite": "^5.0.0 || ^6.0.0" }, "engines": { "node": ">=18.0.0" diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json index 5aff664d1d31..a484c182b8b2 100644 --- a/code/frameworks/react-vite/package.json +++ b/code/frameworks/react-vite/package.json @@ -66,7 +66,7 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "workspace:^", - "vite": "^4.0.0 || ^5.0.0" + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" }, "engines": { "node": ">=18.0.0" diff --git a/code/frameworks/svelte-vite/package.json b/code/frameworks/svelte-vite/package.json index 35b31e47e780..6b0fbd677c0f 100644 --- a/code/frameworks/svelte-vite/package.json +++ b/code/frameworks/svelte-vite/package.json @@ -67,7 +67,7 @@ "@sveltejs/vite-plugin-svelte": "^2.0.0 || ^3.0.0 || ^4.0.0", "storybook": "workspace:^", "svelte": "^4.0.0 || ^5.0.0", - "vite": "^4.0.0 || ^5.0.0" + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" }, "engines": { "node": ">=18.0.0" diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index 81e1390582d2..4b82517808e6 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -71,7 +71,7 @@ "peerDependencies": { "storybook": "workspace:^", "svelte": "^4.0.0 || ^5.0.0", - "vite": "^4.0.0 || ^5.0.0" + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" }, "engines": { "node": ">=18.0.0" diff --git a/code/frameworks/vue3-vite/package.json b/code/frameworks/vue3-vite/package.json index 8f676ca4d931..d1deeb36b59b 100644 --- a/code/frameworks/vue3-vite/package.json +++ b/code/frameworks/vue3-vite/package.json @@ -68,7 +68,7 @@ }, "peerDependencies": { "storybook": "workspace:^", - "vite": "^4.0.0 || ^5.0.0" + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" }, "engines": { "node": ">=18.0.0" diff --git a/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts b/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts index aea3d269a1e3..f87f8b9792d5 100644 --- a/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts +++ b/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts @@ -3,7 +3,7 @@ import { dirname, join, parse, relative, resolve } from 'node:path'; import findPackageJson from 'find-package-json'; import MagicString from 'magic-string'; -import type { PluginOption } from 'vite'; +import type { ModuleNode, PluginOption } from 'vite'; import { type ComponentMeta, type MetaCheckerOptions, @@ -146,6 +146,20 @@ export async function vueComponentMeta(tsconfigPath = 'tsconfig.json'): Promise< return undefined; } }, + // handle hot updates to update the component meta on file changes + async handleHotUpdate({ file, read, server, modules, timestamp }) { + const content = await read(); + checker.updateFile(file, content); + // Invalidate modules manually + const invalidatedModules = new Set(); + + for (const mod of modules) { + server.moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true); + } + + server.ws.send({ type: 'full-reload' }); + return []; + }, }; } diff --git a/code/package.json b/code/package.json index 49d43900fb56..e572f403574a 100644 --- a/code/package.json +++ b/code/package.json @@ -293,5 +293,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.5.0-alpha.11" } diff --git a/code/yarn.lock b/code/yarn.lock index 107c541a4cdb..12d9bf6dcc86 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6172,7 +6172,7 @@ __metadata: vite: "npm:^4.0.4" peerDependencies: storybook: "workspace:^" - vite: ^4.0.0 || ^5.0.0 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 languageName: unknown linkType: soft @@ -6658,7 +6658,7 @@ __metadata: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta storybook: "workspace:^" - vite: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 dependenciesMeta: sharp: optional: true @@ -7112,7 +7112,7 @@ __metadata: react-native: ">=0.74.5" react-native-web: ^0.19.12 storybook: "workspace:^" - vite: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 languageName: unknown linkType: soft @@ -7136,7 +7136,7 @@ __metadata: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta storybook: "workspace:^" - vite: ^4.0.0 || ^5.0.0 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 languageName: unknown linkType: soft @@ -7433,7 +7433,7 @@ __metadata: "@sveltejs/vite-plugin-svelte": ^2.0.0 || ^3.0.0 || ^4.0.0 storybook: "workspace:^" svelte: ^4.0.0 || ^5.0.0 - vite: ^4.0.0 || ^5.0.0 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 languageName: unknown linkType: soft @@ -7492,7 +7492,7 @@ __metadata: peerDependencies: storybook: "workspace:^" svelte: ^4.0.0 || ^5.0.0 - vite: ^4.0.0 || ^5.0.0 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 languageName: unknown linkType: soft @@ -7587,7 +7587,7 @@ __metadata: vue-docgen-api: "npm:^4.75.1" peerDependencies: storybook: "workspace:^" - vite: ^4.0.0 || ^5.0.0 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 languageName: unknown linkType: soft diff --git a/docs/versions/next.json b/docs/versions/next.json index dd115c289b9e..5d1664859ce0 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.5.0-alpha.10","info":{"plain":"- Addon Test: Use pathe for better windows support - [#29676](https://github.com/storybookjs/storybook/pull/29676), thanks @yannbf!\n- Angular: Default to standalone components in Angular v19 - [#29677](https://github.com/storybookjs/storybook/pull/29677), thanks @ingowagner!"}} +{"version":"8.5.0-alpha.11","info":{"plain":"- Core + Addon Test: Refactor test API and fix total test count - [#29656](https://github.com/storybookjs/storybook/pull/29656), thanks @ghengeveld!\n- Core: Emit deprecated `TESTING_MODULE_RUN_ALL_REQUEST` for backward compatibility - [#29711](https://github.com/storybookjs/storybook/pull/29711), thanks @ghengeveld!\n- Frameworks: Add Vite 6 support - [#29710](https://github.com/storybookjs/storybook/pull/29710), thanks @yannbf!\n- TestAddon: Refactor UI & add config options - [#29662](https://github.com/storybookjs/storybook/pull/29662), thanks @ndelangen!\n- Vue: Fix `vue-component-meta` docgen HMR not working - [#29518](https://github.com/storybookjs/storybook/pull/29518), thanks @IonianPlayboy!"}} diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index 1adb524aa12a..72a362c86f12 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -109,12 +109,10 @@ export const install: Task['run'] = async ({ sandboxDir, key }, { link, dryRun, 'svelte-vite/default-ts', 'vue3-vite/default-js', 'vue3-vite/default-ts', - 'angular-cli/15-ts', - 'angular-cli/default-ts', - 'angular-cli/prerelease', + 'svelte-kit/skeleton-ts', ]; - if (sandboxesNeedingWorkarounds.includes(key)) { - await addWorkaroundResolutions({ cwd, dryRun, debug }); + if (sandboxesNeedingWorkarounds.includes(key) || key.includes('vite')) { + await addWorkaroundResolutions({ cwd, dryRun, debug, key }); } await exec( diff --git a/scripts/utils/yarn.ts b/scripts/utils/yarn.ts index 9e14f6ad9dce..505ddf3a78b4 100644 --- a/scripts/utils/yarn.ts +++ b/scripts/utils/yarn.ts @@ -67,7 +67,11 @@ export const installYarn2 = async ({ cwd, dryRun, debug }: YarnOptions) => { ); }; -export const addWorkaroundResolutions = async ({ cwd, dryRun }: YarnOptions) => { +export const addWorkaroundResolutions = async ({ + cwd, + dryRun, + key, +}: YarnOptions & { key?: TemplateKey }) => { logger.info(`🔢 Adding resolutions for workarounds`); if (dryRun) { @@ -81,12 +85,19 @@ export const addWorkaroundResolutions = async ({ cwd, dryRun }: YarnOptions) => // Due to our support of older vite versions '@vitejs/plugin-react': '4.2.0', '@vitejs/plugin-vue': '4.5.0', + // TODO: Remove this once we figure out how to properly test Vite 4, 5 and 6 in our sandboxes + vite: '^5.0.0', + // We need to downgrade the plugin so that it works with Vite 5 projects + '@sveltejs/vite-plugin-svelte': '4.0.2', '@testing-library/dom': '^9.3.4', '@testing-library/jest-dom': '^6.5.0', '@testing-library/user-event': '^14.5.2', - // TODO: Remove this once this issue is fixed https://github.com/thednp/position-observer/issues/1 - '@thednp/shorty': '2.0.7', }; + + if (key.includes('svelte-kit')) { + packageJson.resolutions['@sveltejs/vite-plugin-svelte'] = '^3.0.0'; + } + await writeJSON(packageJsonPath, packageJson, { spaces: 2 }); }; diff --git a/test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts b/test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts index 1882070794ac..2b60a9314d51 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts +++ b/test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts @@ -66,7 +66,7 @@ test.describe("component testing", () => { testStoryElement.click(); } - const testingModuleDescription = await page.locator('[data-module-id="storybook/test/test-provider"]').locator('#testing-module-description'); + const testingModuleDescription = await page.locator('#testing-module-description'); await expect(testingModuleDescription).toContainText('Not run'); @@ -126,7 +126,7 @@ test.describe("component testing", () => { await expect(page.locator('#testing-module-title')).toHaveText('Component tests'); - const testingModuleDescription = await page.locator('[data-module-id="storybook/test/test-provider"]').locator('#testing-module-description'); + const testingModuleDescription = await page.locator('#testing-module-description'); await expect(testingModuleDescription).toContainText('Not run'); @@ -136,8 +136,6 @@ test.describe("component testing", () => { await expect(watchModeButton).toBeEnabled(); await runTestsButton.click(); - - await expect(runTestsButton).toBeDisabled(); await expect(watchModeButton).toBeDisabled(); await expect(testingModuleDescription).toContainText('Testing');