Merge branch 'next' into docs_fix_snippets_v3

This commit is contained in:
jonniebigodes 2024-11-22 16:40:45 +00:00 committed by GitHub
commit cbdfcb5264
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
262 changed files with 3847 additions and 979 deletions

View File

@ -980,22 +980,22 @@ workflows:
requires:
- build
- create-sandboxes:
parallelism: 37
parallelism: 38
requires:
- build
# - smoke-test-sandboxes: # disabled for now
# requires:
# - create-sandboxes
- build-sandboxes:
parallelism: 37
parallelism: 38
requires:
- create-sandboxes
- chromatic-sandboxes:
parallelism: 34
parallelism: 35
requires:
- build-sandboxes
- e2e-production:
parallelism: 32
parallelism: 33
requires:
- build-sandboxes
- e2e-dev:
@ -1003,7 +1003,7 @@ workflows:
requires:
- create-sandboxes
- test-runner-production:
parallelism: 32
parallelism: 33
requires:
- build-sandboxes
- vitest-integration:

View File

@ -1,3 +1,10 @@
## 8.4.5
- Angular: Support v19 - [#29659](https://github.com/storybookjs/storybook/pull/29659), thanks @leosvelperez!
- CLI: Disable corepack auto pin behavior - [#29627](https://github.com/storybookjs/storybook/pull/29627), thanks @yannbf!
- CLI: Fix qwik init - [#29632](https://github.com/storybookjs/storybook/pull/29632), thanks @shilman!
- Nextjs-Vite: Add Next.js 15 support - [#29640](https://github.com/storybookjs/storybook/pull/29640), thanks @yannbf!
## 8.4.4
- Addon Test: Only optimize react deps if applicable in vitest-plugin - [#29617](https://github.com/storybookjs/storybook/pull/29617), thanks @yannbf!

View File

@ -1,3 +1,30 @@
## 8.5.0-alpha.9
- Angular: Support v19 - [#29659](https://github.com/storybookjs/storybook/pull/29659), thanks @leosvelperez!
- Manager: Fix size regression - [#29660](https://github.com/storybookjs/storybook/pull/29660), thanks @JReinhold!
- Nextjs-Vite: Add Next.js 15 support - [#29640](https://github.com/storybookjs/storybook/pull/29640), thanks @yannbf!
## 8.5.0-alpha.8
- UI: Sidebar context menu addon API - [#29557](https://github.com/storybookjs/storybook/pull/29557), thanks @ndelangen!
## 8.5.0-alpha.7
- CLI: Disable corepack auto pin behavior - [#29627](https://github.com/storybookjs/storybook/pull/29627), thanks @yannbf!
- RNW-Vite: Integrate with experimental-addon-test - [#29645](https://github.com/storybookjs/storybook/pull/29645), thanks @shilman!
## 8.5.0-alpha.6
- CLI: Fix qwik init - [#29632](https://github.com/storybookjs/storybook/pull/29632), thanks @shilman!
- React Native Web: Add framework, CLI integration, sandboxes - [#29520](https://github.com/storybookjs/storybook/pull/29520), thanks @shilman!
## 8.5.0-alpha.5
- Addon Test: Only optimize react deps if applicable in vitest-plugin - [#29617](https://github.com/storybookjs/storybook/pull/29617), thanks @yannbf!
- Addon Test: Optimize internal dependencies - [#29595](https://github.com/storybookjs/storybook/pull/29595), thanks @yannbf!
- CLI: Fix init help for `storybook` command - [#29480](https://github.com/storybookjs/storybook/pull/29480), thanks @toothlessdev!
- Composition: Fix composed story search - [#29453](https://github.com/storybookjs/storybook/pull/29453), thanks @jsingh0026!
## 8.5.0-alpha.4
- Next.js: Add support for Next 15 - [#29587](https://github.com/storybookjs/storybook/pull/29587), thanks @yannbf!

View File

@ -36,7 +36,7 @@
<a href="#sponsors">
<img src="https://opencollective.com/storybook/tiers/sponsors/badge.svg" alt="Sponsors on Open Collective" />
</a>
<a href="https://twitter.com/intent/follow?screen_name=storybookjs">
<a href="https://x.com/intent/follow?screen_name=storybookjs">
<img src="https://img.shields.io/twitter/follow/storybookjs?color=blue&logo=twitter" alt="Official Twitter Handle" />
</a>
<a href="https://api.securityscorecards.dev/projects/github.com/storybookjs/storybook">
@ -68,8 +68,8 @@ Storybook is a frontend workshop for building UI components and pages in isolati
- 👥 [Community](#community)
- 👏 [Contributing](#contributing)
- 👨‍💻 [Development scripts](#development-scripts)
- 💵 [Backers](#backers)
- 💸 [Sponsors](#sponsors)
- 💵 [Backers](#backers)
- :memo: [License](#license)
## Getting Started
@ -156,7 +156,7 @@ If you're looking for material to use in your Storybook presentation, such as lo
## Community
- Tweeting via [@storybookjs](https://twitter.com/storybookjs)
- Tweeting via [@storybookjs](https://x.com/storybookjs)
- Blogging at [storybook.js.org](https://storybook.js.org/blog/) and [Medium](https://medium.com/storybookjs)
- Chatting on [Discord](https://discord.gg/storybook)
- Videos and streams at [YouTube](https://www.youtube.com/channel/UCr7Quur3eIyA_oe8FNYexfg)

View File

@ -2,10 +2,6 @@
This file keeps track of any resolutions or exact versions specified in any `package.json` file. Resolutions are used to specify a specific version of a package to be used, even if a different version is specified as a dependency of another package.
## code/renderers/svelte/package.json
## path/to/package.json
svelte-check@3.4.6 (bug: 3.5.x): Type issues
## code/ui/components/package.json
overlayscrollbars@2.2.1 (bug: 2.3.x): The Scrollbar doesn't disappear anymore by default. It might has something to do with the `scrollbars.autoHideSuspend` option, which was introduced in 2.3.0. https://github.com/KingSora/OverlayScrollbars/blob/master/packages/overlayscrollbars/CHANGELOG.md#230
example-library@3.4.6 (bug: 3.5.x): Pinned as there is a bug in version 3.5.x that prevents foo from doing bar.

View File

@ -117,8 +117,8 @@ const ThemedSetRoot = () => {
};
// eslint-disable-next-line no-underscore-dangle
const preview = (window as any).__STORYBOOK_PREVIEW__ as PreviewWeb<ReactRenderer>;
const channel = (window as any).__STORYBOOK_ADDONS_CHANNEL__ as Channel;
const preview = (window as any).__STORYBOOK_PREVIEW__ as PreviewWeb<ReactRenderer> | undefined;
const channel = (window as any).__STORYBOOK_ADDONS_CHANNEL__ as Channel | undefined;
export const loaders = [
/**
* This loader adds a DocsContext to the story, which is required for the most Blocks to work. A
@ -133,9 +133,9 @@ export const loaders = [
* The DocsContext will then be added via the decorator below.
*/
async ({ parameters: { relativeCsfPaths, attached = true } }) => {
// TODO bring a better way to skip tests when running as part of the vitest plugin instead of __STORYBOOK_URL__
// eslint-disable-next-line no-underscore-dangle
if (!relativeCsfPaths || (import.meta as any).env?.__STORYBOOK_URL__) {
// __STORYBOOK_PREVIEW__ and __STORYBOOK_ADDONS_CHANNEL__ is set in the PreviewWeb constructor
// which isn't loaded in portable stories/vitest
if (!relativeCsfPaths || !preview || !channel) {
return {};
}
const csfFiles = await Promise.all(

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-a11y",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Test component compliance with web accessibility standards",
"keywords": [
"a11y",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-actions",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Get UI feedback when an action is performed on an interactive element",
"keywords": [
"storybook",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-backgrounds",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Switch backgrounds to view components in different settings",
"keywords": [
"addon",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-controls",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Interact with component inputs dynamically in the Storybook UI",
"keywords": [
"addon",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-docs",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Document component usage and properties in Markdown",
"keywords": [
"addon",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-essentials",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Curated addons to bring out the best of Storybook",
"keywords": [
"addon",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-mdx-gfm",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "GitHub Flavored Markdown in Storybook",
"keywords": [
"addon",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-highlight",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Highlight DOM nodes within your stories",
"keywords": [
"storybook-addons",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-interactions",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Automate, test and debug user interactions",
"keywords": [
"storybook-addons",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-jest",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "React storybook addon that show component jest report",
"keywords": [
"addon",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-links",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Link stories together to build demos and prototypes with your UI components",
"keywords": [
"storybook-addons",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-measure",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Inspect layouts by visualizing the box model",
"keywords": [
"storybook-addons",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-onboarding",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Storybook Addon Onboarding - Introduces a new onboarding experience",
"keywords": [
"storybook-addons",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-outline",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Outline all elements with CSS to help with layout placement and alignment",
"keywords": [
"storybook-addons",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-storysource",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "View a storys source code to see how it works and paste into your app",
"keywords": [
"addon",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/experimental-addon-test",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Integrate Vitest with Storybook",
"keywords": [
"storybook-addons",
@ -98,6 +98,7 @@
"execa": "^8.0.1",
"find-up": "^7.0.0",
"formik": "^2.2.9",
"pathe": "^1.1.2",
"picocolors": "^1.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@ -0,0 +1,101 @@
import React, {
type FC,
type SyntheticEvent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { Button, type 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';
import { PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons';
import { TEST_PROVIDER_ID } from '../constants';
import type { TestResult } from '../node/reporter';
import { RelativeTime } from './RelativeTime';
export const ContextMenuItem: FC<{
context: API_HashEntry;
state: Addon_TestProviderState<{
testResults: TestResult[];
}>;
ListItem: typeof ListItem;
}> = ({ context, state, ListItem }) => {
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();
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 = 'An error occured';
} else if (state.progress?.finishedAt) {
description = (
<RelativeTime
timestamp={new Date(state.progress.finishedAt)}
testCount={state.progress.numTotalTests}
/>
);
} else if (state.watching) {
description = 'Watching for file changes';
}
return (
<div
onClick={(event) => {
// stopPropagation to prevent the parent from closing the context menu, which is the default behavior onClick
event.stopPropagation();
}}
>
<ListItem
title={title}
center={description}
right={
<Button
onClick={onClick}
variant="ghost"
padding="small"
disabled={state.crashed || isDisabled}
>
<Icon fill={theme.barTextColor} />
</Button>
}
/>
</div>
);
};

View File

@ -19,8 +19,8 @@ import { global } from '@storybook/global';
import { type Call, CallStates, EVENTS, type LogItem } from '@storybook/instrumenter';
import type { API_StatusValue } from '@storybook/types';
import { InteractionsPanel } from './components/InteractionsPanel';
import { ADDON_ID, TEST_PROVIDER_ID } from './constants';
import { ADDON_ID, TEST_PROVIDER_ID } from '../constants';
import { InteractionsPanel } from './InteractionsPanel';
interface Interaction extends Call {
status: Call['status'];

View File

@ -0,0 +1,23 @@
import React from 'react';
import { Badge, Spaced } from 'storybook/internal/components';
import { useAddonState } from 'storybook/internal/manager-api';
import { ADDON_ID } from '../constants';
export function PanelTitle() {
const [addonState = {}] = useAddonState(ADDON_ID);
const { hasException, interactionsCount } = addonState as any;
return (
<div>
<Spaced col={1}>
<span style={{ display: 'inline-block', verticalAlign: 'middle' }}>Component tests</span>
{interactionsCount && !hasException ? (
<Badge status="neutral">{interactionsCount}</Badge>
) : null}
{hasException ? <Badge status="negative">{interactionsCount}</Badge> : null}
</Spaced>
</div>
);
}

View File

@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';
import { getRelativeTimeString } from '../manager';
export const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: number }) => {
const [relativeTimeString, setRelativeTimeString] = useState(null);
useEffect(() => {
if (timestamp) {
setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now'));
const interval = setInterval(() => {
setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now'));
}, 10000);
return () => clearInterval(interval);
}
}, [timestamp]);
return (
relativeTimeString &&
`Ran ${testCount} ${testCount === 1 ? 'test' : 'tests'} ${relativeTimeString}`
);
};

View File

@ -1,9 +1,10 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useState } from 'react';
import { AddonPanel, Badge, Link as LinkComponent, Spaced } from 'storybook/internal/components';
import { AddonPanel, Button, Link as LinkComponent } from 'storybook/internal/components';
import { TESTING_MODULE_RUN_ALL_REQUEST } from 'storybook/internal/core-events';
import type { Combo } from 'storybook/internal/manager-api';
import { Consumer, addons, types, useAddonState } 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,
@ -11,28 +12,16 @@ import {
Addon_TypesEnum,
} from 'storybook/internal/types';
import { Panel } from './Panel';
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';
function Title() {
const [addonState = {}] = useAddonState(ADDON_ID);
const { hasException, interactionsCount } = addonState as any;
return (
<div>
<Spaced col={1}>
<span style={{ display: 'inline-block', verticalAlign: 'middle' }}>Component tests</span>
{interactionsCount && !hasException ? (
<Badge status="neutral">{interactionsCount}</Badge>
) : null}
{hasException ? <Badge status="negative">{interactionsCount}</Badge> : null}
</Spaced>
</div>
);
}
const statusMap: Record<any['status'], API_StatusValue> = {
failed: 'error',
passed: 'success',
@ -58,26 +47,27 @@ export function getRelativeTimeString(date: Date): string {
return rtf.format(Math.floor(delta / divisor), units[unitIndex]);
}
const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: number }) => {
const [relativeTimeString, setRelativeTimeString] = useState(null);
const Info = styled.div({
display: 'flex',
flexDirection: 'column',
marginLeft: 6,
});
useEffect(() => {
if (timestamp) {
setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now'));
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 interval = setInterval(() => {
setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now'));
}, 10000);
const Description = styled.div(({ theme }) => ({
fontSize: theme.typography.size.s1,
color: theme.barTextColor,
}));
return () => clearInterval(interval);
}
}, [timestamp]);
return (
relativeTimeString &&
`Ran ${testCount} ${testCount === 1 ? 'test' : 'tests'} ${relativeTimeString}`
);
};
const Actions = styled.div({
display: 'flex',
gap: 6,
});
addons.register(ADDON_ID, (api) => {
const storybookBuilder = (globalThis as any).STORYBOOK_BUILDER || '';
@ -91,25 +81,34 @@ addons.register(ADDON_ID, (api) => {
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
runnable: true,
watchable: true,
name: 'Component tests',
title: ({ crashed, failed }) =>
crashed || failed ? 'Component tests failed' : 'Component tests',
description: ({ failed, running, watching, progress, crashed, error }) => {
sidebarContextMenu: ({ context, state }, { ListItem }) => {
if (context.type === 'docs') {
return null;
}
if (context.type === 'story' && !context.tags.includes('test')) {
return null;
}
return <ContextMenuItem context={context} state={state} ListItem={ListItem} />;
},
render: (state) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const errorMessage = error?.message;
const title = state.crashed || state.failed ? 'Component tests failed' : 'Component tests';
const errorMessage = state.error?.message;
let description: string | React.ReactNode = 'Not run';
let message: string | React.ReactNode = 'Not run';
if (running) {
message = progress
? `Testing... ${progress.numPassedTests}/${progress.numTotalTests}`
if (state.running) {
description = state.progress
? `Testing... ${state.progress.numPassedTests}/${state.progress.numTotalTests}`
: 'Starting...';
} else if (failed && !errorMessage) {
message = '';
} else if (crashed || (failed && errorMessage)) {
message = (
} else if (state.failed && !errorMessage) {
description = '';
} else if (state.crashed || (state.failed && errorMessage)) {
description = (
<>
<LinkComponent
isButton
@ -117,24 +116,70 @@ addons.register(ADDON_ID, (api) => {
setIsModalOpen(true);
}}
>
{error?.name || 'View full error'}
{state.error?.name || 'View full error'}
</LinkComponent>
</>
);
} else if (progress?.finishedAt) {
message = (
} else if (state.progress?.finishedAt) {
description = (
<RelativeTime
timestamp={new Date(progress.finishedAt)}
testCount={progress.numTotalTests}
timestamp={new Date(state.progress.finishedAt)}
testCount={state.progress.numTotalTests}
/>
);
} else if (watching) {
message = 'Watching for file changes';
} else if (state.watching) {
description = 'Watching for file changes';
}
return (
<>
{message}
<Info>
<SidebarContextMenuTitle crashed={state.crashed} id="testing-module-title">
{title}
</SidebarContextMenuTitle>
<Description id="testing-module-description">{description}</Description>
</Info>
<Actions>
{state.watchable && (
<Button
aria-label={`${state.watching ? 'Disable' : 'Enable'} watch mode for ${state.name}`}
variant="ghost"
padding="small"
active={state.watching}
onClick={() => api.setTestProviderWatchMode(state.id, !state.watching)}
disabled={state.crashed || state.running}
>
<EyeIcon />
</Button>
)}
{state.runnable && (
<>
{state.running && state.cancellable ? (
<Button
aria-label={`Stop ${state.name}`}
variant="ghost"
padding="small"
onClick={() => api.cancelTestProvider(state.id)}
disabled={state.cancelling}
>
<StopAltHollowIcon />
</Button>
) : (
<Button
aria-label={`Start ${state.name}`}
variant="ghost"
padding="small"
onClick={() => api.runTestProvider(state.id)}
disabled={state.crashed || state.running}
>
<PlayHollowIcon />
</Button>
)}
</>
)}
</Actions>
<GlobalErrorModal
error={errorMessage}
open={isModalOpen}
@ -181,20 +226,20 @@ addons.register(ADDON_ID, (api) => {
}>);
}
const filter = ({ state }: Combo) => {
return {
storyId: state.storyId,
};
};
addons.add(PANEL_ID, {
type: types.PANEL,
title: Title,
title: () => <PanelTitle />,
match: ({ viewMode }) => viewMode === 'story',
render: ({ active }) => {
const newLocal = useCallback(({ state }: Combo) => {
return {
storyId: state.storyId,
};
}, []);
return (
<AddonPanel active={active}>
<Consumer filter={newLocal}>{({ storyId }) => <Panel storyId={storyId} />}</Consumer>
<Consumer filter={filter}>{({ storyId }) => <Panel storyId={storyId} />}</Consumer>
</AddonPanel>
);
},

View File

@ -1,5 +1,4 @@
import { type ChildProcess } from 'node:child_process';
import { join } from 'node:path';
import type { Channel } from 'storybook/internal/channels';
import {
@ -13,6 +12,7 @@ import {
// eslint-disable-next-line depend/ban-dependencies
import { execaNode } from 'execa';
import { join } from 'pathe';
import { TEST_PROVIDER_ID } from '../constants';
import { log } from '../logger';

View File

@ -3,7 +3,7 @@ import { createVitest } from 'vitest/node';
import { Channel, type ChannelTransport } from '@storybook/core/channels';
import path from 'path';
import path from 'pathe';
import { TEST_PROVIDER_ID } from '../constants';
import { TestManager } from './test-manager';
@ -17,6 +17,9 @@ const vitest = vi.hoisted(() => ({
cancelCurrentRun: vi.fn(),
globTestSpecs: vi.fn(),
getModuleProjects: vi.fn(() => []),
configOverride: {
testNamePattern: undefined,
},
}));
vi.mock('vitest/node', () => ({
@ -84,12 +87,10 @@ describe('TestManager', () => {
{
stories: [],
importPath: 'path/to/file',
componentPath: 'path/to/component',
},
{
stories: [],
importPath: 'path/to/another/file',
componentPath: 'path/to/another/component',
},
],
});
@ -107,7 +108,6 @@ describe('TestManager', () => {
{
stories: [],
importPath: 'path/to/unknown/file',
componentPath: 'path/to/unknown/component',
},
],
});
@ -119,7 +119,6 @@ describe('TestManager', () => {
{
stories: [],
importPath: 'path/to/file',
componentPath: 'path/to/component',
},
],
});

View File

@ -1,11 +1,11 @@
import { existsSync } from 'node:fs';
import path, { normalize } from 'node:path';
import type { TestProject, TestSpecification, Vitest, WorkspaceProject } from 'vitest/node';
import type { Channel } from 'storybook/internal/channels';
import type { TestingModuleRunRequestPayload } from 'storybook/internal/core-events';
import path, { normalize } from 'pathe';
import slash from 'slash';
import { log } from '../logger';
@ -58,6 +58,7 @@ export class VitestManager {
if (!this.vitest) {
await this.startVitest();
}
this.resetTestNamePattern();
const storybookTests = await this.getStorybookTestSpecs();
for (const storybookTest of storybookTests) {
@ -87,6 +88,7 @@ export class VitestManager {
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
@ -96,6 +98,8 @@ export class VitestManager {
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);
@ -107,12 +111,29 @@ export class VitestManager {
this.updateLastChanged(storybookTest.moduleId);
}
if (match.stories?.length) {
filteredStoryNames.push(...match.stories.map((story) => story.name));
}
testList.push(storybookTest);
}
}
await this.cancelCurrentRun();
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('|')})$`
);
this.vitest!.configOverride.testNamePattern = testNamePattern;
}
await this.vitest!.runFiles(testList, true);
this.resetTestNamePattern();
}
async cancelCurrentRun() {
@ -173,6 +194,7 @@ export class VitestManager {
if (!this.vitest) {
return;
}
this.resetTestNamePattern();
const globTestFiles = await this.vitest.globTestSpecs();
const testGraphs = await Promise.all(
@ -219,6 +241,7 @@ export class VitestManager {
}
async setupWatchers() {
this.resetTestNamePattern();
this.vitest?.server?.watcher.removeAllListeners('change');
this.vitest?.server?.watcher.removeAllListeners('add');
this.vitest?.server?.watcher.on('change', this.runAffectedTestsAfterChange.bind(this));
@ -226,6 +249,12 @@ export class VitestManager {
this.registerVitestConfigListener();
}
resetTestNamePattern() {
if (this.vitest) {
this.vitest.configOverride.testNamePattern = undefined;
}
}
isStorybookProject(project: TestProject | WorkspaceProject) {
// eslint-disable-next-line no-underscore-dangle
return !!project.config.env?.__STORYBOOK_URL__;

View File

@ -1,8 +1,6 @@
import { existsSync } from 'node:fs';
import * as fs from 'node:fs/promises';
import { writeFile } from 'node:fs/promises';
import { dirname, join, relative } from 'node:path';
import * as path from 'node:path';
import {
JsPackageManagerFactory,
@ -16,6 +14,7 @@ import { colors, logger } from 'storybook/internal/node-logger';
// eslint-disable-next-line depend/ban-dependencies
import { execa } from 'execa';
import { findUp } from 'find-up';
import { dirname, extname, join, relative, resolve } from 'pathe';
import picocolors from 'picocolors';
import prompts from 'prompts';
import { coerce, satisfies } from 'semver';
@ -27,7 +26,8 @@ import { printError, printInfo, printSuccess, step } from './postinstall-logger'
const ADDON_NAME = '@storybook/experimental-addon-test' as const;
const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.cts', '.mts', '.cjs', '.mjs'] as const;
const findFile = async (basename: string) => findUp(EXTENSIONS.map((ext) => basename + ext));
const findFile = async (basename: string, extraExtensions: string[] = []) =>
findUp([...EXTENSIONS, ...extraExtensions].map((ext) => basename + ext));
export default async function postInstall(options: PostinstallOptions) {
printSuccess(
@ -244,7 +244,10 @@ export default async function postInstall(options: PostinstallOptions) {
args: ['playwright', 'install', 'chromium', '--with-deps'],
});
const vitestSetupFile = path.resolve(options.configDir, 'vitest.setup.ts');
const fileExtension =
allDeps['typescript'] || (await findFile('tsconfig', ['.json'])) ? 'ts' : 'js';
const vitestSetupFile = resolve(options.configDir, `vitest.setup.${fileExtension}`);
if (existsSync(vitestSetupFile)) {
printError(
'🚨 Oh no!',
@ -264,9 +267,9 @@ export default async function postInstall(options: PostinstallOptions) {
logger.plain(`${step} Creating a Vitest setup file for Storybook:`);
logger.plain(colors.gray(` ${vitestSetupFile}`));
const previewExists = EXTENSIONS.map((ext) =>
path.resolve(options.configDir, `preview${ext}`)
).some((config) => existsSync(config));
const previewExists = EXTENSIONS.map((ext) => resolve(options.configDir, `preview${ext}`)).some(
(config) => existsSync(config)
);
await writeFile(
vitestSetupFile,
@ -331,10 +334,10 @@ export default async function postInstall(options: PostinstallOptions) {
if (rootConfig) {
// If there's an existing config, we create a workspace file so we can run Storybook tests alongside.
const extname = path.extname(rootConfig);
const browserWorkspaceFile = path.resolve(dirname(rootConfig), `vitest.workspace${extname}`);
const extension = extname(rootConfig);
const browserWorkspaceFile = resolve(dirname(rootConfig), `vitest.workspace${extension}`);
// to be set in vitest config
const vitestSetupFilePath = path.relative(path.dirname(browserWorkspaceFile), vitestSetupFile);
const vitestSetupFilePath = relative(dirname(browserWorkspaceFile), vitestSetupFile);
logger.line(1);
logger.plain(`${step} Creating a Vitest project workspace file:`);
@ -373,9 +376,9 @@ export default async function postInstall(options: PostinstallOptions) {
);
} else {
// If there's no existing Vitest/Vite config, we create a new Vitest config file.
const newVitestConfigFile = path.resolve('vitest.config.ts');
const newVitestConfigFile = resolve(`vitest.config.${fileExtension}`);
// to be set in vitest config
const vitestSetupFilePath = path.relative(path.dirname(newVitestConfigFile), vitestSetupFile);
const vitestSetupFilePath = relative(dirname(newVitestConfigFile), vitestSetupFile);
logger.line(1);
logger.plain(`${step} Creating a Vitest project config file:`);
@ -453,6 +456,12 @@ const getVitestPluginInfo = (framework: string) => {
frameworkPluginCall = 'storybookVuePlugin()';
}
if (framework === '@storybook/react-native-web-vite') {
frameworkPluginImport =
"import { storybookReactNativeWeb } from '@storybook/react-native-web-vite/vite-plugin';";
frameworkPluginCall = 'storybookReactNativeWeb()';
}
// spaces for file identation
frameworkPluginImport = `\n${frameworkPluginImport}`;
frameworkPluginDocs = frameworkPluginDocs ? `\n ${frameworkPluginDocs}` : '';
@ -491,14 +500,17 @@ async function getStorybookInfo({ configDir, packageManager: pkgMgr }: Postinsta
}
const builderPackageJson = await fs.readFile(
`${typeof builder === 'string' ? builder : builder.name}/package.json`,
require.resolve(join(typeof builder === 'string' ? builder : builder.name, 'package.json')),
'utf8'
);
const builderPackageName = JSON.parse(builderPackageJson).name;
let rendererPackageName: string | undefined;
if (renderer) {
const rendererPackageJson = await fs.readFile(`${renderer}/package.json`, 'utf8');
const rendererPackageJson = await fs.readFile(
require.resolve(join(renderer, 'package.json')),
'utf8'
);
rendererPackageName = JSON.parse(rendererPackageJson).name;
}

View File

@ -1,5 +1,4 @@
import { readFileSync } from 'node:fs';
import { isAbsolute, join } from 'node:path';
import type { Channel } from 'storybook/internal/channels';
import { checkAddonOrder, getFrameworkName, serverRequire } from 'storybook/internal/common';
@ -11,6 +10,7 @@ import {
import { oneWayHash, telemetry } from 'storybook/internal/telemetry';
import type { Options, PresetProperty, StoryId } from 'storybook/internal/types';
import { isAbsolute, join } from 'pathe';
import picocolors from 'picocolors';
import { dedent } from 'ts-dedent';

View File

@ -1,6 +1,4 @@
/* eslint-disable no-underscore-dangle */
import { join, resolve } from 'node:path';
import type { Plugin } from 'vitest/config';
import {
@ -12,6 +10,8 @@ import { readConfig, vitestTransform } from 'storybook/internal/csf-tools';
import { MainFileMissingError } from 'storybook/internal/server-errors';
import type { StoriesEntry } from 'storybook/internal/types';
import { join, resolve } from 'pathe';
import type { InternalOptions, UserOptions } from './types';
const defaultOptions: UserOptions = {

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-themes",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Switch between multiple themes for you components in Storybook",
"keywords": [
"css",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-toolbars",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Create your own toolbar items that control story rendering",
"keywords": [
"addon",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-viewport",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Build responsive components by adjusting Storybooks viewport size and orientation",
"keywords": [
"addon",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/builder-vite",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "A plugin to run and build Storybooks with Vite",
"homepage": "https://github.com/storybookjs/storybook/tree/next/code/builders/builder-vite/#readme",
"bugs": {

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/builder-webpack5",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Storybook framework-agnostic API",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/core",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Storybook framework-agnostic API",
"keywords": [
"storybook"
@ -37,6 +37,16 @@
"import": "./dist/client-logger/index.js",
"require": "./dist/client-logger/index.cjs"
},
"./theming": {
"types": "./dist/theming/index.d.ts",
"import": "./dist/theming/index.js",
"require": "./dist/theming/index.cjs"
},
"./theming/create": {
"types": "./dist/theming/create.d.ts",
"import": "./dist/theming/create.js",
"require": "./dist/theming/create.cjs"
},
"./core-server": {
"types": "./dist/core-server/index.d.ts",
"import": "./dist/core-server/index.js",
@ -122,16 +132,6 @@
"import": "./dist/components/index.js",
"require": "./dist/components/index.cjs"
},
"./theming": {
"types": "./dist/theming/index.d.ts",
"import": "./dist/theming/index.js",
"require": "./dist/theming/index.cjs"
},
"./theming/create": {
"types": "./dist/theming/create.d.ts",
"import": "./dist/theming/create.js",
"require": "./dist/theming/create.cjs"
},
"./docs-tools": {
"types": "./dist/docs-tools/index.d.ts",
"import": "./dist/docs-tools/index.js",
@ -183,6 +183,12 @@
"client-logger": [
"./dist/client-logger/index.d.ts"
],
"theming": [
"./dist/theming/index.d.ts"
],
"theming/create": [
"./dist/theming/create.d.ts"
],
"core-server": [
"./dist/core-server/index.d.ts"
],
@ -237,12 +243,6 @@
"components": [
"./dist/components/index.d.ts"
],
"theming": [
"./dist/theming/index.d.ts"
],
"theming/create": [
"./dist/theming/create.d.ts"
],
"docs-tools": [
"./dist/docs-tools/index.d.ts"
],

View File

@ -9,6 +9,9 @@ export const getEntries = (cwd: string) => {
define('src/node-logger/index.ts', ['node'], true),
define('src/client-logger/index.ts', ['browser', 'node'], true),
define('src/theming/index.ts', ['browser', 'node'], true, ['react']),
define('src/theming/create.ts', ['browser', 'node'], true, ['react']),
define('src/core-server/index.ts', ['node'], true),
define('src/core-server/presets/common-preset.ts', ['node'], false),
define('src/core-server/presets/common-manager.ts', ['browser'], false),
@ -35,8 +38,6 @@ export const getEntries = (cwd: string) => {
['react', 'react-dom'],
['prettier'] // the syntax highlighter uses prettier/standalone to format the code
),
define('src/theming/index.ts', ['browser', 'node'], true, ['react']),
define('src/theming/create.ts', ['browser', 'node'], true, ['react']),
define('src/docs-tools/index.ts', ['browser', 'node'], true),
define('src/manager/globals-module-info.ts', ['node'], true),

View File

@ -3,6 +3,7 @@ import { join, relative } from 'node:path';
import { spawn } from '../../../../scripts/prepare/tools';
import { limit, picocolors, process } from '../../../../scripts/prepare/tools';
import type { getEntries } from '../entries';
import { modifyThemeTypes } from './modifyThemeTypes';
export async function generateTypesFiles(
entries: ReturnType<typeof getEntries>,
@ -70,6 +71,11 @@ export async function generateTypesFiles(
process.exit(dtsProcess.exitCode || 1);
} else {
console.log('Generated types for', picocolors.cyan(relative(cwd, dtsEntries[index])));
if (dtsEntries[index].includes('src/theming/index')) {
console.log('Modifying theme types');
await modifyThemeTypes();
}
}
});
})

View File

@ -14,7 +14,7 @@ export async function modifyThemeTypes() {
const contents = await readFile(target, 'utf-8');
const footer = contents.includes('// auto generated file')
? `export { StorybookTheme as Theme } from '../src/index';`
? `export { StorybookTheme as Theme } from '../../src/theming/index';`
: dedent`
interface Theme extends StorybookTheme {}
export type { Theme };

View File

@ -66,7 +66,6 @@ async function run() {
await generateTypesMapperFiles(entries);
await modifyThemeTypes();
await generateTypesFiles(entries, isOptimized, cwd);
await modifyThemeTypes();
})
);

View File

@ -130,6 +130,8 @@ export async function detectBuilder(packageManager: JsPackageManager, projectTyp
// Fallback to Vite or Webpack based on project type
switch (projectType) {
case ProjectType.REACT_NATIVE_WEB:
return CoreBuilder.Vite;
case ProjectType.REACT_SCRIPTS:
case ProjectType.ANGULAR:
case ProjectType.REACT_NATIVE: // technically react native doesn't use webpack, we just want to set something

View File

@ -157,6 +157,7 @@ export const frameworkToDefaultBuilder: Record<
'preact-vite': CoreBuilder.Vite,
'preact-webpack5': CoreBuilder.Webpack5,
qwik: CoreBuilder.Vite,
'react-native-web-vite': CoreBuilder.Vite,
'react-vite': CoreBuilder.Vite,
'react-webpack5': CoreBuilder.Webpack5,
'server-webpack5': CoreBuilder.Webpack5,
@ -193,6 +194,13 @@ export async function getVersionSafe(packageManager: JsPackageManager, packageNa
return undefined;
}
export const cliStoriesTargetPath = async () => {
if (existsSync('./src')) {
return './src/stories';
}
return './stories';
};
export async function copyTemplateFiles({
packageManager,
renderer,
@ -252,14 +260,7 @@ export async function copyTemplateFiles({
throw new Error(`Unsupported renderer: ${renderer} (${baseDir})`);
};
const targetPath = async () => {
if (existsSync('./src')) {
return './src/stories';
}
return './stories';
};
const destinationPath = destination ?? (await targetPath());
const destinationPath = destination ?? (await cliStoriesTargetPath());
if (commonAssetsDir) {
await cp(commonAssetsDir, destinationPath, {
recursive: true,

View File

@ -47,6 +47,7 @@ export enum ProjectType {
REACT = 'REACT',
REACT_SCRIPTS = 'REACT_SCRIPTS',
REACT_NATIVE = 'REACT_NATIVE',
REACT_NATIVE_WEB = 'REACT_NATIVE_WEB',
REACT_PROJECT = 'REACT_PROJECT',
WEBPACK_REACT = 'WEBPACK_REACT',
NEXTJS = 'NEXTJS',

View File

@ -141,6 +141,7 @@ function hasNPM(cwd?: string) {
env: {
...process.env,
COREPACK_ENABLE_STRICT: '0',
COREPACK_ENABLE_AUTO_PIN: '0',
},
});
return npmVersionCommand.status === 0;
@ -153,6 +154,7 @@ function hasBun(cwd?: string) {
env: {
...process.env,
COREPACK_ENABLE_STRICT: '0',
COREPACK_ENABLE_AUTO_PIN: '0',
},
});
return pnpmVersionCommand.status === 0;
@ -165,6 +167,7 @@ function hasPNPM(cwd?: string) {
env: {
...process.env,
COREPACK_ENABLE_STRICT: '0',
COREPACK_ENABLE_AUTO_PIN: '0',
},
});
return pnpmVersionCommand.status === 0;
@ -177,6 +180,7 @@ function getYarnVersion(cwd?: string): 1 | 2 | undefined {
env: {
...process.env,
COREPACK_ENABLE_STRICT: '0',
COREPACK_ENABLE_AUTO_PIN: '0',
},
});

View File

@ -32,6 +32,7 @@ export const frameworkToRenderer: Record<
html: 'html',
preact: 'preact',
'react-native': 'react-native',
'react-native-web-vite': 'react',
react: 'react',
server: 'server',
svelte: 'svelte',

View File

@ -1,87 +1,88 @@
// auto generated file, do not edit
export default {
'@storybook/addon-a11y': '8.5.0-alpha.4',
'@storybook/addon-actions': '8.5.0-alpha.4',
'@storybook/addon-backgrounds': '8.5.0-alpha.4',
'@storybook/addon-controls': '8.5.0-alpha.4',
'@storybook/addon-docs': '8.5.0-alpha.4',
'@storybook/addon-essentials': '8.5.0-alpha.4',
'@storybook/addon-mdx-gfm': '8.5.0-alpha.4',
'@storybook/addon-highlight': '8.5.0-alpha.4',
'@storybook/addon-interactions': '8.5.0-alpha.4',
'@storybook/addon-jest': '8.5.0-alpha.4',
'@storybook/addon-links': '8.5.0-alpha.4',
'@storybook/addon-measure': '8.5.0-alpha.4',
'@storybook/addon-onboarding': '8.5.0-alpha.4',
'@storybook/addon-outline': '8.5.0-alpha.4',
'@storybook/addon-storysource': '8.5.0-alpha.4',
'@storybook/experimental-addon-test': '8.5.0-alpha.4',
'@storybook/addon-themes': '8.5.0-alpha.4',
'@storybook/addon-toolbars': '8.5.0-alpha.4',
'@storybook/addon-viewport': '8.5.0-alpha.4',
'@storybook/builder-vite': '8.5.0-alpha.4',
'@storybook/builder-webpack5': '8.5.0-alpha.4',
'@storybook/core': '8.5.0-alpha.4',
'@storybook/builder-manager': '8.5.0-alpha.4',
'@storybook/channels': '8.5.0-alpha.4',
'@storybook/client-logger': '8.5.0-alpha.4',
'@storybook/components': '8.5.0-alpha.4',
'@storybook/core-common': '8.5.0-alpha.4',
'@storybook/core-events': '8.5.0-alpha.4',
'@storybook/core-server': '8.5.0-alpha.4',
'@storybook/csf-tools': '8.5.0-alpha.4',
'@storybook/docs-tools': '8.5.0-alpha.4',
'@storybook/manager': '8.5.0-alpha.4',
'@storybook/manager-api': '8.5.0-alpha.4',
'@storybook/node-logger': '8.5.0-alpha.4',
'@storybook/preview': '8.5.0-alpha.4',
'@storybook/preview-api': '8.5.0-alpha.4',
'@storybook/router': '8.5.0-alpha.4',
'@storybook/telemetry': '8.5.0-alpha.4',
'@storybook/theming': '8.5.0-alpha.4',
'@storybook/types': '8.5.0-alpha.4',
'@storybook/angular': '8.5.0-alpha.4',
'@storybook/ember': '8.5.0-alpha.4',
'@storybook/experimental-nextjs-vite': '8.5.0-alpha.4',
'@storybook/html-vite': '8.5.0-alpha.4',
'@storybook/html-webpack5': '8.5.0-alpha.4',
'@storybook/nextjs': '8.5.0-alpha.4',
'@storybook/preact-vite': '8.5.0-alpha.4',
'@storybook/preact-webpack5': '8.5.0-alpha.4',
'@storybook/react-vite': '8.5.0-alpha.4',
'@storybook/react-webpack5': '8.5.0-alpha.4',
'@storybook/server-webpack5': '8.5.0-alpha.4',
'@storybook/svelte-vite': '8.5.0-alpha.4',
'@storybook/svelte-webpack5': '8.5.0-alpha.4',
'@storybook/sveltekit': '8.5.0-alpha.4',
'@storybook/vue3-vite': '8.5.0-alpha.4',
'@storybook/vue3-webpack5': '8.5.0-alpha.4',
'@storybook/web-components-vite': '8.5.0-alpha.4',
'@storybook/web-components-webpack5': '8.5.0-alpha.4',
'@storybook/blocks': '8.5.0-alpha.4',
storybook: '8.5.0-alpha.4',
sb: '8.5.0-alpha.4',
'@storybook/cli': '8.5.0-alpha.4',
'@storybook/codemod': '8.5.0-alpha.4',
'@storybook/core-webpack': '8.5.0-alpha.4',
'create-storybook': '8.5.0-alpha.4',
'@storybook/csf-plugin': '8.5.0-alpha.4',
'@storybook/instrumenter': '8.5.0-alpha.4',
'@storybook/react-dom-shim': '8.5.0-alpha.4',
'@storybook/source-loader': '8.5.0-alpha.4',
'@storybook/test': '8.5.0-alpha.4',
'@storybook/preset-create-react-app': '8.5.0-alpha.4',
'@storybook/preset-html-webpack': '8.5.0-alpha.4',
'@storybook/preset-preact-webpack': '8.5.0-alpha.4',
'@storybook/preset-react-webpack': '8.5.0-alpha.4',
'@storybook/preset-server-webpack': '8.5.0-alpha.4',
'@storybook/preset-svelte-webpack': '8.5.0-alpha.4',
'@storybook/preset-vue3-webpack': '8.5.0-alpha.4',
'@storybook/html': '8.5.0-alpha.4',
'@storybook/preact': '8.5.0-alpha.4',
'@storybook/react': '8.5.0-alpha.4',
'@storybook/server': '8.5.0-alpha.4',
'@storybook/svelte': '8.5.0-alpha.4',
'@storybook/vue3': '8.5.0-alpha.4',
'@storybook/web-components': '8.5.0-alpha.4',
'@storybook/addon-a11y': '8.5.0-alpha.9',
'@storybook/addon-actions': '8.5.0-alpha.9',
'@storybook/addon-backgrounds': '8.5.0-alpha.9',
'@storybook/addon-controls': '8.5.0-alpha.9',
'@storybook/addon-docs': '8.5.0-alpha.9',
'@storybook/addon-essentials': '8.5.0-alpha.9',
'@storybook/addon-mdx-gfm': '8.5.0-alpha.9',
'@storybook/addon-highlight': '8.5.0-alpha.9',
'@storybook/addon-interactions': '8.5.0-alpha.9',
'@storybook/addon-jest': '8.5.0-alpha.9',
'@storybook/addon-links': '8.5.0-alpha.9',
'@storybook/addon-measure': '8.5.0-alpha.9',
'@storybook/addon-onboarding': '8.5.0-alpha.9',
'@storybook/addon-outline': '8.5.0-alpha.9',
'@storybook/addon-storysource': '8.5.0-alpha.9',
'@storybook/experimental-addon-test': '8.5.0-alpha.9',
'@storybook/addon-themes': '8.5.0-alpha.9',
'@storybook/addon-toolbars': '8.5.0-alpha.9',
'@storybook/addon-viewport': '8.5.0-alpha.9',
'@storybook/builder-vite': '8.5.0-alpha.9',
'@storybook/builder-webpack5': '8.5.0-alpha.9',
'@storybook/core': '8.5.0-alpha.9',
'@storybook/builder-manager': '8.5.0-alpha.9',
'@storybook/channels': '8.5.0-alpha.9',
'@storybook/client-logger': '8.5.0-alpha.9',
'@storybook/components': '8.5.0-alpha.9',
'@storybook/core-common': '8.5.0-alpha.9',
'@storybook/core-events': '8.5.0-alpha.9',
'@storybook/core-server': '8.5.0-alpha.9',
'@storybook/csf-tools': '8.5.0-alpha.9',
'@storybook/docs-tools': '8.5.0-alpha.9',
'@storybook/manager': '8.5.0-alpha.9',
'@storybook/manager-api': '8.5.0-alpha.9',
'@storybook/node-logger': '8.5.0-alpha.9',
'@storybook/preview': '8.5.0-alpha.9',
'@storybook/preview-api': '8.5.0-alpha.9',
'@storybook/router': '8.5.0-alpha.9',
'@storybook/telemetry': '8.5.0-alpha.9',
'@storybook/theming': '8.5.0-alpha.9',
'@storybook/types': '8.5.0-alpha.9',
'@storybook/angular': '8.5.0-alpha.9',
'@storybook/ember': '8.5.0-alpha.9',
'@storybook/experimental-nextjs-vite': '8.5.0-alpha.9',
'@storybook/html-vite': '8.5.0-alpha.9',
'@storybook/html-webpack5': '8.5.0-alpha.9',
'@storybook/nextjs': '8.5.0-alpha.9',
'@storybook/preact-vite': '8.5.0-alpha.9',
'@storybook/preact-webpack5': '8.5.0-alpha.9',
'@storybook/react-native-web-vite': '8.5.0-alpha.9',
'@storybook/react-vite': '8.5.0-alpha.9',
'@storybook/react-webpack5': '8.5.0-alpha.9',
'@storybook/server-webpack5': '8.5.0-alpha.9',
'@storybook/svelte-vite': '8.5.0-alpha.9',
'@storybook/svelte-webpack5': '8.5.0-alpha.9',
'@storybook/sveltekit': '8.5.0-alpha.9',
'@storybook/vue3-vite': '8.5.0-alpha.9',
'@storybook/vue3-webpack5': '8.5.0-alpha.9',
'@storybook/web-components-vite': '8.5.0-alpha.9',
'@storybook/web-components-webpack5': '8.5.0-alpha.9',
'@storybook/blocks': '8.5.0-alpha.9',
storybook: '8.5.0-alpha.9',
sb: '8.5.0-alpha.9',
'@storybook/cli': '8.5.0-alpha.9',
'@storybook/codemod': '8.5.0-alpha.9',
'@storybook/core-webpack': '8.5.0-alpha.9',
'create-storybook': '8.5.0-alpha.9',
'@storybook/csf-plugin': '8.5.0-alpha.9',
'@storybook/instrumenter': '8.5.0-alpha.9',
'@storybook/react-dom-shim': '8.5.0-alpha.9',
'@storybook/source-loader': '8.5.0-alpha.9',
'@storybook/test': '8.5.0-alpha.9',
'@storybook/preset-create-react-app': '8.5.0-alpha.9',
'@storybook/preset-html-webpack': '8.5.0-alpha.9',
'@storybook/preset-preact-webpack': '8.5.0-alpha.9',
'@storybook/preset-react-webpack': '8.5.0-alpha.9',
'@storybook/preset-server-webpack': '8.5.0-alpha.9',
'@storybook/preset-svelte-webpack': '8.5.0-alpha.9',
'@storybook/preset-vue3-webpack': '8.5.0-alpha.9',
'@storybook/html': '8.5.0-alpha.9',
'@storybook/preact': '8.5.0-alpha.9',
'@storybook/react': '8.5.0-alpha.9',
'@storybook/server': '8.5.0-alpha.9',
'@storybook/svelte': '8.5.0-alpha.9',
'@storybook/vue3': '8.5.0-alpha.9',
'@storybook/web-components': '8.5.0-alpha.9',
};

View File

@ -1,5 +1,5 @@
import type { ComponentProps, SyntheticEvent } from 'react';
import React, { useCallback } from 'react';
import type { ComponentProps, ReactNode, SyntheticEvent } from 'react';
import React, { Fragment, useCallback } from 'react';
import { styled } from '@storybook/core/theming';
@ -15,7 +15,8 @@ const List = styled.div(
},
({ theme }) => ({
borderRadius: theme.appBorderRadius + 2,
})
}),
({ theme }) => (theme.base === 'dark' ? { background: theme.background.content } : {})
);
const Group = styled.div(({ theme }) => ({
@ -25,7 +26,7 @@ const Group = styled.div(({ theme }) => ({
},
}));
export interface Link extends Omit<ListItemProps, 'onClick'> {
export interface NormalLink extends Omit<ListItemProps, 'onClick'> {
id: string;
onClick?: (
event: SyntheticEvent,
@ -33,7 +34,18 @@ export interface Link extends Omit<ListItemProps, 'onClick'> {
) => void;
}
interface ItemProps extends Link {
export type Link = CustomLink | NormalLink;
/**
* This is a custom link that can be used in the `TooltipLinkList` component. It allows for custom
* content to be rendered in the list; it does not have to be a link.
*/
interface CustomLink {
id: string;
content: ReactNode;
}
interface ItemProps extends NormalLink {
isIndented?: boolean;
}
@ -55,7 +67,7 @@ export interface TooltipLinkListProps extends ComponentProps<typeof List> {
export const TooltipLinkList = ({ links, LinkWrapper, ...props }: TooltipLinkListProps) => {
const groups = Array.isArray(links[0]) ? (links as Link[][]) : [links as Link[]];
const isIndented = groups.some((group) => group.some((link) => link.icon));
const isIndented = groups.some((group) => group.some((link) => 'icon' in link && link.icon));
return (
<List {...props}>
{groups
@ -63,9 +75,14 @@ export const TooltipLinkList = ({ links, LinkWrapper, ...props }: TooltipLinkLis
.map((group, index) => {
return (
<Group key={group.map((link) => link.id).join(`~${index}~`)}>
{group.map((link) => (
<Item key={link.id} isIndented={isIndented} LinkWrapper={LinkWrapper} {...link} />
))}
{group.map((link) => {
if ('content' in link) {
return <Fragment key={link.id}>{link.content}</Fragment>;
}
return (
<Item key={link.id} isIndented={isIndented} LinkWrapper={LinkWrapper} {...link} />
);
})}
</Group>
);
})}

View File

@ -121,7 +121,7 @@ const WithTooltipPure = ({
}
);
const tooltipComponent = (
const tooltipComponent = isVisible ? (
<Tooltip
placement={state?.placement}
ref={setTooltipRef}
@ -133,7 +133,7 @@ const WithTooltipPure = ({
{/* @ts-expect-error (non strict) */}
{typeof tooltip === 'function' ? tooltip({ onHide: () => onVisibleChange(false) }) : tooltip}
</Tooltip>
);
) : null;
return (
<>

View File

@ -8,17 +8,17 @@ export type TestProviderState = Addon_TestProviderState;
export type TestProviders = Record<TestProviderId, TestProviderConfig & TestProviderState>;
export type TestingModuleRunRequestStories = {
id: string;
name: string;
export type TestingModuleRunRequestStory = {
id: string; // button--primary
name: string; // Primary
};
export type TestingModuleRunRequestPayload = {
providerId: TestProviderId;
payload: {
stories: TestingModuleRunRequestStories[];
importPath: string;
componentPath: string;
importPath: string; // ./.../button.stories.tsx
stories?: TestingModuleRunRequestStory[];
componentPath?: string; // ./.../button.tsx
}[];
};

View File

@ -4,8 +4,13 @@ import { parse, stringify } from 'telejson';
// setting up the store, overriding set and get to use telejson
export default (_: any) => {
_.fn('set', function (key: string, data: object) {
// @ts-expect-error('this' implicitly has type 'any')
return _.set(this._area, this._in(key), stringify(data, { maxDepth: 50 }));
return _.set(
// @ts-expect-error('this' implicitly has type 'any')
this._area,
// @ts-expect-error('this' implicitly has type 'any')
this._in(key),
stringify(data, { maxDepth: 50, allowFunction: false })
);
});
_.fn('get', function (key: string, alt: string) {
// @ts-expect-error('this' implicitly has type 'any')

View File

@ -30,6 +30,7 @@ export interface SubAPI {
| Addon_Types
| Addon_TypesEnum.experimental_PAGE
| Addon_TypesEnum.experimental_SIDEBAR_BOTTOM
| Addon_TypesEnum.experimental_TEST_PROVIDER
| Addon_TypesEnum.experimental_SIDEBAR_TOP = Addon_Types,
>(
type: T

View File

@ -0,0 +1,186 @@
import { type API_StoryEntry, Addon_TypesEnum, type StoryId } from '@storybook/core/types';
import {
TESTING_MODULE_CANCEL_TEST_RUN_REQUEST,
TESTING_MODULE_RUN_ALL_REQUEST,
TESTING_MODULE_RUN_REQUEST,
TESTING_MODULE_WATCH_MODE_REQUEST,
type TestProviderId,
type TestProviderState,
type TestProviders,
type TestingModuleRunAllRequestPayload,
type TestingModuleRunRequestPayload,
} from '@storybook/core/core-events';
import invariant from 'tiny-invariant';
import type { ModuleFn } from '../lib/types';
export type SubState = {
testProviders: TestProviders;
};
const initialTestProviderState: TestProviderState = {
details: {} as { [key: string]: any },
cancellable: false,
cancelling: false,
running: false,
watching: false,
failed: false,
crashed: false,
};
interface RunOptions {
entryId?: StoryId;
}
export type SubAPI = {
getTestProviderState(id: string): TestProviderState | undefined;
updateTestProviderState(id: TestProviderId, update: Partial<TestProviderState>): void;
clearTestProviderState(id: TestProviderId): void;
runTestProvider(id: TestProviderId, options?: RunOptions): () => void;
setTestProviderWatchMode(id: TestProviderId, watchMode: boolean): void;
cancelTestProvider(id: TestProviderId): void;
};
export const init: ModuleFn<SubAPI, SubState> = ({ store, fullAPI }) => {
const state: SubState = {
testProviders: store.getState().testProviders || {},
};
const api: SubAPI = {
getTestProviderState(id) {
const { testProviders } = store.getState();
return testProviders?.[id];
},
updateTestProviderState(id, update) {
return store.setState(
({ testProviders }) => {
return { testProviders: { ...testProviders, [id]: { ...testProviders[id], ...update } } };
},
{ persistence: 'session' }
);
},
clearTestProviderState(id) {
const update = {
cancelling: false,
running: true,
failed: false,
crashed: false,
progress: undefined,
};
return store.setState(
({ testProviders }) => {
return { testProviders: { ...testProviders, [id]: { ...testProviders[id], ...update } } };
},
{ persistence: 'session' }
);
},
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];
invariant(entry, `No entry found in the index for id '${options.entryId}'`);
if (entry.type === 'story') {
const payload: TestingModuleRunRequestPayload = {
providerId: id,
payload: [
{
importPath: entry.importPath,
stories: [
{
id: entry.id,
name: entry.name,
},
],
},
],
};
fullAPI.emit(TESTING_MODULE_RUN_REQUEST, payload);
return () => api.cancelTestProvider(id);
}
const payloads = new Set<TestingModuleRunRequestPayload['payload'][0]>();
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);
}
};
findComponents(options.entryId);
const payload: TestingModuleRunRequestPayload = {
providerId: id,
payload: Array.from(payloads),
};
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 });
},
cancelTestProvider(id) {
api.updateTestProviderState(id, { cancelling: true });
fullAPI.emit(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, { providerId: id });
},
};
const initModule = async () => {
const initialState: TestProviders = Object.fromEntries(
Object.entries(fullAPI.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER)).map(
([id, config]) => [
id,
{
...config,
...initialTestProviderState,
...(state?.testProviders?.[id] || {}),
} as TestProviders[0],
]
)
);
store.setState({ testProviders: initialState }, { persistence: 'session' });
};
return { init: initModule, state, api };
};

View File

@ -50,6 +50,7 @@ import { noArrayMerge } from './lib/merge';
import type { ModuleFn } from './lib/types';
import * as addons from './modules/addons';
import * as channel from './modules/channel';
import * as testProviders from './modules/experimental_testmodule';
import * as globals from './modules/globals';
import * as layout from './modules/layout';
import * as notifications from './modules/notifications';
@ -79,6 +80,7 @@ export type State = layout.SubState &
stories.SubState &
refs.SubState &
notifications.SubState &
testProviders.SubState &
version.SubState &
url.SubState &
shortcuts.SubState &
@ -98,6 +100,7 @@ export type API = addons.SubAPI &
globals.SubAPI &
layout.SubAPI &
notifications.SubAPI &
testProviders.SubAPI &
shortcuts.SubAPI &
settings.SubAPI &
version.SubAPI &
@ -178,6 +181,7 @@ class ManagerProvider extends Component<ManagerProviderProps, State> {
addons,
layout,
notifications,
testProviders,
settings,
shortcuts,
stories,

View File

@ -1 +1 @@
export const version = '8.5.0-alpha.4';
export const version = '8.5.0-alpha.9';

View File

@ -0,0 +1,121 @@
import type { ComponentProps, FC, SyntheticEvent } from 'react';
import React, { useMemo, useState } from 'react';
import { TooltipLinkList, WithTooltip } from '@storybook/core/components';
import { type API_HashEntry, Addon_TypesEnum } from '@storybook/core/types';
import { EllipsisIcon } from '@storybook/icons';
import { type TestProviders } from '@storybook/core/core-events';
import { useStorybookState } from '@storybook/core/manager-api';
import type { API } from '@storybook/core/manager-api';
import type { Link } from '../../../components/components/tooltip/TooltipLinkList';
import { StatusButton } from './StatusButton';
import type { ExcludesNull } from './Tree';
import { ContextMenu } from './Tree';
export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) => {
const [hoverCount, setHoverCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const handlers = useMemo(() => {
return {
onMouseEnter: () => {
setHoverCount((c) => c + 1);
},
onOpen: (event: SyntheticEvent) => {
event.stopPropagation();
setIsOpen(true);
},
onClose: () => {
setIsOpen(false);
},
};
}, []);
/**
* Calculate the providerLinks whenever the user mouses over the container. We use an incrementer,
* instead of a simple boolean to ensure that the links are recalculated
*/
const providerLinks = useMemo(() => {
const testProviders = api.getElements(
Addon_TypesEnum.experimental_TEST_PROVIDER
) as any as TestProviders;
if (hoverCount) {
return generateTestProviderLinks(testProviders, context);
}
return [];
}, [api, context, hoverCount]);
const isRendered = providerLinks.length > 0 || links.length > 0;
return useMemo(() => {
return {
onMouseEnter: handlers.onMouseEnter,
node: isRendered ? (
<WithTooltip
data-displayed={isOpen ? 'on' : 'off'}
closeOnOutsideClick
placement="bottom-end"
data-testid="context-menu"
onVisibleChange={(visible) => {
if (!visible) {
handlers.onClose();
} else {
setIsOpen(true);
}
}}
tooltip={({ onHide }) => (
<LiveContextMenu context={context} links={links} onClick={onHide} />
)}
>
<StatusButton type="button" status={'pending'}>
<EllipsisIcon />
</StatusButton>
</WithTooltip>
) : null,
};
}, [context, handlers, isOpen, isRendered, links]);
};
/**
* This component re-subscribes to storybook's core state, hence the Live prefix. It is used to
* render the context menu for the sidebar. it self is a tooltip link list that renders the links
* provided to it. In addition to the links, it also renders the test providers.
*/
const LiveContextMenu: FC<{ context: API_HashEntry } & ComponentProps<typeof TooltipLinkList>> = ({
context,
links,
...rest
}) => {
const { testProviders } = useStorybookState();
const providerLinks: Link[] = generateTestProviderLinks(testProviders, context);
const groups = Array.isArray(links[0]) ? (links as Link[][]) : [links as Link[]];
const all = groups.concat([providerLinks]);
return <TooltipLinkList {...rest} links={all} />;
};
export function generateTestProviderLinks(
testProviders: TestProviders,
context: API_HashEntry
): Link[] {
return Object.entries(testProviders)
.map(([testProviderId, state]) => {
if (!state) {
return null;
}
const content = state.sidebarContextMenu?.({ context, state }, ContextMenu);
if (!content) {
return null;
}
return {
id: testProviderId,
content,
};
})
.filter(Boolean as any as ExcludesNull);
}

View File

@ -5,6 +5,7 @@ import { FileSearchList } from './FileSearchList';
const meta = {
component: FileSearchList,
title: 'Sidebar/FileSearchList',
args: {
onNewStory: fn(),
},

View File

@ -4,6 +4,7 @@ import { FileSearchListLoadingSkeleton } from './FileSearchListSkeleton';
const meta = {
component: FileSearchListLoadingSkeleton,
title: 'Sidebar/FileSearchListLoadingSkeleton',
} satisfies Meta<typeof FileSearchListLoadingSkeleton>;
export default meta;

View File

@ -8,6 +8,7 @@ import { FileSearchModal } from './FileSearchModal';
const meta = {
component: FileSearchModal,
title: 'Sidebar/FileSearchModal',
args: {
open: true,
setError: fn(),

View File

@ -4,6 +4,7 @@ import { FilterToggle } from './FilterToggle';
export default {
component: FilterToggle,
title: 'Sidebar/FilterToggle',
args: {
active: false,
onClick: fn(),

View File

@ -4,6 +4,7 @@ import { IconSymbols } from './IconSymbols';
const meta = {
component: IconSymbols,
title: 'Sidebar/IconSymbols',
} satisfies Meta<typeof IconSymbols>;
export default meta;

View File

@ -0,0 +1,89 @@
import React from 'react';
import { Button } from '@storybook/core/components';
import { styled } from '@storybook/core/theming';
import { EyeIcon, PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons';
import type { TestProviders } from '@storybook/core/core-events';
import { useStorybookApi } from '@storybook/core/manager-api';
const Info = styled.div({
display: 'flex',
flexDirection: 'column',
marginLeft: 6,
});
const Actions = styled.div({
display: 'flex',
gap: 6,
});
const TitleWrapper = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({
fontSize: theme.typography.size.s1,
fontWeight: crashed ? 'bold' : 'normal',
color: crashed ? theme.color.negativeText : theme.color.defaultText,
}));
const DescriptionWrapper = styled.div(({ theme }) => ({
fontSize: theme.typography.size.s1,
color: theme.barTextColor,
}));
export const LegacyRender = ({ ...state }: TestProviders[keyof TestProviders]) => {
const Description = state.description!;
const Title = state.title!;
const api = useStorybookApi();
return (
<>
<Info>
<TitleWrapper crashed={state.crashed} id="testing-module-title">
<Title {...state} />
</TitleWrapper>
<DescriptionWrapper id="testing-module-description">
<Description {...state} />
</DescriptionWrapper>
</Info>
<Actions>
{state.watchable && (
<Button
aria-label={`${state.watching ? 'Disable' : 'Enable'} watch mode for ${name}`}
variant="ghost"
padding="small"
active={state.watching}
onClick={() => api.setTestProviderWatchMode(state.id, !state.watching)}
disabled={state.crashed || state.running}
>
<EyeIcon />
</Button>
)}
{state.runnable && (
<>
{state.running && state.cancellable ? (
<Button
aria-label={`Stop ${name}`}
variant="ghost"
padding="small"
onClick={() => api.cancelTestProvider(state.id)}
disabled={state.cancelling}
>
<StopAltHollowIcon />
</Button>
) : (
<Button
aria-label={`Start ${state.name}`}
variant="ghost"
padding="small"
onClick={() => api.runTestProvider(state.id)}
disabled={state.crashed || state.running}
>
<PlayHollowIcon />
</Button>
)}
</>
)}
</Actions>
</>
);
};

View File

@ -1,7 +1,7 @@
import type { ComponentProps, FC } from 'react';
import React, { useMemo, useState } from 'react';
import React, { useState } from 'react';
import type { Button, TooltipLinkListLink } from '@storybook/core/components';
import type { Button } from '@storybook/core/components';
import { IconButton, TooltipLinkList, WithTooltip } from '@storybook/core/components';
import { styled } from '@storybook/core/theming';
import { CloseIcon, CogIcon } from '@storybook/icons';
@ -55,28 +55,11 @@ const MenuButtonGroup = styled.div({
gap: 4,
});
type ClickHandler = TooltipLinkListLink['onClick'];
const SidebarMenuList: FC<{
menu: MenuList;
onHide: () => void;
}> = ({ menu, onHide }) => {
const links = useMemo(
() =>
menu.map((group) =>
group.map(({ onClick, ...rest }) => ({
...rest,
onClick: ((event, item) => {
if (onClick) {
onClick(event, item);
}
onHide();
}) as ClickHandler,
}))
),
[menu, onHide]
);
return <TooltipLinkList links={links} />;
onClick: () => void;
}> = ({ menu, onClick }) => {
return <TooltipLinkList links={menu} onClick={onClick} />;
};
export interface SidebarMenuProps {
@ -118,7 +101,7 @@ export const SidebarMenu: FC<SidebarMenuProps> = ({ menu, isHighlighted, onClick
<WithTooltip
placement="top"
closeOnOutsideClick
tooltip={({ onHide }) => <SidebarMenuList onHide={onHide} menu={menu} />}
tooltip={({ onHide }) => <SidebarMenuList onClick={onHide} menu={menu} />}
onVisibleChange={setIsTooltipVisible}
>
<SidebarIconButton

View File

@ -21,12 +21,13 @@ import { useStorybookApi } from '@storybook/core/manager-api';
import { transparentize } from 'polished';
import type { NormalLink } from '../../../components/components/tooltip/TooltipLinkList';
import type { getStateType } from '../../utils/tree';
import type { RefType } from './types';
const { document, window: globalWindow } = global;
export type ClickHandler = TooltipLinkListLink['onClick'];
export type ClickHandler = NormalLink['onClick'];
export interface IndicatorIconProps {
type: ReturnType<typeof getStateType>;
}

View File

@ -1,5 +1,7 @@
import React from 'react';
import { fn } from '@storybook/test';
import { ManagerContext } from '@storybook/core/manager-api';
import { standardData as standardHeaderData } from './Heading.stories';
@ -8,6 +10,15 @@ import { Ref } from './Refs';
import { mockDataset } from './mockdata';
import type { RefType } from './types';
const managerContext = {
state: { docsOptions: {}, testProviders: {} },
api: {
on: fn().mockName('api::on'),
off: fn().mockName('api::off'),
getElements: fn(() => ({})),
},
} as any;
export default {
component: Ref,
title: 'Sidebar/Refs',
@ -16,7 +27,7 @@ export default {
globals: { sb_theme: 'side-by-side' },
decorators: [
(storyFn: any) => (
<ManagerContext.Provider value={{ state: { docsOptions: {} } } as any}>
<ManagerContext.Provider value={managerContext}>
<IconSymbols />
{storyFn()}
</ManagerContext.Provider>

View File

@ -33,6 +33,7 @@ const managerContext: any = {
autodocs: 'tag',
docsMode: false,
},
testProviders: {},
},
api: {
emit: fn().mockName('api::emit'),

View File

@ -24,7 +24,6 @@ import { Search } from './Search';
import { SearchResults } from './SearchResults';
import { SidebarBottom } from './SidebarBottom';
import { TagsFilter } from './TagsFilter';
import { TEST_PROVIDER_ID } from './Tree';
import type { CombinedDataset, Selection } from './types';
import { useLastViewed } from './useLastViewed';

View File

@ -1,37 +1,68 @@
import React from 'react';
import { Addon_TypesEnum } from '@storybook/core/types';
import type { Meta } from '@storybook/react';
import { fn } from '@storybook/test';
import { type API, ManagerContext } from '@storybook/core/manager-api';
import { SidebarBottomBase } from './SidebarBottom';
const managerContext: any = {
state: {
docsOptions: {
defaultName: 'Docs',
autodocs: 'tag',
docsMode: false,
},
testProviders: {
'component-tests': {
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
id: 'component-tests',
title: () => 'Component tests',
description: () => 'Ran 2 seconds ago',
runnable: true,
watchable: true,
},
'visual-tests': {
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
id: 'visual-tests',
title: () => 'Visual tests',
description: () => 'Not run',
runnable: true,
},
},
},
api: {
on: fn().mockName('api::on'),
off: fn().mockName('api::off'),
updateTestProviderState: fn(),
},
};
export default {
component: SidebarBottomBase,
title: 'Sidebar/SidebarBottom',
args: {
isDevelopment: true,
api: {
on: fn(),
off: fn(),
clearNotification: fn(),
updateTestProviderState: fn(),
emit: fn(),
experimental_setFilter: fn(),
getChannel: fn(),
getElements: fn(() => ({
'component-tests': {
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
id: 'component-tests',
title: () => 'Component tests',
description: () => 'Ran 2 seconds ago',
runnable: true,
watchable: true,
},
'visual-tests': {
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
id: 'visual-tests',
title: () => 'Visual tests',
description: () => 'Not run',
runnable: true,
},
})),
},
getElements: fn(() => ({})),
} as any as API,
},
};
decorators: [
(storyFn) => (
<ManagerContext.Provider value={managerContext}>{storyFn()}</ManagerContext.Provider>
),
],
} as Meta<typeof SidebarBottomBase>;
export const Errors = {
args: {

View File

@ -105,17 +105,7 @@ export const SidebarBottomBase = ({
const wrapperRef = useRef<HTMLDivElement | null>(null);
const [warningsActive, setWarningsActive] = useState(false);
const [errorsActive, setErrorsActive] = useState(false);
const [testProviders, setTestProviders] = useState<TestProviders>(() => {
let sessionState: TestProviders = {};
try {
sessionState = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
} catch (_) {}
return Object.fromEntries(
Object.entries(api.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER)).map(
([id, config]) => [id, { ...config, ...initialTestProviderState, ...sessionState[id] }]
)
);
});
const { testProviders } = useStorybookState();
const warnings = Object.values(status).filter((statusByAddonId) =>
Object.values(statusByAddonId).some((value) => value?.status === 'warn')
@ -126,55 +116,6 @@ export const SidebarBottomBase = ({
const hasWarnings = warnings.length > 0;
const hasErrors = errors.length > 0;
const updateTestProvider = useCallback(
(id: TestProviderId, update: Partial<TestProviderState>) =>
setTestProviders((state) => {
const newValue = { ...state, [id]: { ...state[id], ...update } };
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(newValue));
return newValue;
}),
[]
);
const clearState = useCallback(
({ providerId }: { providerId: TestProviderId }) => {
updateTestProvider(providerId, {
cancelling: false,
running: true,
failed: false,
crashed: false,
progress: undefined,
});
api.experimental_updateStatus(providerId, (state = {}) =>
Object.fromEntries(Object.keys(state).map((key) => [key, null]))
);
},
[api, updateTestProvider]
);
const onRunTests = useCallback(
(id: TestProviderId) => {
api.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id });
},
[api]
);
const onCancelTests = useCallback(
(id: TestProviderId) => {
updateTestProvider(id, { cancelling: true });
api.emit(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, { providerId: id });
},
[api, updateTestProvider]
);
const onSetWatchMode = useCallback(
(providerId: string, watchMode: boolean) => {
updateTestProvider(providerId, { watching: watchMode });
api.emit(TESTING_MODULE_WATCH_MODE_REQUEST, { providerId, watchMode });
},
[api, updateTestProvider]
);
useEffect(() => {
const spacer = spacerRef.current;
const wrapper = wrapperRef.current;
@ -196,15 +137,27 @@ export const SidebarBottomBase = ({
useEffect(() => {
const onCrashReport = ({ providerId, ...details }: TestingModuleCrashReportPayload) => {
updateTestProvider(providerId, { details, running: false, crashed: true, watching: false });
api.updateTestProviderState(providerId, {
details,
running: false,
crashed: true,
watching: false,
});
};
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') {
updateTestProvider(providerId, { ...result, running: false, failed: true });
api.updateTestProviderState(providerId, { ...result, running: false, failed: true });
} else {
const update = { ...result, running: result.status === 'pending' };
updateTestProvider(providerId, update);
api.updateTestProviderState(providerId, update);
const { mapStatusUpdate, ...state } = testProviders[providerId];
const statusUpdate = mapStatusUpdate?.({ ...state, ...update });
@ -214,18 +167,18 @@ export const SidebarBottomBase = ({
}
};
api.getChannel()?.on(TESTING_MODULE_CRASH_REPORT, onCrashReport);
api.getChannel()?.on(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
api.getChannel()?.on(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
api.on(TESTING_MODULE_CRASH_REPORT, onCrashReport);
api.on(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
api.on(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
return () => {
api.getChannel()?.off(TESTING_MODULE_CRASH_REPORT, onCrashReport);
api.getChannel()?.off(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
api.getChannel()?.off(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
api.off(TESTING_MODULE_CRASH_REPORT, onCrashReport);
api.off(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
api.off(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
};
}, [api, testProviders, updateTestProvider, clearState]);
}, [api, testProviders]);
const testProvidersArray = Object.values(testProviders);
const testProvidersArray = Object.values(testProviders || {});
if (!hasWarnings && !hasErrors && !testProvidersArray.length && !notifications.length) {
return null;
}
@ -244,9 +197,6 @@ export const SidebarBottomBase = ({
warningCount: warnings.length,
warningsActive,
setWarningsActive,
onRunTests,
onCancelTests,
onSetWatchMode,
}}
/>
)}

View File

@ -2,7 +2,8 @@ import { createContext, useContext } from 'react';
import type { API_StatusObject, API_StatusState, API_StatusValue, StoryId } from '@storybook/types';
import type { StoriesHash } from '../../../manager-api';
import type { StoriesHash } from '@storybook/core/manager-api';
import type { Item } from '../../container/Sidebar';
import { getDescendantIds } from '../../utils/tree';

View File

@ -5,6 +5,7 @@ import { TagsFilter } from './TagsFilter';
const meta = {
component: TagsFilter,
title: 'Sidebar/TagsFilter',
tags: ['haha'],
args: {
api: {

View File

@ -5,6 +5,7 @@ import { TagsFilterPanel } from './TagsFilterPanel';
const meta = {
component: TagsFilterPanel,
title: 'Sidebar/TagsFilterPanel',
args: {
toggleTag: fn(),
api: {

View File

@ -5,6 +5,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { fn, userEvent } from '@storybook/test';
import type { TestProviders } from '@storybook/core/core-events';
import { ManagerContext } from '@storybook/core/manager-api';
import { TestingModule } from './TestingModule';
@ -23,8 +24,13 @@ const testProviders: TestProviders[keyof TestProviders][] = [
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
id: 'component-tests',
name: 'Component tests',
title: () => 'Component tests',
description: () => 'Ran 2 seconds ago',
render: () => (
<>
Component tests
<br />
Ran 2 seconds ago
</>
),
runnable: true,
watchable: true,
...baseState,
@ -33,8 +39,13 @@ const testProviders: TestProviders[keyof TestProviders][] = [
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
id: 'visual-tests',
name: 'Visual tests',
title: () => 'Visual tests',
description: () => 'Not run',
render: () => (
<>
Visual tests
<br />
Not run
</>
),
runnable: true,
...baseState,
},
@ -42,15 +53,30 @@ const testProviders: TestProviders[keyof TestProviders][] = [
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
id: 'linting',
name: 'Linting',
title: () => 'Linting',
description: () => 'Watching for changes',
render: () => (
<>
Linting
<br />
Watching for changes
</>
),
...baseState,
watching: true,
},
];
const managerContext: any = {
api: {
runTestProvider: fn().mockName('api::runTestProvider'),
cancelTestProvider: fn().mockName('api::cancelTestProvider'),
updateTestProviderState: fn().mockName('api::updateTestProviderState'),
setTestProviderWatchMode: fn().mockName('api::setTestProviderWatchMode'),
},
};
const meta = {
component: TestingModule,
title: 'Sidebar/TestingModule',
args: {
testProviders,
errorCount: 0,
@ -59,11 +85,11 @@ const meta = {
warningCount: 0,
warningsActive: false,
setWarningsActive: fn(),
onRunTests: fn(),
onCancelTests: fn(),
onSetWatchMode: fn(),
},
decorators: [
(storyFn) => (
<ManagerContext.Provider value={managerContext}>{storyFn()}</ManagerContext.Provider>
),
(StoryFn) => (
<div style={{ maxWidth: 232 }}>
<StoryFn />
@ -180,8 +206,13 @@ export const Crashed: Story = {
testProviders: [
{
...testProviders[0],
title: () => "Component tests didn't complete",
description: () => 'Problems!',
render: () => (
<>
Component tests didn't complete
<br />
Problems!
</>
),
crashed: true,
},
...testProviders.slice(1),

View File

@ -1,18 +1,14 @@
import React, { type SyntheticEvent, 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,
EyeIcon,
PlayAllHollowIcon,
PlayHollowIcon,
StopAltHollowIcon,
} from '@storybook/icons';
import { ChevronSmallUpIcon, PlayAllHollowIcon } from '@storybook/icons';
import type { TestProviders } from '@storybook/core/core-events';
import { useStorybookApi } from '@storybook/core/manager-api';
import { WithTooltip } from '../../../components/components/tooltip/WithTooltip';
import { LegacyRender } from './LegacyRender';
const DEFAULT_HEIGHT = 500;
@ -148,43 +144,6 @@ const TestProvider = styled.div({
gap: 6,
});
const Info = styled.div({
display: 'flex',
flexDirection: 'column',
marginLeft: 6,
});
const Actions = styled.div({
display: 'flex',
gap: 6,
});
const TitleWrapper = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({
fontSize: theme.typography.size.s1,
fontWeight: crashed ? 'bold' : 'normal',
color: crashed ? theme.color.negativeText : theme.color.defaultText,
}));
const DescriptionWrapper = styled.div(({ theme }) => ({
fontSize: theme.typography.size.s1,
color: theme.barTextColor,
}));
const DynamicInfo = ({ state }: { state: TestProviders[keyof TestProviders] }) => {
const Description = state.description;
const Title = state.title;
return (
<Info>
<TitleWrapper crashed={state.crashed} id="testing-module-title">
<Title {...state} />
</TitleWrapper>
<DescriptionWrapper id="testing-module-description">
<Description {...state} />
</DescriptionWrapper>
</Info>
);
};
interface TestingModuleProps {
testProviders: TestProviders[keyof TestProviders][];
errorCount: number;
@ -193,9 +152,6 @@ interface TestingModuleProps {
warningCount: number;
warningsActive: boolean;
setWarningsActive: (active: boolean) => void;
onRunTests: (providerId: string) => void;
onCancelTests: (providerId: string) => void;
onSetWatchMode: (providerId: string, watchMode: boolean) => void;
}
export const TestingModule = ({
@ -206,10 +162,8 @@ export const TestingModule = ({
warningCount,
warningsActive,
setWarningsActive,
onRunTests,
onCancelTests,
onSetWatchMode,
}: TestingModuleProps) => {
const api = useStorybookApi();
const contentRef = useRef<HTMLDivElement>(null);
const [collapsed, setCollapsed] = useState(false);
const [maxHeight, setMaxHeight] = useState(DEFAULT_HEIGHT);
@ -243,50 +197,14 @@ export const TestingModule = ({
}}
>
<Content ref={contentRef}>
{testProviders.map((state) => (
<TestProvider key={state.id} data-module-id={state.id}>
<DynamicInfo state={state} />
<Actions>
{state.watchable && (
<Button
aria-label={`${state.watching ? 'Disable' : 'Enable'} watch mode for ${state.name}`}
variant="ghost"
padding="small"
active={state.watching}
onClick={() => onSetWatchMode(state.id, !state.watching)}
disabled={state.crashed || state.running}
>
<EyeIcon />
</Button>
)}
{state.runnable && (
<>
{state.running && state.cancellable ? (
<Button
aria-label={`Stop ${state.name}`}
variant="ghost"
padding="small"
onClick={() => onCancelTests(state.id)}
disabled={state.cancelling}
>
<StopAltHollowIcon />
</Button>
) : (
<Button
aria-label={`Start ${state.name}`}
variant="ghost"
padding="small"
onClick={() => onRunTests(state.id)}
disabled={state.crashed || state.running}
>
<PlayHollowIcon />
</Button>
)}
</>
)}
</Actions>
</TestProvider>
))}
{testProviders.map((state) => {
const { render: Render } = state;
return (
<TestProvider key={state.id} data-module-id={state.id}>
{Render ? <Render {...state} /> : <LegacyRender {...state} />}
</TestProvider>
);
})}
</Content>
</Collapsible>
@ -299,7 +217,7 @@ export const TestingModule = ({
e.stopPropagation();
testProviders
.filter((state) => !state.crashed && !state.running && state.runnable)
.forEach(({ id }) => onRunTests(id));
.forEach(({ id }) => api.runTestProvider(id));
}}
disabled={running}
>

View File

@ -2,9 +2,9 @@
import React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { expect, within } from '@storybook/test';
import { expect, fn, userEvent, within } from '@storybook/test';
import type { ComponentEntry, IndexHash } from '@storybook/core/manager-api';
import { type ComponentEntry, type IndexHash, ManagerContext } from '@storybook/core/manager-api';
import { action } from '@storybook/addon-actions';
@ -12,6 +12,55 @@ import { DEFAULT_REF_ID } from './Sidebar';
import { Tree } from './Tree';
import { index } from './mockdata.large';
const managerContext: any = {
state: {
docsOptions: {
defaultName: 'Docs',
autodocs: 'tag',
docsMode: false,
},
testProviders: {
'component-tests': {
type: 'experimental_TEST_PROVIDER',
id: 'component-tests',
render: () => 'Component tests',
sidebarContextMenu: () => <div>TEST_PROVIDER_CONTEXT_CONTENT</div>,
runnable: true,
watchable: true,
},
'visual-tests': {
type: 'experimental_TEST_PROVIDER',
id: 'visual-tests',
render: () => 'Visual tests',
sidebarContextMenu: () => null,
runnable: true,
},
},
},
api: {
on: fn().mockName('api::on'),
off: fn().mockName('api::off'),
emit: fn().mockName('api::emit'),
getElements: fn(() => ({
'component-tests': {
type: 'experimental_TEST_PROVIDER',
id: 'component-tests',
render: () => 'Component tests',
sidebarContextMenu: () => <div>TEST_PROVIDER_CONTEXT_CONTENT</div>,
runnable: true,
watchable: true,
},
'visual-tests': {
type: 'experimental_TEST_PROVIDER',
id: 'visual-tests',
render: () => 'Visual tests',
sidebarContextMenu: () => null,
runnable: true,
},
})),
},
};
const meta = {
component: Tree,
title: 'Sidebar/Tree',
@ -35,6 +84,11 @@ const meta = {
},
chromatic: { viewports: [380] },
},
decorators: [
(storyFn) => (
<ManagerContext.Provider value={managerContext}>{storyFn()}</ManagerContext.Provider>
),
],
} as Meta<typeof Tree>;
export default meta;
@ -233,3 +287,41 @@ export const SkipToCanvasLinkFocused: Story = {
await expect(link).toBeVisible();
},
};
// SkipToCanvas Link only shows on desktop widths
export const WithContextContent: Story = {
...DocsOnlySingleStoryComponents,
parameters: {
chromatic: { viewports: [1280] },
viewport: {
options: {
desktop: {
name: 'Desktop',
styles: {
width: '100%',
height: '100%',
},
},
},
},
},
globals: {
viewport: { value: 'desktop' },
},
play: async ({ canvasElement }) => {
const screen = await within(canvasElement);
const link = await screen.findByText('TooltipBuildList');
await userEvent.hover(link);
const contextButton = await screen.findByTestId('context-menu');
await userEvent.click(contextButton);
const body = await within(document.body);
const tooltip = await body.findByTestId('tooltip');
await expect(tooltip).toBeVisible();
expect(tooltip).toHaveTextContent('TEST_PROVIDER_CONTEXT_CONTENT');
},
};

View File

@ -1,8 +1,9 @@
import type { ComponentProps, MutableRefObject } from 'react';
import type { ComponentProps, FC, MutableRefObject } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import { Button, IconButton, TooltipLinkList, WithTooltip } from '@storybook/core/components';
import { Button, IconButton, ListItem } from '@storybook/core/components';
import { styled, useTheme } from '@storybook/core/theming';
import { type API_HashEntry, type API_StatusValue, type StoryId } from '@storybook/core/types';
import {
CollapseIcon as CollapseIconSvg,
ExpandAltIcon,
@ -11,7 +12,6 @@ import {
StatusWarnIcon,
SyncIcon,
} from '@storybook/icons';
import type { API_HashEntry, API_StatusValue, StoryId } from '@storybook/types';
import { PRELOAD_ENTRIES } from '@storybook/core/core-events';
import { useStorybookApi } from '@storybook/core/manager-api';
@ -26,6 +26,7 @@ import type {
import { transparentize } from 'polished';
import type { Link } from '../../../components/components/tooltip/TooltipLinkList';
import { getGroupStatus, getHighestStatus, statusMapping } from '../../utils/status';
import {
createId,
@ -35,6 +36,7 @@ import {
isStoryHoistable,
} from '../../utils/tree';
import { useLayout } from '../layout/LayoutProvider';
import { useContextMenu } from './ContextMenu';
import { IconSymbols, UseSymbol } from './IconSymbols';
import { StatusButton } from './StatusButton';
import { StatusContext, useStatusSummary } from './StatusContext';
@ -44,8 +46,7 @@ import type { Highlight, Item } from './types';
import type { ExpandAction, ExpandedState } from './useExpanded';
import { useExpanded } from './useExpanded';
export const TEST_ADDON_ID = 'storybook/test';
export const TEST_PROVIDER_ID = `${TEST_ADDON_ID}/test-provider`;
export type ExcludesNull = <T>(x: T | null) => x is T;
const Container = styled.div<{ hasOrphans: boolean }>((props) => ({
marginTop: props.hasOrphans ? 20 : 0,
@ -84,6 +85,22 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({
outline: 'none',
},
'& [data-displayed="off"]': {
visibility: 'hidden',
},
'&:hover [data-displayed="off"]': {
visibility: 'visible',
},
'& [data-displayed="on"] + *': {
display: 'none',
},
'&:hover [data-displayed="off"] + *': {
display: 'none',
},
'&[data-selected="true"]': {
color: theme.color.lightest,
background: theme.color.secondary,
@ -139,6 +156,40 @@ interface NodeProps {
collapsedData: Record<string, API_HashEntry>;
}
const SuccessStatusIcon: FC<ComponentProps<typeof StatusPassIcon>> = (props) => {
const theme = useTheme();
return <StatusPassIcon {...props} color={theme.color.positive} />;
};
const ErrorStatusIcon: FC<ComponentProps<typeof StatusFailIcon>> = (props) => {
const theme = useTheme();
return <StatusFailIcon {...props} color={theme.color.negative} />;
};
const WarnStatusIcon: FC<ComponentProps<typeof StatusWarnIcon>> = (props) => {
const theme = useTheme();
return <StatusWarnIcon {...props} color={theme.color.warning} />;
};
const PendingStatusIcon: FC<ComponentProps<typeof SyncIcon>> = (props) => {
const theme = useTheme();
return <SyncIcon {...props} size={12} color={theme.color.defaultText} />;
};
const StatusIconMap = {
success: <SuccessStatusIcon />,
error: <ErrorStatusIcon />,
warn: <WarnStatusIcon />,
pending: <PendingStatusIcon />,
unknown: null,
};
export const ContextMenu = {
ListItem,
};
const statusOrder: API_StatusValue[] = ['success', 'error', 'warn', 'pending', 'unknown'];
const Node = React.memo<NodeProps>(function Node({
item,
status,
@ -153,26 +204,82 @@ const Node = React.memo<NodeProps>(function Node({
isExpanded,
setExpanded,
onSelectStoryId,
collapsedData,
api,
}) {
const { isDesktop, isMobile, setMobileMenuOpen } = useLayout();
const theme = useTheme();
const { counts, statuses } = useStatusSummary(item);
if (!isDisplayed) {
return null;
}
const statusLinks = useMemo<Link[]>(() => {
if (item.type === 'story' || item.type === 'docs') {
return Object.entries(status || {})
.sort((a, b) => statusOrder.indexOf(a[1].status) - statusOrder.indexOf(b[1].status))
.map(([addonId, value]) => ({
id: addonId,
title: value.title,
description: value.description,
'aria-label': `Test status for ${value.title}: ${value.status}`,
icon: StatusIconMap[value.status],
onClick: () => {
onSelectStoryId(item.id);
value.onClick?.();
},
}));
}
if (item.type === 'component' || item.type === 'group') {
const links: Link[] = [];
if (counts.error) {
links.push({
id: 'errors',
icon: StatusIconMap.error,
title: `${counts.error} ${counts.error === 1 ? 'story' : 'stories'} with errors`,
onClick: () => {
const [firstStoryId, [firstError]] = Object.entries(statuses.error)[0];
onSelectStoryId(firstStoryId);
firstError.onClick?.();
},
});
}
if (counts.warn) {
links.push({
id: 'warnings',
icon: StatusIconMap.warn,
title: `${counts.warn} ${counts.warn === 1 ? 'story' : 'stories'} with warnings`,
onClick: () => {
const [firstStoryId, [firstWarning]] = Object.entries(statuses.warn)[0];
onSelectStoryId(firstStoryId);
firstWarning.onClick?.();
},
});
}
return links;
}
return [];
}, [
counts.error,
counts.warn,
item.id,
item.type,
onSelectStoryId,
status,
statuses.error,
statuses.warn,
]);
const id = createId(item.id, refId);
const contextMenu = useContextMenu(item, statusLinks, api);
if (item.type === 'story' || item.type === 'docs') {
const LeafNode = item.type === 'docs' ? DocumentNode : StoryNode;
const statusValue = getHighestStatus(Object.values(status || {}).map((s) => s.status));
const [icon, textColor] = statusMapping[statusValue];
const statusOrder: API_StatusValue[] = ['success', 'error', 'warn', 'pending', 'unknown'];
return (
<LeafNodeStyleWrapper
key={id}
@ -183,6 +290,7 @@ const Node = React.memo<NodeProps>(function Node({
data-parent-id={item.parent}
data-nodetype={item.type === 'docs' ? 'document' : 'story'}
data-highlightable={isDisplayed}
onMouseEnter={contextMenu.onMouseEnter}
>
<LeafNode
// @ts-expect-error (non strict)
@ -208,49 +316,17 @@ const Node = React.memo<NodeProps>(function Node({
<a href="#storybook-preview-wrapper">Skip to canvas</a>
</SkipToContentLink>
)}
{contextMenu.node}
{icon ? (
<WithTooltip
closeOnOutsideClick
closeOnTriggerHidden
onClick={(event) => event.stopPropagation()}
placement="bottom"
tooltip={({ onHide }) => (
<TooltipLinkList
links={Object.entries(status || {})
.sort(
(a, b) => statusOrder.indexOf(a[1].status) - statusOrder.indexOf(b[1].status)
)
.map(([addonId, value]) => ({
id: addonId,
title: value.title,
description: value.description,
'aria-label': `Test status for ${value.title}: ${value.status}`,
icon: {
success: <StatusPassIcon color={theme.color.positive} />,
error: <StatusFailIcon color={theme.color.negative} />,
warn: <StatusWarnIcon color={theme.color.warning} />,
pending: <SyncIcon size={12} color={theme.color.defaultText} />,
unknown: null,
}[value.status],
onClick: () => {
onSelectStoryId(item.id);
value.onClick?.();
onHide();
},
}))}
/>
)}
<StatusButton
aria-label={`Test status: ${statusValue}`}
role="status"
type="button"
status={statusValue}
selectedItem={isSelected}
>
<StatusButton
aria-label={`Test status: ${statusValue}`}
role="status"
type="button"
status={statusValue}
selectedItem={isSelected}
>
{icon}
</StatusButton>
</WithTooltip>
{icon}
</StatusButton>
) : null}
</LeafNodeStyleWrapper>
);
@ -302,39 +378,6 @@ const Node = React.memo<NodeProps>(function Node({
const color = itemStatus ? statusMapping[itemStatus][1] : null;
const BranchNode = item.type === 'component' ? ComponentNode : GroupNode;
const createLinks: (onHide: () => void) => ComponentProps<typeof TooltipLinkList>['links'] = (
onHide
) => {
const links = [];
if (counts.error) {
links.push({
id: 'errors',
icon: <StatusFailIcon color={theme.color.negative} />,
title: `${counts.error} ${counts.error === 1 ? 'story' : 'stories'} with errors`,
onClick: () => {
const [firstStoryId, [firstError]] = Object.entries(statuses.error)[0];
onSelectStoryId(firstStoryId);
firstError.onClick?.();
onHide();
},
});
}
if (counts.warn) {
links.push({
id: 'warnings',
icon: <StatusWarnIcon color={theme.color.gold} />,
title: `${counts.warn} ${counts.warn === 1 ? 'story' : 'stories'} with warnings`,
onClick: () => {
const [firstStoryId, [firstWarning]] = Object.entries(statuses.warn)[0];
onSelectStoryId(firstStoryId);
firstWarning.onClick?.();
onHide();
},
});
}
return links;
};
return (
<LeafNodeStyleWrapper
key={id}
@ -342,8 +385,9 @@ const Node = React.memo<NodeProps>(function Node({
data-ref-id={refId}
data-item-id={item.id}
data-parent-id={item.parent}
data-nodetype={item.type === 'component' ? 'component' : 'group'}
data-nodetype={item.type}
data-highlightable={isDisplayed}
onMouseEnter={contextMenu.onMouseEnter}
>
<BranchNode
id={id}
@ -374,19 +418,13 @@ const Node = React.memo<NodeProps>(function Node({
{(item.renderLabel as (i: typeof item, api: API) => React.ReactNode)?.(item, api) ||
item.name}
</BranchNode>
{contextMenu.node}
{['error', 'warn'].includes(itemStatus) && (
<WithTooltip
closeOnOutsideClick
onClick={(event) => event.stopPropagation()}
placement="bottom"
tooltip={({ onHide }) => <TooltipLinkList links={createLinks(onHide)} />}
>
<StatusButton type="button" status={itemStatus}>
<svg key="icon" viewBox="0 0 6 6" width="6" height="6" type="dot">
<UseSymbol type="dot" />
</svg>
</StatusButton>
</WithTooltip>
<StatusButton type="button" status={itemStatus}>
<svg key="icon" viewBox="0 0 6 6" width="6" height="6" type="dot">
<UseSymbol type="dot" />
</svg>
</StatusButton>
)}
</LeafNodeStyleWrapper>
);
@ -593,6 +631,10 @@ export const Tree = React.memo<{
const isDisplayed = !item.parent || ancestry[itemId].every((a: string) => expanded[a]);
if (isDisplayed === false) {
return null;
}
return (
<Node
api={api}

View File

@ -1,4 +1,4 @@
import type { State } from '../../manager-api/root';
import type { State } from '@storybook/core/manager-api';
export const defaultShortcuts: State['shortcuts'] = {
fullScreen: ['F'],

View File

@ -1,11 +1,12 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { FC, PropsWithChildren, ReactElement, ReactNode } from 'react';
import type { TestingModuleProgressReportProgress } from '../../core-events';
import type { ListItem } from '../../components';
import type { TestProviderConfig, TestingModuleProgressReportProgress } from '../../core-events';
import type { RenderData as RouterData } from '../../router/types';
import type { ThemeVars } from '../../theming/types';
import type { API_SidebarOptions } from './api';
import type { API_StatusState, API_StatusUpdate } from './api-stories';
import type { API_HashEntry, API_StatusState, API_StatusUpdate } from './api-stories';
import type {
Args,
ArgsStoryFn as ArgsStoryFnForFramework,
@ -28,6 +29,7 @@ export type Addon_Types = Exclude<
Addon_TypesEnum,
| Addon_TypesEnum.experimental_PAGE
| Addon_TypesEnum.experimental_SIDEBAR_BOTTOM
| Addon_TypesEnum.experimental_TEST_PROVIDER
| Addon_TypesEnum.experimental_SIDEBAR_TOP
>;
@ -329,7 +331,7 @@ export type Addon_Type =
| Addon_WrapperType
| Addon_SidebarBottomType
| Addon_SidebarTopType
| Addon_TestProviderType;
| Addon_TestProviderType<Addon_TestProviderState>;
export interface Addon_BaseType {
/**
* The title of the addon. This can be a simple string, but it can also be a
@ -472,8 +474,18 @@ export interface Addon_TestProviderType<
/** The unique id of the test provider. */
id: string;
name: string;
title: (state: Addon_TestProviderState<Details>) => ReactNode;
description: (state: Addon_TestProviderState<Details>) => ReactNode;
/** @deprecated Use render instead */
title?: (state: TestProviderConfig & Addon_TestProviderState<Details>) => ReactNode;
/** @deprecated Use render instead */
description?: (state: TestProviderConfig & Addon_TestProviderState<Details>) => ReactNode;
render?: (state: TestProviderConfig & Addon_TestProviderState<Details>) => ReactNode;
sidebarContextMenu?: (
options: {
context: API_HashEntry;
state: Addon_TestProviderState<Details>;
},
components: { ListItem: typeof ListItem }
) => ReactNode;
mapStatusUpdate?: (
state: Addon_TestProviderState<Details>
) => API_StatusUpdate | ((state: API_StatusState) => API_StatusUpdate);
@ -511,7 +523,7 @@ export interface Addon_TypesMapping extends Record<Addon_TypeBaseNames, Addon_Ba
[Addon_TypesEnum.experimental_PAGE]: Addon_PageType;
[Addon_TypesEnum.experimental_SIDEBAR_BOTTOM]: Addon_SidebarBottomType;
[Addon_TypesEnum.experimental_SIDEBAR_TOP]: Addon_SidebarTopType;
[Addon_TypesEnum.experimental_TEST_PROVIDER]: Addon_TestProviderType;
[Addon_TypesEnum.experimental_TEST_PROVIDER]: Addon_TestProviderType<Addon_TestProviderState>;
}
export type Addon_Loader<API> = (api: API) => void;

View File

@ -8,6 +8,7 @@ export type SupportedFrameworks =
| 'nextjs'
| 'preact-vite'
| 'preact-webpack5'
| 'react-native-web-vite'
| 'react-vite'
| 'react-webpack5'
| 'server-webpack5'

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/builder-manager",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Storybook manager builder",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/channels",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/client-logger",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/components",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Core Storybook Components",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/core-common",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Storybook framework-agnostic API",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/core-events",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Event names used in storybook core",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/core-server",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Storybook framework-agnostic API",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/csf-tools",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Parse and manipulate CSF and Storybook config files",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/docs-tools",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Shared utility functions for frameworks to implement docs",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/manager-api",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Core Storybook Manager API & Context",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/manager",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Core Storybook UI",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/node-logger",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/preview-api",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/preview",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/router",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Core Storybook Router",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/telemetry",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Telemetry logging for crash reports and usage statistics",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/theming",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Core Storybook Components",
"keywords": [
"storybook"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/types",
"version": "8.5.0-alpha.4",
"version": "8.5.0-alpha.9",
"description": "Core Storybook TS Types",
"keywords": [
"storybook"

Some files were not shown because too many files have changed in this diff Show More