diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 2be90903a97..cfca4a3569c 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,9 @@ +## 8.5.0-beta.7 + +- Addon Test: Context menu updates - [#30107](https://github.com/storybookjs/storybook/pull/30107), thanks @ghengeveld! +- Storysource Addon: Fix source-loader prettier imports - [#29669](https://github.com/storybookjs/storybook/pull/29669), thanks @slax57! +- Vue: Extend sourceDecorator to support v-bind and nested keys in slots - [#28787](https://github.com/storybookjs/storybook/pull/28787), thanks @JoCa96! + ## 8.5.0-beta.6 - Addon Test: Always use installed version of vitest - [#30134](https://github.com/storybookjs/storybook/pull/30134), thanks @kasperpeulen! diff --git a/code/addons/docs/docs/props-tables.md b/code/addons/docs/docs/props-tables.md index 5aeba6cbcd9..b367c47c25b 100644 --- a/code/addons/docs/docs/props-tables.md +++ b/code/addons/docs/docs/props-tables.md @@ -18,7 +18,7 @@ Storybook Docs automatically generates props tables for components in supported ## Usage -For framework-specific setup instructions, see the framework's README: [React](../react/README.md), [Vue3 ](../vue3/README.md), [Angular](../angular/README.md), [Web Components](../web-components/README.md), [Ember](../ember/README.md). +For framework-specific setup instructions, see the framework's README: [React](../react/README.md), [Vue3](../vue3/README.md), [Angular](../angular/README.md), [Web Components](../web-components/README.md), [Ember](../ember/README.md). ### DocsPage diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx deleted file mode 100644 index 229a396b538..00000000000 --- a/code/addons/test/src/components/ContextMenuItem.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { - type FC, - type SyntheticEvent, - useCallback, - useEffect, - useRef, - useState, -} from 'react'; - -import { Button, ListItem } from 'storybook/internal/components'; -import type { TestProviderConfig } from 'storybook/internal/core-events'; -import { useStorybookApi } from 'storybook/internal/manager-api'; -import { useTheme } from 'storybook/internal/theming'; -import { type API_HashEntry, type Addon_TestProviderState } from 'storybook/internal/types'; - -import { PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons'; - -import { type Config, type Details, TEST_PROVIDER_ID } from '../constants'; -import { Description } from './Description'; -import { Title } from './Title'; - -export const ContextMenuItem: FC<{ - context: API_HashEntry; - state: TestProviderConfig & Addon_TestProviderState; -}> = ({ context, state }) => { - const api = useStorybookApi(); - const [isDisabled, setDisabled] = useState(false); - - const id = useRef(context.id); - id.current = context.id; - - const Icon = state.running ? StopAltHollowIcon : PlayHollowIcon; - - useEffect(() => { - setDisabled(false); - }, [state.running]); - - const onClick = useCallback( - (event: SyntheticEvent) => { - setDisabled(true); - event.stopPropagation(); - if (state.running) { - api.cancelTestProvider(TEST_PROVIDER_ID); - } else { - api.runTestProvider(TEST_PROVIDER_ID, { entryId: id.current }); - } - }, - [api, state.running] - ); - - const theme = useTheme(); - - return ( -
{ - // stopPropagation to prevent the parent from closing the context menu, which is the default behavior onClick - event.stopPropagation(); - }} - > - } - center={} - right={ - - } - /> -
- ); -}; diff --git a/code/addons/test/src/components/Description.tsx b/code/addons/test/src/components/Description.tsx index 58a80dbfdcc..3779ab64193 100644 --- a/code/addons/test/src/components/Description.tsx +++ b/code/addons/test/src/components/Description.tsx @@ -4,6 +4,7 @@ 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 type { TestResultResult } from '../node/reporter'; import { GlobalErrorContext } from './GlobalErrorModal'; import { RelativeTime } from './RelativeTime'; @@ -19,11 +20,13 @@ const PositiveText = styled.span(({ theme }) => ({ color: theme.color.positiveText, })); -interface DescriptionProps extends ComponentProps { +interface DescriptionProps extends Omit, 'results'> { state: TestProviderConfig & TestProviderState; + entryId?: string; + results?: TestResultResult[]; } -export function Description({ state, ...props }: DescriptionProps) { +export function Description({ state, entryId, results, ...props }: DescriptionProps) { const isMounted = React.useRef(false); const [isUpdated, setUpdated] = React.useState(false); const { setModalOpen } = React.useContext(GlobalErrorContext); @@ -48,15 +51,17 @@ export function Description({ state, ...props }: DescriptionProps) { description = state.progress ? `Testing... ${state.progress.numPassedTests}/${state.progress.numTotalTests}` : 'Starting...'; + } else if (entryId && results?.length) { + description = `Ran ${results.length} ${results.length === 1 ? 'test' : 'tests'}`; } else if (state.failed && !errorMessage) { description = 'Failed'; } else if (state.crashed || (state.failed && errorMessage)) { - description = ( - <> - setModalOpen(true)}> - {state.error?.name || 'View full error'} - - + description = setModalOpen ? ( + setModalOpen(true)}> + {state.error?.name || 'View full error'} + + ) : ( + state.error?.name || 'Failed' ); } else if (state.progress?.finishedAt) { description = ( diff --git a/code/addons/test/src/components/TestProviderRender.stories.tsx b/code/addons/test/src/components/TestProviderRender.stories.tsx index 5111c1507a4..06bb133c560 100644 --- a/code/addons/test/src/components/TestProviderRender.stories.tsx +++ b/code/addons/test/src/components/TestProviderRender.stories.tsx @@ -233,7 +233,7 @@ export const Editing: Story = { play: async ({ canvasElement }) => { const screen = within(canvasElement); - screen.getByLabelText(/Open settings/).click(); + screen.getByLabelText(/Show settings/).click(); }, }; diff --git a/code/addons/test/src/components/TestProviderRender.tsx b/code/addons/test/src/components/TestProviderRender.tsx index 4b1f4c35bc4..ab8e61fadf0 100644 --- a/code/addons/test/src/components/TestProviderRender.tsx +++ b/code/addons/test/src/components/TestProviderRender.tsx @@ -1,6 +1,12 @@ import React, { type ComponentProps, type FC, useCallback, useMemo, useRef, useState } from 'react'; -import { Button, ListItem, ProgressSpinner } from 'storybook/internal/components'; +import { + Button, + ListItem, + ProgressSpinner, + TooltipNote, + WithTooltip, +} from 'storybook/internal/components'; import { TESTING_MODULE_CONFIG_CHANGE, type TestProviderConfig, @@ -31,7 +37,6 @@ import { type Config, type Details, PANEL_ID } from '../constants'; import { type TestStatus } from '../node/reporter'; import { Description } from './Description'; import { TestStatusIcon } from './TestStatusIcon'; -import { Title } from './Title'; const Container = styled.div({ display: 'flex', @@ -52,6 +57,12 @@ const Info = styled.div({ minWidth: 0, }); +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: 2, @@ -92,7 +103,7 @@ const statusMap: Record['statu warning: 'warning', passed: 'positive', skipped: 'unknown', - pending: 'unknown', + pending: 'pending', }; export const TestProviderRender: FC< @@ -185,11 +196,7 @@ export const TestProviderRender: FC< }) .sort((a, b) => statusOrder.indexOf(a.status) - statusOrder.indexOf(b.status)); - const status = state.running - ? 'unknown' - : state.failed - ? 'failed' - : (results[0]?.status ?? 'unknown'); + const status = results[0]?.status ?? (state.running ? 'pending' : 'unknown'); const openPanel = (id: string, panelId: string) => { api.selectStory(id); @@ -201,57 +208,90 @@ export const TestProviderRender: FC< - - <Description id="testing-module-description" state={state} /> + <Title id="testing-module-title" crashed={state.crashed}> + {state.crashed ? 'Local tests failed' : 'Run local tests'} + + - - {state.watchable && !entryId && ( - + + )} + {!entryId && state.watchable && ( + } > - - + + )} {state.runnable && ( <> {state.running && state.cancellable ? ( - + + ) : ( - + + )} )} @@ -266,19 +306,6 @@ export const TestProviderRender: FC< icon={} right={} /> - Coverage} - icon={} - right={ - updateConfig({ coverage: !config.coverage })} - /> - } - /> {isA11yAddon && ( )} + {!entryId && ( + Coverage} + icon={} + right={ + updateConfig({ coverage: !config.coverage })} + /> + } + /> + )} ) : ( @@ -320,34 +362,6 @@ export const TestProviderRender: FC< ) } /> - {coverageSummary ? ( - Coverage} - href={'/coverage/index.html'} - // @ts-expect-error ListItem doesn't include all anchor attributes in types, but it is an achor element - target="_blank" - aria-label="Open coverage report" - icon={ - - } - right={ - coverageSummary.percentage ? ( - - {coverageSummary.percentage} % - - ) : null - } - /> - ) : ( - Coverage} - icon={} - /> - )} {isA11yAddon && ( Accessibility {a11ySkippedLabel}} @@ -371,6 +385,38 @@ export const TestProviderRender: FC< right={isStoryEntry ? null : a11yNotPassedAmount || null} /> )} + {!entryId && ( + <> + {coverageSummary ? ( + Coverage} + href={'/coverage/index.html'} + // @ts-expect-error ListItem doesn't include all anchor attributes in types, but it is an achor element + target="_blank" + aria-label="Open coverage report" + icon={ + + } + right={ + coverageSummary.percentage ? ( + + {coverageSummary.percentage} % + + ) : null + } + /> + ) : ( + Coverage} + icon={} + /> + )} + + )} )} diff --git a/code/addons/test/src/components/TestStatusIcon.stories.tsx b/code/addons/test/src/components/TestStatusIcon.stories.tsx index 3a38df50a80..e46d22e015d 100644 --- a/code/addons/test/src/components/TestStatusIcon.stories.tsx +++ b/code/addons/test/src/components/TestStatusIcon.stories.tsx @@ -16,6 +16,12 @@ export const Unknown: Story = { }, }; +export const Pending: Story = { + args: { + status: 'pending', + }, +}; + export const Positive: Story = { args: { status: 'positive', diff --git a/code/addons/test/src/components/TestStatusIcon.tsx b/code/addons/test/src/components/TestStatusIcon.tsx index 7b201ce768c..536b603dbfb 100644 --- a/code/addons/test/src/components/TestStatusIcon.tsx +++ b/code/addons/test/src/components/TestStatusIcon.tsx @@ -1,7 +1,7 @@ import { styled } from 'storybook/internal/theming'; export const TestStatusIcon = styled.div<{ - status: 'positive' | 'warning' | 'negative' | 'critical' | 'unknown'; + status: 'pending' | 'positive' | 'warning' | 'negative' | 'critical' | 'unknown'; percentage?: number; }>( ({ percentage }) => ({ @@ -13,6 +13,12 @@ export const TestStatusIcon = styled.div<{ : 'var(--status-color)', borderRadius: '50%', }), + ({ status, theme }) => + status === 'pending' && { + animation: `${theme.animation.glow} 1.5s ease-in-out infinite`, + '--status-color': theme.color.mediumdark, + '--status-background': `${theme.color.mediumdark}66`, + }, ({ status, theme }) => status === 'positive' && { '--status-color': theme.color.positive, diff --git a/code/addons/test/src/components/Title.tsx b/code/addons/test/src/components/Title.tsx deleted file mode 100644 index fecb454cffd..00000000000 --- a/code/addons/test/src/components/Title.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, { type ComponentProps } from 'react'; - -import { type TestProviderConfig, type TestProviderState } from 'storybook/internal/core-events'; -import { styled } from 'storybook/internal/theming'; - -const Wrapper = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({ - fontSize: theme.typography.size.s1, - fontWeight: crashed ? 'bold' : 'normal', - color: crashed ? theme.color.negativeText : theme.color.defaultText, -})); - -export const Title = ({ - state, - ...props -}: { state: TestProviderConfig & TestProviderState } & ComponentProps) => { - return ( - - {state.crashed || state.failed ? 'Local tests failed' : 'Run local tests'} - - ); -}; diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index ac4aeb671d5..3392c7674ee 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -22,6 +22,7 @@ const empty = { const PositionedWithTooltip = styled(WithTooltip)({ position: 'absolute', right: 0, + zIndex: 1, }); const FloatingStatusButton = styled(StatusButton)({ diff --git a/code/core/src/manager/components/sidebar/LegacyRender.tsx b/code/core/src/manager/components/sidebar/LegacyRender.tsx index f8afa4317f7..22b151c968f 100644 --- a/code/core/src/manager/components/sidebar/LegacyRender.tsx +++ b/code/core/src/manager/components/sidebar/LegacyRender.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Button, ProgressSpinner } from '@storybook/core/components'; +import { Button, ProgressSpinner, TooltipNote, WithTooltip } from '@storybook/core/components'; import { styled } from '@storybook/core/theming'; import { EyeIcon, PlayHollowIcon, StopAltIcon } from '@storybook/icons'; @@ -61,46 +61,68 @@ export const LegacyRender = ({ ...state }: TestProviders[keyof TestProviders]) = {state.watchable && ( - + + )} {state.runnable && ( <> {state.running && state.cancellable ? ( - + + + + + ) : ( - + + )} )} diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx index 22cf6e68ef9..d8d0936d5f2 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx @@ -1,11 +1,10 @@ -import React, { type FC, Fragment, useEffect, useState } from 'react'; +import React, { type FC, useEffect, useState } from 'react'; import { Addon_TypesEnum } from '@storybook/core/types'; import type { Meta, StoryObj } from '@storybook/react'; -import { expect, fn, waitFor, within } from '@storybook/test'; +import { expect, fireEvent, 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'; @@ -156,18 +155,18 @@ export const DynamicHeight: StoryObj = { const screen = await within(canvasElement); const toggleButton = await screen.getByLabelText(/Expand/); - await userEvent.click(toggleButton); + await fireEvent.click(toggleButton); 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 fireEvent.click(toggleButton); await waitFor(() => expect(collapse.getBoundingClientRect()).toHaveProperty('height', 0)); - await userEvent.click(toggleButton); + await fireEvent.click(toggleButton); await waitFor(() => expect(collapse.getBoundingClientRect()).not.toHaveProperty('height', 0)); }, diff --git a/code/core/src/manager/components/sidebar/TestingModule.tsx b/code/core/src/manager/components/sidebar/TestingModule.tsx index b66512007c0..9fe95426e59 100644 --- a/code/core/src/manager/components/sidebar/TestingModule.tsx +++ b/code/core/src/manager/components/sidebar/TestingModule.tsx @@ -274,21 +274,31 @@ export const TestingModule = ({ )} {hasTestProviders && ( - + } + trigger="hover" > - - + + + + )} {errorCount > 0 && ( diff --git a/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-flow.js b/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-flow.js index 76529ba9643..220b09d87fd 100644 --- a/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-flow.js +++ b/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-flow.js @@ -1,4 +1,4 @@ -import parseFlow from 'prettier/plugins/flow'; +import * as parseFlow from 'prettier/plugins/flow'; function parse(source) { return parseFlow.parsers.flow.parse(source); diff --git a/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-js.js b/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-js.js index 28b34aca107..325c50d1a18 100644 --- a/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-js.js +++ b/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-js.js @@ -1,4 +1,4 @@ -import parseJs from 'prettier/plugins/babel'; +import * as parseJs from 'prettier/plugins/babel'; function parse(source) { try { diff --git a/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-ts.js b/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-ts.js index 71e2870f59c..22ff9af14a1 100644 --- a/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-ts.js +++ b/code/lib/source-loader/src/abstract-syntax-tree/parsers/parser-ts.js @@ -1,4 +1,4 @@ -import parseTs from 'prettier/plugins/typescript'; +import * as parseTs from 'prettier/plugins/typescript'; function parse(source) { try { diff --git a/code/package.json b/code/package.json index a8d93c4468a..57c42fd1fc5 100644 --- a/code/package.json +++ b/code/package.json @@ -294,5 +294,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.5.0-beta.7" } diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 1cd1ee9a2e0..f0a926b0a1e 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -152,16 +152,20 @@ test('should generate source code for slots with bindings', () => { type TestBindings = { foo: string; bar?: number; + boo: { + mimeType: string; + }; }; const slots = { - a: ({ foo, bar }: TestBindings) => `Slot with bindings ${foo} and ${bar}`, - b: ({ foo }: TestBindings) => h('a', { href: foo, target: foo }, `Test link: ${foo}`), + a: ({ foo, bar, boo }: TestBindings) => `Slot with bindings ${foo}, ${bar} and ${boo.mimeType}`, + b: ({ foo, boo }: TestBindings) => + h('a', { href: foo, target: foo, type: boo.mimeType, ...boo }, `Test link: ${foo}`), }; - const expectedCode = ` + const expectedCode = ` -`; +`; const actualCode = generateSlotSourceCode(slots, Object.keys(slots), { imports: {}, diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index cc2f922f63f..c3265394ea9 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -26,6 +26,20 @@ export type SourceCodeGeneratorContext = { imports: Record>; }; +/** + * Used to get the tracking data from the proxy. A symbol is unique, so when using it as a key it + * can't be accidentally accessed. + */ +const TRACKING_SYMBOL = Symbol('DEEP_ACCESS_SYMBOL'); + +type TrackingProxy = { + [TRACKING_SYMBOL]: true; + toString: () => string; +}; + +const isProxy = (obj: unknown): obj is TrackingProxy => + !!(obj && typeof obj === 'object' && TRACKING_SYMBOL in obj); + /** Decorator to generate Vue source code for stories. */ export const sourceDecorator: Decorator = (storyFn, ctx) => { const story = storyFn(); @@ -226,6 +240,10 @@ export const generatePropsSourceCode = ( return; } // do not render undefined/null values // do not render undefined/null values + if (isProxy(value)) { + value = value.toString(); + } + switch (typeof value) { case 'string': if (value === '') { @@ -269,7 +287,7 @@ export const generatePropsSourceCode = ( case 'object': { properties.push({ name: propName, - value: formatObject(value), + value: formatObject(value ?? {}), // to follow Vue best practices, complex values like object and arrays are // usually placed inside the