Merge pull request #29725 from storybookjs/version-non-patch-from-8.5.0-alpha.11

Release: Prerelease 8.5.0-alpha.12
This commit is contained in:
Yann Braga 2024-11-28 17:21:33 +01:00 committed by GitHub
commit 021d6bf660
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 522 additions and 192 deletions

View File

@ -1,3 +1,10 @@
## 8.5.0-alpha.12
- Core / Addon Test: Add config UI to Testing Module - [#29708](https://github.com/storybookjs/storybook/pull/29708), thanks @ghengeveld!
- Manager: Add tags property to GroupEntry objects - [#29672](https://github.com/storybookjs/storybook/pull/29672), thanks @Sidnioulz!
- Svelte: Support `@sveltejs/vite-plugin-svelte` v5 - [#29731](https://github.com/storybookjs/storybook/pull/29731), thanks @JReinhold!
- Toolbars: Suppress deprecation warning when using dynamic icons - [#29545](https://github.com/storybookjs/storybook/pull/29545), thanks @ValeraS!
## 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!

View File

@ -23,7 +23,7 @@ export function GuidedTour({
const theme = useTheme();
useEffect(() => {
let timeout: NodeJS.Timeout;
let timeout: ReturnType<typeof setTimeout>;
setStepIndex((current) => {
const index = steps.findIndex(({ key }) => key === step);

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { Link as LinkComponent } from 'storybook/internal/components';
import { type TestProviderConfig, type TestProviderState } from 'storybook/internal/core-events';
@ -11,6 +11,10 @@ export const DescriptionStyle = styled.div(({ theme }) => ({
color: theme.barTextColor,
}));
const PositiveText = styled.span(({ theme }) => ({
color: theme.color.positiveText,
}));
export function Description({
errorMessage,
setIsModalOpen,
@ -20,9 +24,24 @@ export function Description({
errorMessage: string;
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
}) {
let description: string | React.ReactNode = 'Not run';
const isMounted = React.useRef(false);
const [isUpdated, setUpdated] = React.useState(false);
if (state.running) {
useEffect(() => {
if (isMounted.current) {
setUpdated(true);
const timeout = setTimeout(setUpdated, 2000, false);
return () => {
clearTimeout(timeout);
};
}
isMounted.current = true;
}, [state.config]);
let description: string | React.ReactNode = 'Not run';
if (isUpdated) {
description = <PositiveText>Settings updated</PositiveText>;
} else if (state.running) {
description = state.progress
? `Testing... ${state.progress.numPassedTests}/${state.progress.numTotalTests}`
: 'Starting...';

View File

@ -23,7 +23,9 @@ const managerContext: any = {
},
},
api: {
getDocsUrl: fn().mockName('api::getDocsUrl'),
getDocsUrl: fn(({ subpath }) => `https://storybook.js.org/docs/${subpath}`).mockName(
'api::getDocsUrl'
),
emit: fn().mockName('api::emit'),
updateTestProviderState: fn().mockName('api::updateTestProviderState'),
},
@ -98,6 +100,9 @@ export default {
</ManagerContext.Provider>
),
],
parameters: {
layout: 'fullscreen',
},
} as Meta<typeof TestProviderRender>;
export const Default: Story = {
@ -153,6 +158,6 @@ export const EnableEditing: Story = {
play: async ({ canvasElement }) => {
const screen = within(canvasElement);
screen.getByLabelText('Edit').click();
screen.getByLabelText(/Open settings/).click();
},
};

View File

@ -1,20 +1,43 @@
import React, { type FC, Fragment, useCallback, useRef, useState } from 'react';
import React, { type FC, useCallback, useRef, useState } from 'react';
import { Button } from 'storybook/internal/components';
import { Button, ListItem } 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 { styled, useTheme } from 'storybook/internal/theming';
import { EditIcon, EyeIcon, PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons';
import {
AccessibilityIcon,
EditIcon,
EyeIcon,
PlayHollowIcon,
PointerHandIcon,
ShieldIcon,
StopAltHollowIcon,
} from '@storybook/icons';
import { isEqual } from 'es-toolkit';
import { debounce } from 'es-toolkit/compat';
import { type Config, type Details, TEST_PROVIDER_ID } from '../constants';
import { Description } from './Description';
import { GlobalErrorModal } from './GlobalErrorModal';
import { TestStatusIcon } from './TestStatusIcon';
const Container = styled.div({
display: 'flex',
flexDirection: 'column',
});
const Heading = styled.div({
display: 'flex',
justifyContent: 'space-between',
padding: '8px 2px',
gap: 6,
});
const Info = styled.div({
display: 'flex',
@ -33,32 +56,37 @@ const Actions = styled.div({
gap: 6,
});
const Head = styled.div({
display: 'flex',
justifyContent: 'space-between',
gap: 6,
const Extras = styled.div({
marginBottom: 2,
});
const Checkbox = styled.input({
margin: 0,
'&:enabled': {
cursor: 'pointer',
},
});
export const TestProviderRender: FC<{
api: API;
state: TestProviderConfig & TestProviderState<Details, Config>;
}> = ({ state, api }) => {
const [isEditing, setIsEditing] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const theme = useTheme();
const title = state.crashed || state.failed ? 'Component tests failed' : 'Component tests';
const title = state.crashed || state.failed ? 'Local tests failed' : 'Run local tests';
const errorMessage = state.error?.message;
const [config, changeConfig] = useConfig(
const [config, updateConfig] = useConfig(
api,
state.id,
state.config || { a11y: false, coverage: false },
api
state.config || { a11y: false, coverage: false }
);
const [isEditing, setIsEditing] = useState(false);
return (
<Fragment>
<Head>
<Container>
<Heading>
<Info>
<Title crashed={state.crashed} id="testing-module-title">
{title}
@ -68,11 +96,11 @@ export const TestProviderRender: FC<{
<Actions>
<Button
aria-label={`Edit`}
aria-label={`${isEditing ? 'Close' : 'Open'} settings for ${state.name}`}
variant="ghost"
padding="small"
active={isEditing}
onClick={() => setIsEditing((v) => !v)}
onClick={() => setIsEditing(!isEditing)}
>
<EditIcon />
</Button>
@ -105,7 +133,7 @@ export const TestProviderRender: FC<{
aria-label={`Start ${state.name}`}
variant="ghost"
padding="small"
onClick={() => api.runTestProvider(state.id, {})}
onClick={() => api.runTestProvider(state.id)}
disabled={state.crashed || state.running}
>
<PlayHollowIcon />
@ -114,29 +142,60 @@ export const TestProviderRender: FC<{
</>
)}
</Actions>
</Head>
</Heading>
{!isEditing ? (
<Fragment>
{Object.entries(config).map(([key, value]) => (
<div key={key}>
{key}: {value ? 'ON' : 'OFF'}
</div>
))}
</Fragment>
{isEditing ? (
<Extras>
<ListItem
as="label"
title="Component tests"
icon={<PointerHandIcon color={theme.textMutedColor} />}
right={<Checkbox type="checkbox" checked disabled />}
/>
<ListItem
as="label"
title="Coverage"
icon={<ShieldIcon color={theme.textMutedColor} />}
right={
<Checkbox
type="checkbox"
disabled // TODO: Implement coverage
checked={config.coverage}
onChange={() => updateConfig({ coverage: !config.coverage })}
/>
}
/>
<ListItem
as="label"
title="Accessibility"
icon={<AccessibilityIcon color={theme.textMutedColor} />}
right={
<Checkbox
type="checkbox"
disabled // TODO: Implement a11y
checked={config.a11y}
onChange={() => updateConfig({ a11y: !config.a11y })}
/>
}
/>
</Extras>
) : (
<Fragment>
{Object.entries(config).map(([key, value]) => (
<div
key={key}
onClick={() => {
changeConfig({ [key]: !value });
}}
>
{key}: {value ? 'ON' : 'OFF'}
</div>
))}
</Fragment>
<Extras>
<ListItem
title="Component tests"
icon={<TestStatusIcon status="positive" aria-label="status: passed" />}
/>
<ListItem
title="Coverage"
icon={<TestStatusIcon percentage={60} status="warning" aria-label="status: warning" />}
right={`60%`}
/>
<ListItem
title="Accessibility"
icon={<TestStatusIcon status="negative" aria-label="status: failed" />}
right={73}
/>
</Extras>
)}
<GlobalErrorModal
@ -150,33 +209,35 @@ export const TestProviderRender: FC<{
api.runTestProvider(TEST_PROVIDER_ID);
}}
/>
</Fragment>
</Container>
);
};
function useConfig(id: string, config: Config, api: API) {
const data = useRef<Config>(config);
data.current = config || {
a11y: false,
coverage: false,
};
function useConfig(api: API, providerId: string, initialConfig: Config) {
const [currentConfig, setConfig] = useState<Config>(initialConfig);
const lastConfig = useRef(initialConfig);
const changeConfig = useCallback(
(update: Partial<Config>) => {
const newConfig = {
...data.current,
...update,
};
api.updateTestProviderState(id, {
config: newConfig,
});
api.emit(TESTING_MODULE_CONFIG_CHANGE, {
providerId: id,
config: newConfig,
} as TestingModuleConfigChangePayload);
},
[api, id]
const saveConfig = useCallback(
debounce((config: Config) => {
if (!isEqual(config, lastConfig.current)) {
api.updateTestProviderState(providerId, { config });
api.emit(TESTING_MODULE_CONFIG_CHANGE, { providerId, config });
lastConfig.current = config;
}
}, 500),
[api, providerId]
);
return [data.current, changeConfig] as const;
const updateConfig = useCallback(
(update: Partial<Config>) => {
setConfig((value) => {
const updated = { ...value, ...update };
saveConfig(updated);
return updated;
});
},
[saveConfig]
);
return [currentConfig, updateConfig] as const;
}

View File

@ -0,0 +1,63 @@
import type { Meta, StoryObj } from '@storybook/react';
import { TestStatusIcon } from './TestStatusIcon';
const meta = {
component: TestStatusIcon,
} satisfies Meta<typeof TestStatusIcon>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Unknown: Story = {
args: {
status: 'unknown',
},
};
export const Positive: Story = {
args: {
status: 'positive',
},
};
export const Warning: Story = {
args: {
status: 'warning',
},
};
export const Negative: Story = {
args: {
status: 'negative',
},
};
export const UnknownPercentage: Story = {
args: {
status: 'unknown',
percentage: 50,
},
};
export const PositivePercentage: Story = {
args: {
status: 'positive',
percentage: 60,
},
};
export const WarningPercentage: Story = {
args: {
status: 'warning',
percentage: 40,
},
};
export const NegativePercentage: Story = {
args: {
status: 'negative',
percentage: 30,
},
};

View File

@ -0,0 +1,36 @@
import { styled } from 'storybook/internal/theming';
export const TestStatusIcon = styled.div<{
status: 'positive' | 'warning' | 'negative' | 'unknown';
percentage?: number;
}>(
({ percentage }) => ({
width: percentage ? 12 : 6,
height: percentage ? 12 : 6,
margin: percentage ? 1 : 4,
background: percentage
? `conic-gradient(var(--status-color) ${percentage}%, var(--status-background) ${percentage + 1}%)`
: 'var(--status-color)',
borderRadius: '50%',
}),
({ status, theme }) =>
status === 'positive' && {
'--status-color': theme.color.positive,
'--status-background': `${theme.color.positive}66`,
},
({ status, theme }) =>
status === 'warning' && {
'--status-color': theme.color.gold,
'--status-background': `${theme.color.gold}66`,
},
({ status, theme }) =>
status === 'negative' && {
'--status-color': theme.color.negative,
'--status-background': `${theme.color.negative}66`,
},
({ status, theme }) =>
status === 'unknown' && {
'--status-color': theme.color.mediumdark,
'--status-background': `${theme.color.mediumdark}66`,
}
);

View File

@ -35,6 +35,7 @@ addons.register(ADDON_ID, (api) => {
runnable: true,
watchable: true,
name: 'Component tests',
render: (state) => <TestProviderRender api={api} state={state} />,
sidebarContextMenu: ({ context, state }) => {
if (context.type === 'docs') {
@ -47,8 +48,6 @@ addons.register(ADDON_ID, (api) => {
return <ContextMenuItem context={context} state={state} />;
},
render: (state) => <TestProviderRender api={api} state={state} />,
mapStatusUpdate: (state) =>
Object.fromEntries(
(state.details.testResults || []).flatMap((testResult) =>

View File

@ -21,7 +21,9 @@ export const ToolbarMenuListItem = ({
disabled,
currentValue,
}: ToolbarMenuListItemProps) => {
const Icon = icon && <Icons style={{ opacity: 1 }} icon={icon} />;
const Icon = icon && (
<Icons style={{ opacity: 1 }} icon={icon} __suppressDeprecationWarning={true} />
);
const Item: TooltipLinkListLink = {
id: value ?? '_reset',

View File

@ -150,6 +150,15 @@ const Item = styled.div<ItemProps>(
opacity: 1,
},
},
({ theme, as }) =>
as === 'label' && {
'&:has(input:not(:disabled))': {
cursor: 'pointer',
'&:hover': {
background: theme.background.hoverable,
},
},
},
({ disabled }) => disabled && { cursor: 'not-allowed' }
);

View File

@ -1,6 +1,6 @@
export default <T>(a: T[], b: T[]): T[] => {
// no point in intersecting if one of the input is ill-defined
if (!a || !b) {
if (!Array.isArray(a) || !Array.isArray(b) || !a.length || !b.length) {
return [];
}

View File

@ -1,4 +1,5 @@
import type {
API_BaseEntry,
API_ComponentEntry,
API_DocsEntry,
API_GroupEntry,
@ -16,6 +17,7 @@ import type {
StoryId,
StoryIndexV2,
StoryIndexV3,
Tag,
} from '@storybook/core/types';
import { sanitize } from '@storybook/csf';
@ -248,6 +250,7 @@ export const transformStoryIndexToStoriesHash = (
type: 'root',
id,
name: names[idx],
tags: [],
depth: idx,
renderLabel,
startCollapsed: collapsedRoots.includes(id),
@ -267,6 +270,7 @@ export const transformStoryIndexToStoriesHash = (
type: 'component',
id,
name: names[idx],
tags: [],
parent: paths[idx - 1],
depth: idx,
renderLabel,
@ -274,14 +278,12 @@ export const transformStoryIndexToStoriesHash = (
children: [childId],
}),
});
// merge computes a union of arrays but we want an intersection on this
// specific array property, so it's easier to add it after the merge.
acc[id].tags = intersect(acc[id]?.tags ?? item.tags, item.tags);
} else {
acc[id] = merge<API_GroupEntry>((acc[id] || {}) as API_GroupEntry, {
type: 'group',
id,
name: names[idx],
tags: [],
parent: paths[idx - 1],
depth: idx,
renderLabel,
@ -295,6 +297,7 @@ export const transformStoryIndexToStoriesHash = (
// Finally add an entry for the docs/story itself
acc[item.id] = {
type: 'story',
tags: [],
...item,
depth: paths.length,
parent: paths[paths.length - 1],
@ -313,9 +316,18 @@ export const transformStoryIndexToStoriesHash = (
}
acc[item.id] = item;
// Ensure we add the children depth-first *before* inserting any other entries
// Ensure we add the children depth-first *before* inserting any other entries,
// and compute tags from the children put in the accumulator afterwards, once
// they're all known and we can compute a sound intersection.
if (item.type === 'root' || item.type === 'group' || item.type === 'component') {
item.children.forEach((childId: any) => addItem(acc, storiesHashOutOfOrder[childId]));
item.tags = item.children.reduce((currentTags: Tag[] | null, childId: any): Tag[] => {
const child = acc[childId];
// On the first child, we have nothing to intersect against so we use it as a source of data.
return currentTags === null ? child.tags : intersect(currentTags, child.tags);
}, null);
}
return acc;
}

View File

@ -21,7 +21,6 @@ export type SubState = {
};
const initialTestProviderState: TestProviderState = {
config: {} as { [key: string]: any },
details: {} as { [key: string]: any },
cancellable: false,
cancelling: false,

View File

@ -162,6 +162,7 @@ describe('stories API', () => {
expect(index!['design-system']).toMatchObject({
type: 'root',
name: 'Design System', // root name originates from `kind`, so it gets trimmed
tags: [],
});
expect(index!['design-system-some-component']).toMatchObject({
type: 'component',
@ -186,6 +187,7 @@ describe('stories API', () => {
title: 'Root/First',
name: 'Story 1',
importPath: './path/to/root/first.ts',
tags: [],
},
...mockEntries,
},
@ -207,6 +209,7 @@ describe('stories API', () => {
type: 'root',
id: 'root',
children: ['root-first'],
tags: [],
});
});
it('sets roots when showRoots = true', () => {
@ -222,6 +225,7 @@ describe('stories API', () => {
id: 'a-b--1',
title: 'a/b',
name: '1',
tags: [],
importPath: './a/b.ts',
},
},
@ -233,6 +237,7 @@ describe('stories API', () => {
type: 'root',
id: 'a',
children: ['a-b'],
tags: [],
});
expect(index!['a-b']).toMatchObject({
type: 'component',
@ -332,6 +337,76 @@ describe('stories API', () => {
tags: ['shared', 'two-specific'],
});
});
it('intersects story/docs tags to compute tags for root and group entries', () => {
const moduleArgs = createMockModuleArgs({});
const { api } = initStories(moduleArgs as unknown as ModuleArgs);
const { store } = moduleArgs;
api.setIndex({
v: 5,
entries: {
'a-sampleone': {
type: 'story',
id: 'a-sampleone',
title: 'A/SampleOne',
name: '1',
tags: ['shared', 'one-specific'],
importPath: './a.ts',
},
'a-sampletwo': {
type: 'story',
id: 'a-sampletwo',
title: 'A/SampleTwo',
name: '2',
tags: ['shared', 'two-specific'],
importPath: './a.ts',
},
'a-embedded-othertopic': {
type: 'docs',
id: 'a-embedded-othertopic',
title: 'A/Embedded/OtherTopic',
name: '3',
tags: ['shared', 'embedded-docs-specific', 'other'],
storiesImports: [],
importPath: './embedded/other.mdx',
},
'a-embedded-extras': {
type: 'docs',
id: 'a-embedded-extras',
title: 'A/Embedded/Extras',
name: '3',
tags: ['shared', 'embedded-docs-specific', 'extras'],
storiesImports: [],
importPath: './embedded/extras.mdx',
},
},
});
const { index } = store.getState();
// We need exact key ordering, even if in theory JS doesn't guarantee it
expect(Object.keys(index!)).toEqual([
'a',
'a-sampleone',
'a-sampletwo',
'a-embedded',
'a-embedded-othertopic',
'a-embedded-extras',
]);
// Acts as the root, so that the next level is a group we're testing.
expect(index!.a).toMatchObject({
type: 'root',
id: 'a',
children: ['a-sampleone', 'a-sampletwo', 'a-embedded'],
tags: ['shared'],
});
// The object of this test.
expect(index!['a-embedded']).toMatchObject({
type: 'group',
id: 'a-embedded',
parent: 'a',
name: 'Embedded',
tags: ['shared', 'embedded-docs-specific'],
});
});
// Stories can get out of order for a few reasons -- see reproductions on
// https://github.com/storybookjs/storybook/issues/5518
it('does the right thing for out of order stories', async () => {
@ -1515,6 +1590,7 @@ describe('stories API', () => {
"parent": "a",
"prepared": false,
"renderLabel": undefined,
"tags": [],
"title": "a",
"type": "story",
},
@ -1526,6 +1602,7 @@ describe('stories API', () => {
"parent": "a",
"prepared": false,
"renderLabel": undefined,
"tags": [],
"title": "a",
"type": "story",
},
@ -1581,6 +1658,7 @@ describe('stories API', () => {
"parent": "a",
"prepared": false,
"renderLabel": undefined,
"tags": [],
"title": "a",
"type": "story",
},
@ -1623,6 +1701,7 @@ describe('stories API', () => {
"parent": "a",
"prepared": false,
"renderLabel": undefined,
"tags": [],
"title": "a",
"type": "story",
},
@ -1634,6 +1713,7 @@ describe('stories API', () => {
"parent": "a",
"prepared": false,
"renderLabel": undefined,
"tags": [],
"title": "a",
"type": "story",
},

View File

@ -7,6 +7,12 @@ import { EyeIcon, PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons';
import type { TestProviders } from '@storybook/core/core-events';
import { useStorybookApi } from '@storybook/core/manager-api';
const Container = styled.div({
display: 'flex',
justifyContent: 'space-between',
padding: '8px 2px',
});
const Info = styled.div({
display: 'flex',
flexDirection: 'column',
@ -35,7 +41,7 @@ export const LegacyRender = ({ ...state }: TestProviders[keyof TestProviders]) =
const api = useStorybookApi();
return (
<>
<Container>
<Info>
<TitleWrapper crashed={state.crashed} id="testing-module-title">
<Title {...state} />
@ -84,6 +90,6 @@ export const LegacyRender = ({ ...state }: TestProviders[keyof TestProviders]) =
</>
)}
</Actions>
</>
</Container>
);
};

View File

@ -155,7 +155,9 @@ export const DynamicHeight: StoryObj = {
play: async ({ canvasElement }) => {
const screen = await within(canvasElement);
const toggleButton = await screen.getByLabelText('Collapse testing module');
const toggleButton = await screen.getByLabelText(/Expand/);
await userEvent.click(toggleButton);
const content = await screen.findByText('CUSTOM CONTENT WITH DYNAMIC HEIGHT');
const collapse = await screen.getByTestId('collapse');

View File

@ -1,14 +1,21 @@
import React from 'react';
import type { Listener } from '@storybook/core/channels';
import { styled } from '@storybook/core/theming';
import { Addon_TypesEnum } from '@storybook/core/types';
import type { Meta, StoryObj } from '@storybook/react';
import { fn, userEvent } from '@storybook/test';
import { fireEvent, fn } from '@storybook/test';
import type { TestProviders } from '@storybook/core/core-events';
import { ManagerContext } from '@storybook/core/manager-api';
import { TESTING_MODULE_CONFIG_CHANGE, type TestProviders } from '@storybook/core/core-events';
import { ManagerContext, mockChannel } from '@storybook/core/manager-api';
import { TestingModule } from './TestingModule';
const TestProvider = styled.div({
padding: 8,
fontSize: 12,
});
const baseState = {
details: {},
cancellable: false,
@ -24,13 +31,8 @@ const testProviders: TestProviders[keyof TestProviders][] = [
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
id: 'component-tests',
name: 'Component tests',
render: () => (
<>
Component tests
<br />
Ran 2 seconds ago
</>
),
title: () => 'Component tests',
description: () => 'Ran 2 seconds ago',
runnable: true,
watchable: true,
...baseState,
@ -39,13 +41,8 @@ const testProviders: TestProviders[keyof TestProviders][] = [
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
id: 'visual-tests',
name: 'Visual tests',
render: () => (
<>
Visual tests
<br />
Not run
</>
),
title: () => 'Visual tests',
description: () => 'Not run',
runnable: true,
...baseState,
},
@ -53,20 +50,23 @@ const testProviders: TestProviders[keyof TestProviders][] = [
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
id: 'linting',
name: 'Linting',
render: () => (
<>
Linting
<br />
Watching for changes
</>
),
render: () => <TestProvider>Custom render function</TestProvider>,
...baseState,
watching: true,
},
];
let triggerUpdate: () => void;
const channel = mockChannel();
const managerContext: any = {
api: {
on: (eventName: string, listener: Listener) => {
if (eventName === TESTING_MODULE_CONFIG_CHANGE) {
triggerUpdate = listener;
}
return channel.on(eventName, listener);
},
off: (eventName: string, listener: Listener) => channel.off(eventName, listener),
runTestProvider: fn().mockName('api::runTestProvider'),
cancelTestProvider: fn().mockName('api::cancelTestProvider'),
updateTestProviderState: fn().mockName('api::updateTestProviderState'),
@ -104,10 +104,11 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Collapsed: Story = {
export const Expanded: Story = {
play: async ({ canvas }) => {
const button = await canvas.findByRole('button', { name: /Collapse/ });
await userEvent.click(button);
const button = await canvas.findByRole('button', { name: /Expand/ });
await fireEvent.click(button);
await new Promise((resolve) => setTimeout(resolve, 500));
},
};
@ -116,6 +117,7 @@ export const Statuses: Story = {
errorCount: 14,
warningCount: 42,
},
play: Expanded.play,
};
export const ErrorsActive: Story = {
@ -123,6 +125,7 @@ export const ErrorsActive: Story = {
...Statuses.args,
errorsActive: true,
},
play: Expanded.play,
};
export const WarningsActive: Story = {
@ -130,6 +133,7 @@ export const WarningsActive: Story = {
...Statuses.args,
warningsActive: true,
},
play: Expanded.play,
};
export const BothActive: Story = {
@ -138,28 +142,29 @@ export const BothActive: Story = {
errorsActive: true,
warningsActive: true,
},
play: Expanded.play,
};
export const CollapsedStatuses: Story = {
args: Statuses.args,
play: Collapsed.play,
};
export const Running: Story = {
args: {
testProviders: [{ ...testProviders[0], running: true }, ...testProviders.slice(1)],
},
play: Expanded.play,
};
export const RunningAll: Story = {
args: {
testProviders: testProviders.map((tp) => ({ ...tp, running: !!tp.runnable })),
},
play: Expanded.play,
};
export const CollapsedRunning: Story = {
args: RunningAll.args,
play: Collapsed.play,
};
export const Cancellable: Story = {
@ -169,6 +174,7 @@ export const Cancellable: Story = {
...testProviders.slice(1),
],
},
play: Expanded.play,
};
export const Cancelling: Story = {
@ -178,12 +184,14 @@ export const Cancelling: Story = {
...testProviders.slice(1),
],
},
play: Expanded.play,
};
export const Watching: Story = {
args: {
testProviders: [{ ...testProviders[0], watching: true }, ...testProviders.slice(1)],
},
play: Expanded.play,
};
export const Failing: Story = {
@ -193,12 +201,14 @@ export const Failing: Story = {
...testProviders.slice(1),
],
},
play: Expanded.play,
};
export const Failed: Story = {
args: {
testProviders: [{ ...testProviders[0], failed: true }, ...testProviders.slice(1)],
},
play: Expanded.play,
};
export const Crashed: Story = {
@ -207,17 +217,26 @@ export const Crashed: Story = {
{
...testProviders[0],
render: () => (
<>
<TestProvider>
Component tests didn't complete
<br />
Problems!
</>
</TestProvider>
),
crashed: true,
},
...testProviders.slice(1),
],
},
play: Expanded.play,
};
export const Updated: Story = {
args: {},
play: async (context) => {
await Expanded.play!(context);
triggerUpdate?.();
},
};
export const NoTestProvider: Story = {

View File

@ -1,18 +1,11 @@
import React, {
Fragment,
type SyntheticEvent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import React, { type SyntheticEvent, useCallback, useEffect, useRef, useState } from 'react';
import { Button, TooltipNote } from '@storybook/core/components';
import { WithTooltip } from '@storybook/core/components';
import { keyframes, styled } from '@storybook/core/theming';
import { ChevronSmallUpIcon, PlayAllHollowIcon } from '@storybook/icons';
import type { TestProviders } from '@storybook/core/core-events';
import { TESTING_MODULE_CONFIG_CHANGE, type TestProviders } from '@storybook/core/core-events';
import { useStorybookApi } from '@storybook/core/manager-api';
import { LegacyRender } from './LegacyRender';
@ -29,42 +22,42 @@ const spin = keyframes({
'100%': { transform: 'rotate(360deg)' },
});
const Outline = styled.div<{ crashed: boolean; failed: boolean; running: boolean }>(
({ crashed, running, theme, failed }) => ({
position: 'relative',
lineHeight: '20px',
width: '100%',
padding: 1,
overflow: 'hidden',
background: `var(--sb-sidebar-bottom-card-background, ${theme.background.content})`,
borderRadius:
`var(--sb-sidebar-bottom-card-border-radius, ${theme.appBorderRadius + 1}px)` as any,
boxShadow: `inset 0 0 0 1px ${crashed && !running ? theme.color.negative : theme.appBorderColor}, var(--sb-sidebar-bottom-card-box-shadow, 0 1px 2px 0 rgba(0, 0, 0, 0.05), 0px -5px 20px 10px ${theme.background.app})`,
transitionProperty:
'color, background-color, border-color, text-decoration-color, fill, stroke',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '0.15s',
const Outline = styled.div<{
crashed: boolean;
failed: boolean;
running: boolean;
updated: boolean;
}>(({ crashed, failed, running, theme, updated }) => ({
position: 'relative',
lineHeight: '20px',
width: '100%',
padding: 1,
overflow: 'hidden',
backgroundColor: `var(--sb-sidebar-bottom-card-background, ${theme.background.content})`,
borderRadius:
`var(--sb-sidebar-bottom-card-border-radius, ${theme.appBorderRadius + 1}px)` as any,
boxShadow: `inset 0 0 0 1px ${crashed && !running ? theme.color.negative : updated ? theme.color.positive : theme.appBorderColor}, var(--sb-sidebar-bottom-card-box-shadow, 0 1px 2px 0 rgba(0, 0, 0, 0.05), 0px -5px 20px 10px ${theme.background.app})`,
transition: 'box-shadow 1s',
'&:after': {
content: '""',
display: running ? 'block' : 'none',
position: 'absolute',
left: '50%',
top: '50%',
marginLeft: 'calc(max(100vw, 100vh) * -0.5)',
marginTop: 'calc(max(100vw, 100vh) * -0.5)',
height: 'max(100vw, 100vh)',
width: 'max(100vw, 100vh)',
animation: `${spin} 3s linear infinite`,
background: failed
? // Hardcoded colors to prevent themes from messing with them (orange+gold, secondary+seafoam)
`conic-gradient(transparent 90deg, #FC521F 150deg, #FFAE00 210deg, transparent 270deg)`
: `conic-gradient(transparent 90deg, #029CFD 150deg, #37D5D3 210deg, transparent 270deg)`,
opacity: 1,
willChange: 'auto',
},
})
);
'&:after': {
content: '""',
display: running ? 'block' : 'none',
position: 'absolute',
left: '50%',
top: '50%',
marginLeft: 'calc(max(100vw, 100vh) * -0.5)',
marginTop: 'calc(max(100vw, 100vh) * -0.5)',
height: 'max(100vw, 100vh)',
width: 'max(100vw, 100vh)',
animation: `${spin} 3s linear infinite`,
background: failed
? // Hardcoded colors to prevent themes from messing with them (orange+gold, secondary+seafoam)
`conic-gradient(transparent 90deg, #FC521F 150deg, #FFAE00 210deg, transparent 270deg)`
: `conic-gradient(transparent 90deg, #029CFD 150deg, #37D5D3 210deg, transparent 270deg)`,
opacity: 1,
willChange: 'auto',
},
}));
const Card = styled.div(({ theme }) => ({
position: 'relative',
@ -85,10 +78,8 @@ const Collapsible = styled.div(({ theme }) => ({
}));
const Content = styled.div({
padding: '12px 6px',
display: 'flex',
flexDirection: 'column',
gap: '12px',
});
const Bar = styled.div<{ onClick?: (e: SyntheticEvent) => void }>(({ onClick }) => ({
@ -145,11 +136,13 @@ const StatusButton = styled(Button)<{ status: 'negative' | 'warning' }>(
})
);
const TestProvider = styled.div({
display: 'flex',
justifyContent: 'space-between',
gap: 6,
});
const TestProvider = styled.div(({ theme }) => ({
padding: 4,
'&:not(:last-child)': {
boxShadow: `inset 0 -1px 0 ${theme.appBorderColor}`,
},
}));
interface TestingModuleProps {
testProviders: TestProviders[keyof TestProviders][];
@ -172,13 +165,13 @@ export const TestingModule = ({
}: TestingModuleProps) => {
const api = useStorybookApi();
const contentRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<null | ReturnType<typeof setTimeout>>(null);
const contentRef = useRef<HTMLDivElement>(null);
const [maxHeight, setMaxHeight] = useState(DEFAULT_HEIGHT);
const [isCollapsed, setCollapsed] = useState(false);
const [isUpdated, setUpdated] = useState(false);
const [isCollapsed, setCollapsed] = useState(true);
const [isChangingCollapse, setChangingCollapse] = useState(false);
useEffect(() => {
if (contentRef.current) {
setMaxHeight(contentRef.current?.getBoundingClientRect().height || DEFAULT_HEIGHT);
@ -197,6 +190,19 @@ export const TestingModule = ({
}
}, [isCollapsed]);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
const handler = () => {
setUpdated(true);
timeout = setTimeout(setUpdated, 1000, false);
};
api.on(TESTING_MODULE_CONFIG_CHANGE, handler);
return () => {
api.off(TESTING_MODULE_CONFIG_CHANGE, handler);
clearTimeout(timeout);
};
}, [api]);
const toggleCollapsed = useCallback((event: SyntheticEvent) => {
event.stopPropagation();
setChangingCollapse(true);
@ -224,6 +230,7 @@ export const TestingModule = ({
running={isRunning}
crashed={isCrashed}
failed={isFailed || errorCount > 0}
updated={isUpdated}
>
<Card>
{hasTestProviders && (
@ -238,13 +245,9 @@ export const TestingModule = ({
<Content ref={contentRef}>
{testProviders.map((state) => {
const { render: Render } = state;
return Render ? (
<Fragment key={state.id}>
<Render {...state} />
</Fragment>
) : (
<TestProvider key={state.id}>
<LegacyRender {...state} />
return (
<TestProvider key={state.id} data-module-id={state.id}>
{Render ? <Render {...state} /> : <LegacyRender {...state} />}
</TestProvider>
);
})}

View File

@ -50,6 +50,7 @@ const generateStories = ({ title, refId }: { title: string; refId?: string }): A
name: root,
children: [componentId],
startCollapsed: false,
tags: [],
},
{
type: 'component',

View File

@ -7,6 +7,7 @@ export interface API_BaseEntry {
id: StoryId;
depth: number;
name: string;
tags: Tag[];
refId?: string;
renderLabel?: (item: API_BaseEntry, api: any) => any;
}
@ -27,7 +28,6 @@ export interface API_ComponentEntry extends API_BaseEntry {
type: 'component';
parent?: StoryId;
children: StoryId[];
tags: Tag[];
}
export interface API_DocsEntry extends API_BaseEntry {
@ -35,7 +35,6 @@ export interface API_DocsEntry extends API_BaseEntry {
parent: StoryId;
title: ComponentTitle;
importPath: Path;
tags: Tag[];
prepared: boolean;
parameters?: {
[parameterName: string]: any;
@ -47,7 +46,6 @@ export interface API_StoryEntry extends API_BaseEntry {
parent: StoryId;
title: ComponentTitle;
importPath: Path;
tags: Tag[];
prepared: boolean;
parameters?: {
[parameterName: string]: any;

View File

@ -64,7 +64,7 @@
"vite": "^4.0.0"
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.0.0 || ^3.0.0 || ^4.0.0",
"@sveltejs/vite-plugin-svelte": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
"storybook": "workspace:^",
"svelte": "^4.0.0 || ^5.0.0",
"vite": "^4.0.0 || ^5.0.0 || ^6.0.0"

View File

@ -293,5 +293,6 @@
"Dependency Upgrades"
]
]
}
},
"deferredNextVersion": "8.5.0-alpha.12"
}

View File

@ -7430,7 +7430,7 @@ __metadata:
typescript: "npm:^5.3.2"
vite: "npm:^4.0.0"
peerDependencies:
"@sveltejs/vite-plugin-svelte": ^2.0.0 || ^3.0.0 || ^4.0.0
"@sveltejs/vite-plugin-svelte": ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
storybook: "workspace:^"
svelte: ^4.0.0 || ^5.0.0
vite: ^4.0.0 || ^5.0.0 || ^6.0.0

View File

@ -1 +1 @@
{"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!"}}
{"version":"8.5.0-alpha.12","info":{"plain":"- Core / Addon Test: Add config UI to Testing Module - [#29708](https://github.com/storybookjs/storybook/pull/29708), thanks @ghengeveld!\n- Manager: Add tags property to GroupEntry objects - [#29672](https://github.com/storybookjs/storybook/pull/29672), thanks @Sidnioulz!\n- Svelte: Support `@sveltejs/vite-plugin-svelte` v5 - [#29731](https://github.com/storybookjs/storybook/pull/29731), thanks @JReinhold!\n- Toolbars: Suppress deprecation warning when using dynamic icons - [#29545](https://github.com/storybookjs/storybook/pull/29545), thanks @ValeraS!"}}

View File

@ -89,7 +89,7 @@ export const install: Task['run'] = async ({ sandboxDir, key }, { link, dryRun,
dryRun,
debug,
});
await addWorkaroundResolutions({ cwd, dryRun, debug });
await addWorkaroundResolutions({ cwd, dryRun, debug, key });
} else {
// We need to add package resolutions to ensure that we only ever install the latest version
// of any storybook packages as verdaccio is not able to both proxy to npm and publish over

View File

@ -94,7 +94,7 @@ export const addWorkaroundResolutions = async ({
'@testing-library/user-event': '^14.5.2',
};
if (key.includes('svelte-kit')) {
if (key?.includes('svelte-kit')) {
packageJson.resolutions['@sveltejs/vite-plugin-svelte'] = '^3.0.0';
}

View File

@ -49,11 +49,14 @@ test.describe("component testing", () => {
await sbPage.navigateToStory("addons/test", "Mismatch Failure");
const expandButton = await page.getByLabel('Expand testing module')
await expandButton.click();
// For whatever reason, sometimes it takes longer for the story to load
const storyElement = sbPage
.getCanvasBodyElement()
.getByRole("button", { name: "test" });
await expect(storyElement).toBeVisible({ timeout: 10000 });
await expect(storyElement).toBeVisible({ timeout: 30000 });
await sbPage.viewAddonPanel("Component tests");
@ -65,13 +68,12 @@ test.describe("component testing", () => {
if ((await testStoryElement.getAttribute("aria-expanded")) !== "true") {
testStoryElement.click();
}
const testingModuleDescription = await page.locator('#testing-module-description');
await expect(testingModuleDescription).toContainText('Not run');
const runTestsButton = await page.getByLabel('Start component tests')
await runTestsButton.click();
await expect(testingModuleDescription).toContainText('Testing', { timeout: 60000 });
@ -117,14 +119,17 @@ test.describe("component testing", () => {
const sbPage = new SbPage(page, expect);
await sbPage.navigateToStory("addons/test", "Expected Failure");
const expandButton = await page.getByLabel('Expand testing module')
await expandButton.click();
// For whatever reason, sometimes it takes longer for the story to load
const storyElement = sbPage
.getCanvasBodyElement()
.getByRole("button", { name: "test" });
await expect(storyElement).toBeVisible({ timeout: 10000 });
await expect(storyElement).toBeVisible({ timeout: 30000 });
await expect(page.locator('#testing-module-title')).toHaveText('Component tests');
await expect(page.locator('#testing-module-title')).toHaveText('Run local tests');
const testingModuleDescription = await page.locator('#testing-module-description');
@ -142,7 +147,7 @@ test.describe("component testing", () => {
// Wait for test results to appear
await expect(testingModuleDescription).toHaveText(/Ran \d+ tests/, { timeout: 30000 });
await expect(runTestsButton).toBeEnabled();
await expect(watchModeButton).toBeEnabled();
@ -186,11 +191,14 @@ test.describe("component testing", () => {
const sbPage = new SbPage(page, expect);
await sbPage.navigateToStory("addons/test", "Expected Failure");
const expandButton = await page.getByLabel('Expand testing module')
await expandButton.click();
// For whatever reason, sometimes it takes longer for the story to load
const storyElement = sbPage
.getCanvasBodyElement()
.getByRole("button", { name: "test" });
await expect(storyElement).toBeVisible({ timeout: 10000 });
await expect(storyElement).toBeVisible({ timeout: 30000 });
await page.getByLabel("Enable watch mode for Component tests").click();