From 1abc35379ec793e2d36ae17d09a6a3d635f830f8 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 6 Nov 2024 13:08:54 +0100 Subject: [PATCH 01/47] allow custom link in context menu in sidebar, add addon type for injecting anything into context menu --- code/.storybook/manager.tsx | 43 +++++++++++++++++++ .../components/tooltip/TooltipLinkList.tsx | 31 ++++++++++--- code/core/src/manager-api/lib/addons.ts | 2 + code/core/src/manager-api/modules/addons.ts | 1 + .../src/manager/components/sidebar/Tree.tsx | 15 ++++++- code/core/src/types/modules/addons.ts | 14 +++++- 6 files changed, 97 insertions(+), 9 deletions(-) diff --git a/code/.storybook/manager.tsx b/code/.storybook/manager.tsx index 1a06234f6e2e..66cad61aa7a8 100644 --- a/code/.storybook/manager.tsx +++ b/code/.storybook/manager.tsx @@ -1,4 +1,10 @@ +import React from 'react'; + +import { IconButton } from 'storybook/internal/components'; import { addons } from 'storybook/internal/manager-api'; +import { Addon_TypesEnum } from 'storybook/internal/types'; + +import { AdminIcon } from '@storybook/icons'; import { startCase } from 'es-toolkit/compat'; @@ -7,3 +13,40 @@ addons.setConfig({ renderLabel: ({ name, type }) => (type === 'story' ? name : startCase(name)), }, }); + +// TEMP, to demo new api +addons.add('my-addon', { + type: Addon_TypesEnum.experimental_CONTEXT, + render(props) { + console.log({ props }); + return
My Test
; + }, +}); + +// TEMP, to set status once, to have the status bullet show up +addons.register('my-addon', (api) => { + addons.add('my-addon2', { + type: Addon_TypesEnum.TOOL, + title: 'My Addon 2', + render(props) { + console.log({ props }); + return ( + { + const x = api.getCurrentStoryData(); + api.experimental_updateStatus('my-addon', { + [x.id]: { + description: 'This is a test', + status: 'error', + title: 'Test', + data: { foo: 'bar' }, + }, + }); + }} + > + + + ); + }, + }); +}); diff --git a/code/core/src/components/components/tooltip/TooltipLinkList.tsx b/code/core/src/components/components/tooltip/TooltipLinkList.tsx index 263c91d5bfa6..b785b8150d4f 100644 --- a/code/core/src/components/components/tooltip/TooltipLinkList.tsx +++ b/code/core/src/components/components/tooltip/TooltipLinkList.tsx @@ -1,8 +1,10 @@ -import type { ComponentProps, SyntheticEvent } from 'react'; +import type { ComponentProps, ReactElement, ReactNode, SyntheticEvent } from 'react'; import React, { useCallback } from 'react'; import { styled } from '@storybook/core/theming'; +import { has } from 'es-toolkit/compat'; + import type { LinkWrapperType, ListItemProps } from './ListItem'; import ListItem from './ListItem'; @@ -25,7 +27,7 @@ const Group = styled.div(({ theme }) => ({ }, })); -export interface Link extends Omit { +export interface NormalLink extends Omit { id: string; onClick?: ( event: SyntheticEvent, @@ -33,7 +35,19 @@ export interface Link extends Omit { ) => 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; + icon?: any; + content: ReactNode; +} + +interface ItemProps extends NormalLink { isIndented?: boolean; } @@ -63,9 +77,14 @@ export const TooltipLinkList = ({ links, LinkWrapper, ...props }: TooltipLinkLis .map((group, index) => { return ( link.id).join(`~${index}~`)}> - {group.map((link) => ( - - ))} + {group.map((link) => { + if ('content' in link) { + return link.content; + } + return ( + + ); + })} ); })} diff --git a/code/core/src/manager-api/lib/addons.ts b/code/core/src/manager-api/lib/addons.ts index a7aa438e0863..a66d054e52ed 100644 --- a/code/core/src/manager-api/lib/addons.ts +++ b/code/core/src/manager-api/lib/addons.ts @@ -20,6 +20,7 @@ import { global } from '@storybook/global'; import { logger } from '@storybook/core/client-logger'; import { SET_CONFIG } from '@storybook/core/core-events'; +import type { Addon_ContextType } from '../../types'; import type { API } from '../root'; import { mockChannel } from './storybook-channel-mock'; @@ -95,6 +96,7 @@ export class AddonStore { | Omit | Omit | Omit + | Omit | Omit ): void { const { type } = addon; diff --git a/code/core/src/manager-api/modules/addons.ts b/code/core/src/manager-api/modules/addons.ts index c620792e4fee..fe2788edcc52 100644 --- a/code/core/src/manager-api/modules/addons.ts +++ b/code/core/src/manager-api/modules/addons.ts @@ -28,6 +28,7 @@ export interface SubAPI { getElements: < T extends | Addon_Types + | Addon_TypesEnum.experimental_CONTEXT | Addon_TypesEnum.experimental_PAGE | Addon_TypesEnum.experimental_SIDEBAR_BOTTOM | Addon_TypesEnum.experimental_SIDEBAR_TOP = Addon_Types, diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index da9267f6caa4..1f88742e4ab4 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -3,6 +3,12 @@ import React, { useCallback, useMemo, useRef } from 'react'; import { Button, IconButton, TooltipLinkList, WithTooltip } from '@storybook/core/components'; import { styled, useTheme } from '@storybook/core/theming'; +import { + type API_HashEntry, + type API_StatusValue, + Addon_TypesEnum, + type StoryId, +} from '@storybook/core/types'; import { CollapseIcon as CollapseIconSvg, ExpandAltIcon, @@ -11,7 +17,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 +31,7 @@ import type { import { transparentize } from 'polished'; +import type { Link } from '../../../components/components/tooltip/TooltipLinkList'; import { getGroupStatus, getHighestStatus, statusMapping } from '../../utils/status'; import { createId, @@ -302,10 +308,15 @@ const Node = React.memo(function Node({ const color = itemStatus ? statusMapping[itemStatus][1] : null; const BranchNode = item.type === 'component' ? ComponentNode : GroupNode; + const elements = api.getElements(Addon_TypesEnum.experimental_CONTEXT); + const createLinks: (onHide: () => void) => ComponentProps['links'] = ( onHide ) => { - const links = []; + const links: Link[] = Object.entries(elements).map(([k, e]) => ({ + id: k, + content: e.render({ entry: item }), + })); if (counts.error) { links.push({ id: 'errors', diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index 2c995322b09d..89e0fb694be5 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -2,10 +2,11 @@ import type { FC, PropsWithChildren, ReactElement, ReactNode } from 'react'; import type { TestingModuleProgressReportProgress } from '../../core-events'; +import type { Addon } from '../../manager-api'; 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, @@ -326,6 +327,7 @@ export interface Addon_RenderOptions { export type Addon_Type = | Addon_BaseType | Addon_PageType + | Addon_ContextType | Addon_WrapperType | Addon_SidebarBottomType | Addon_SidebarTopType @@ -427,6 +429,12 @@ export interface Addon_PageType { */ render: FC; } +export interface Addon_ContextType { + type: Addon_TypesEnum.experimental_CONTEXT; + /** The unique id. */ + id: string; + render: FC<{ entry: API_HashEntry }>; +} export interface Addon_WrapperType { type: Addon_TypesEnum.PREVIEW; @@ -500,6 +508,7 @@ export type Addon_TestProviderState
{ [Addon_TypesEnum.PREVIEW]: Addon_WrapperType; + [Addon_TypesEnum.experimental_CONTEXT]: Addon_ContextType; [Addon_TypesEnum.experimental_PAGE]: Addon_PageType; [Addon_TypesEnum.experimental_SIDEBAR_BOTTOM]: Addon_SidebarBottomType; [Addon_TypesEnum.experimental_SIDEBAR_TOP]: Addon_SidebarTopType; @@ -577,4 +587,6 @@ export enum Addon_TypesEnum { experimental_SIDEBAR_TOP = 'sidebar-top', /** This adds items to the Testing Module in the sidebar. */ experimental_TEST_PROVIDER = 'test-provider', + /** This adds items to the Testing Module in the sidebar. */ + experimental_CONTEXT = 'context-menu', } From c69c1f30a742c6b3c8860d1bad4e687dda435f77 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 8 Nov 2024 16:53:33 +0100 Subject: [PATCH 02/47] hoist testProvider state to root, add WIP sidebar contextMenu --- code/.storybook/manager.tsx | 26 +++-- code/addons/test/src/manager.tsx | 3 + .../components/tooltip/WithTooltip.tsx | 4 +- .../src/core-events/data/testing-module.ts | 8 +- code/core/src/manager-api/lib/addons.ts | 2 - .../modules/experimental_testmodule.ts | 100 ++++++++++++++++++ code/core/src/manager-api/root.tsx | 4 + .../components/sidebar/SidebarBottom.tsx | 65 +++--------- .../src/manager/components/sidebar/Tree.tsx | 95 +++++++++++------ code/core/src/types/modules/addons.ts | 11 +- 10 files changed, 205 insertions(+), 113 deletions(-) create mode 100644 code/core/src/manager-api/modules/experimental_testmodule.ts diff --git a/code/.storybook/manager.tsx b/code/.storybook/manager.tsx index 66cad61aa7a8..5b6650ba4d75 100644 --- a/code/.storybook/manager.tsx +++ b/code/.storybook/manager.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { IconButton } from 'storybook/internal/components'; import { addons } from 'storybook/internal/manager-api'; @@ -14,22 +14,26 @@ addons.setConfig({ }, }); -// TEMP, to demo new api -addons.add('my-addon', { - type: Addon_TypesEnum.experimental_CONTEXT, - render(props) { - console.log({ props }); - return
My Test
; - }, -}); +// // TEMP, to demo new api +// addons.add('my-addon', { +// type: Addon_TypesEnum.experimental_CONTEXT, +// render(props) { +// console.log({ props }); + +// if (props.entry.type === 'docs') { +// return null; +// } + +// return
My Test
; +// }, +// }); // TEMP, to set status once, to have the status bullet show up addons.register('my-addon', (api) => { addons.add('my-addon2', { type: Addon_TypesEnum.TOOL, title: 'My Addon 2', - render(props) { - console.log({ props }); + render() { return ( { diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 0b558a3d4a20..6c6ace1a5c73 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -93,6 +93,9 @@ addons.register(ADDON_ID, (api) => { watchable: true, name: 'Component tests', + contextMenu: ({ context, state }) => { + return
Testing {state.running ? '!' : '?'}
; + }, title: ({ crashed, failed }) => crashed || failed ? 'Component tests failed' : 'Component tests', description: ({ failed, running, watching, progress, crashed, error }) => { diff --git a/code/core/src/components/components/tooltip/WithTooltip.tsx b/code/core/src/components/components/tooltip/WithTooltip.tsx index ace6ac6ea275..c6bddae93795 100644 --- a/code/core/src/components/components/tooltip/WithTooltip.tsx +++ b/code/core/src/components/components/tooltip/WithTooltip.tsx @@ -121,7 +121,7 @@ const WithTooltipPure = ({ } ); - const tooltipComponent = ( + const tooltipComponent = isVisible ? ( onVisibleChange(false) }) : tooltip} - ); + ) : null; return ( <> diff --git a/code/core/src/core-events/data/testing-module.ts b/code/core/src/core-events/data/testing-module.ts index d9e9640935a1..93d7d4545c87 100644 --- a/code/core/src/core-events/data/testing-module.ts +++ b/code/core/src/core-events/data/testing-module.ts @@ -9,16 +9,16 @@ export type TestProviderState = Addon_TestProviderState; export type TestProviders = Record; export type TestingModuleRunRequestStories = { - id: string; - name: string; + id: string; // button--primary + name: string; // Primary }; export type TestingModuleRunRequestPayload = { providerId: TestProviderId; payload: { stories: TestingModuleRunRequestStories[]; - importPath: string; - componentPath: string; + importPath: string; // ./.../button.stories.tsx + componentPath: string; // ./.../button.tsx }[]; }; diff --git a/code/core/src/manager-api/lib/addons.ts b/code/core/src/manager-api/lib/addons.ts index a66d054e52ed..a7aa438e0863 100644 --- a/code/core/src/manager-api/lib/addons.ts +++ b/code/core/src/manager-api/lib/addons.ts @@ -20,7 +20,6 @@ import { global } from '@storybook/global'; import { logger } from '@storybook/core/client-logger'; import { SET_CONFIG } from '@storybook/core/core-events'; -import type { Addon_ContextType } from '../../types'; import type { API } from '../root'; import { mockChannel } from './storybook-channel-mock'; @@ -96,7 +95,6 @@ export class AddonStore { | Omit | Omit | Omit - | Omit | Omit ): void { const { type } = addon; diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts new file mode 100644 index 000000000000..ed46e454be49 --- /dev/null +++ b/code/core/src/manager-api/modules/experimental_testmodule.ts @@ -0,0 +1,100 @@ +import { Addon_TypesEnum } from '@storybook/core/types'; + +import { + TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, + TESTING_MODULE_RUN_ALL_REQUEST, + type TestProviderId, + type TestProviderState, + type TestProviders, +} from '@storybook/core/core-events'; + +import type { ModuleFn } from '../lib/types'; + +export type SubState = { + testProviders: TestProviders; +}; + +const STORAGE_KEY = '@storybook/manager/test-providers'; + +const initialTestProviderState: TestProviderState = { + details: {} as { [key: string]: any }, + cancellable: false, + cancelling: false, + running: false, + watching: false, + failed: false, + crashed: false, +}; + +export type SubAPI = { + getTestproviderState(id: string): TestProviderState | undefined; + updateTestproviderState(id: TestProviderId, update: Partial): void; + clearTestproviderState(id: TestProviderId): void; + runTestprovider(id: TestProviderId): void; + cancelTestprovider(id: TestProviderId): void; +}; + +export const init: ModuleFn = ({ store, fullAPI }) => { + let sessionState: TestProviders = {}; + try { + sessionState = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}'); + } catch (_) { + // + } + + const state: SubState = { + testProviders: sessionState, + }; + + 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) { + fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id }); + + return () => api.cancelTestprovider(id); + }, + cancelTestprovider(id) { + api.updateTestproviderState(id, { cancelling: true }); + fullAPI.emit(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, { providerId: id }); + }, + }; + + const initModule = async () => { + const initialState = Object.fromEntries( + Object.entries(fullAPI.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER)).map( + ([id, config]) => [id, { ...config, ...initialTestProviderState, ...sessionState[id] }] + ) + ); + + store.setState({ testProviders: initialState }, { persistence: 'session' }); + }; + + return { init: initModule, state, api }; +}; diff --git a/code/core/src/manager-api/root.tsx b/code/core/src/manager-api/root.tsx index 1c150b47f4b3..a314e9372eb6 100644 --- a/code/core/src/manager-api/root.tsx +++ b/code/core/src/manager-api/root.tsx @@ -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 { addons, layout, notifications, + testproviders, settings, shortcuts, stories, diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index f6aae845bee3..b7a329df934e 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -104,18 +104,14 @@ export const SidebarBottomBase = ({ const spacerRef = useRef(null); const wrapperRef = useRef(null); const [warningsActive, setWarningsActive] = useState(false); + const { testProviders } = useStorybookState(); + const { + updateTestproviderState: updateTestProvider, + clearTestproviderState, + runTestprovider: onRunTests, + cancelTestprovider: onCancelTests, + } = useStorybookApi(); const [errorsActive, setErrorsActive] = useState(false); - const [testProviders, setTestProviders] = useState(() => { - 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 warnings = Object.values(status).filter((statusByAddonId) => Object.values(statusByAddonId).some((value) => value?.status === 'warn') @@ -126,45 +122,14 @@ export const SidebarBottomBase = ({ const hasWarnings = warnings.length > 0; const hasErrors = errors.length > 0; - const updateTestProvider = useCallback( - (id: TestProviderId, update: Partial) => - 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, - }); + clearTestproviderState(providerId); 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] + [api, clearTestproviderState] ); const onSetWatchMode = useCallback( @@ -214,14 +179,14 @@ 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]); diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index 1f88742e4ab4..f676d684ca38 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -170,6 +170,14 @@ const Node = React.memo(function Node({ return null; } + const StatusIconMap = { + success: , + error: , + warn: , + pending: , + unknown: null, + }; + const id = createId(item.id, refId); if (item.type === 'story' || item.type === 'docs') { const LeafNode = item.type === 'docs' ? DocumentNode : StoryNode; @@ -179,6 +187,42 @@ const Node = React.memo(function Node({ const statusOrder: API_StatusValue[] = ['success', 'error', 'warn', 'pending', 'unknown']; + function createLinks(onHide: () => void): Link[] | Link[][] { + const elements = api.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER); + const links: Link[] = Object.entries(elements) + .filter(([k, e]) => e.contextMenu) + .map(([k, e]) => { + const R = e.contextMenu; + + const state = api.getTestproviderState(k); + console.log({ R, k, e, s: state }); + + return { + id: k, + content: R && state ? : null, + }; + }); + + links.push( + ...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?.(); + onHide(); + }, + })) + ); + + return links; + } + return ( (function Node({ closeOnTriggerHidden onClick={(event) => event.stopPropagation()} placement="bottom" - tooltip={({ onHide }) => ( - 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: , - error: , - warn: , - pending: , - unknown: null, - }[value.status], - onClick: () => { - onSelectStoryId(item.id); - value.onClick?.(); - onHide(); - }, - }))} - /> - )} + tooltip={({ onHide }) => } > (function Node({ const color = itemStatus ? statusMapping[itemStatus][1] : null; const BranchNode = item.type === 'component' ? ComponentNode : GroupNode; - const elements = api.getElements(Addon_TypesEnum.experimental_CONTEXT); + function createLinks(onHide: () => void): Link[] | Link[][] { + const elements = api.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER); + const links: Link[] = Object.entries(elements) + .filter(([k, e]) => e.contextMenu) + .map(([k, e]) => { + const R = e.contextMenu; + + const state = api.getTestproviderState(k); + console.log({ R, k, e, s: state }); - const createLinks: (onHide: () => void) => ComponentProps['links'] = ( - onHide - ) => { - const links: Link[] = Object.entries(elements).map(([k, e]) => ({ - id: k, - content: e.render({ entry: item }), - })); + return { + id: k, + content: R && state ? : null, + }; + }); if (counts.error) { links.push({ id: 'errors', @@ -344,7 +369,7 @@ const Node = React.memo(function Node({ }); } return links; - }; + } return ( ; -} export interface Addon_WrapperType { type: Addon_TypesEnum.PREVIEW; @@ -482,6 +475,7 @@ export interface Addon_TestProviderType< name: string; title: (state: Addon_TestProviderState
) => ReactNode; description: (state: Addon_TestProviderState
) => ReactNode; + contextMenu?: FC<{ context: API_HashEntry; state: Addon_TestProviderState
}>; mapStatusUpdate?: ( state: Addon_TestProviderState
) => API_StatusUpdate | ((state: API_StatusState) => API_StatusUpdate); @@ -517,7 +511,6 @@ type Addon_TypeBaseNames = Exclude< export interface Addon_TypesMapping extends Record { [Addon_TypesEnum.PREVIEW]: Addon_WrapperType; - [Addon_TypesEnum.experimental_CONTEXT]: Addon_ContextType; [Addon_TypesEnum.experimental_PAGE]: Addon_PageType; [Addon_TypesEnum.experimental_SIDEBAR_BOTTOM]: Addon_SidebarBottomType; [Addon_TypesEnum.experimental_SIDEBAR_TOP]: Addon_SidebarTopType; From 716917d99e86d4d11abab5cbd10b1e20d7c2cbd1 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 12 Nov 2024 13:23:48 +0100 Subject: [PATCH 03/47] improve UI --- code/addons/test/src/manager.tsx | 25 +- .../components/tooltip/TooltipLinkList.tsx | 4 +- code/core/src/manager-api/modules/addons.ts | 1 + .../src/manager/components/sidebar/Tree.tsx | 315 +++++++++++------- code/core/src/types/modules/addons.ts | 10 +- 5 files changed, 233 insertions(+), 122 deletions(-) diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 6c6ace1a5c73..4f47bd2de63c 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -79,6 +79,20 @@ const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: nu ); }; +const COunter = () => { + const [count, setCount] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setCount((prev) => prev + 1); + }, 1000); + + return () => clearInterval(interval); + }, []); + + return
{count}
; +}; + addons.register(ADDON_ID, (api) => { const storybookBuilder = (globalThis as any).STORYBOOK_BUILDER || ''; if (storybookBuilder.includes('vite')) { @@ -94,7 +108,16 @@ addons.register(ADDON_ID, (api) => { name: 'Component tests', contextMenu: ({ context, state }) => { - return
Testing {state.running ? '!' : '?'}
; + if (context.type === 'docs') { + return null; + } + + return ( +
+ Testing {state?.progress?.percentageCompleted} {state.running ? '!' : '?'} +
+ ); + // return ; }, title: ({ crashed, failed }) => crashed || failed ? 'Component tests failed' : 'Component tests', diff --git a/code/core/src/components/components/tooltip/TooltipLinkList.tsx b/code/core/src/components/components/tooltip/TooltipLinkList.tsx index b785b8150d4f..c18fe5476d95 100644 --- a/code/core/src/components/components/tooltip/TooltipLinkList.tsx +++ b/code/core/src/components/components/tooltip/TooltipLinkList.tsx @@ -1,10 +1,8 @@ -import type { ComponentProps, ReactElement, ReactNode, SyntheticEvent } from 'react'; +import type { ComponentProps, ReactNode, SyntheticEvent } from 'react'; import React, { useCallback } from 'react'; import { styled } from '@storybook/core/theming'; -import { has } from 'es-toolkit/compat'; - import type { LinkWrapperType, ListItemProps } from './ListItem'; import ListItem from './ListItem'; diff --git a/code/core/src/manager-api/modules/addons.ts b/code/core/src/manager-api/modules/addons.ts index fe2788edcc52..50a2379c14ef 100644 --- a/code/core/src/manager-api/modules/addons.ts +++ b/code/core/src/manager-api/modules/addons.ts @@ -31,6 +31,7 @@ export interface SubAPI { | Addon_TypesEnum.experimental_CONTEXT | Addon_TypesEnum.experimental_PAGE | Addon_TypesEnum.experimental_SIDEBAR_BOTTOM + | Addon_TypesEnum.experimental_TEST_PROVIDER | Addon_TypesEnum.experimental_SIDEBAR_TOP = Addon_Types, >( type: T diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index f676d684ca38..dd76352d34ee 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -1,5 +1,5 @@ -import type { ComponentProps, MutableRefObject } from 'react'; -import React, { useCallback, useMemo, useRef } from 'react'; +import type { ComponentProps, FC, MutableRefObject } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { Button, IconButton, TooltipLinkList, WithTooltip } from '@storybook/core/components'; import { styled, useTheme } from '@storybook/core/theming'; @@ -11,6 +11,7 @@ import { } from '@storybook/core/types'; import { CollapseIcon as CollapseIconSvg, + EllipsisIcon, ExpandAltIcon, StatusFailIcon, StatusPassIcon, @@ -18,8 +19,8 @@ import { SyncIcon, } from '@storybook/icons'; -import { PRELOAD_ENTRIES } from '@storybook/core/core-events'; -import { useStorybookApi } from '@storybook/core/manager-api'; +import { PRELOAD_ENTRIES, type TestProviders } from '@storybook/core/core-events'; +import { useStorybookApi, useStorybookState } from '@storybook/core/manager-api'; import type { API, ComponentEntry, @@ -53,6 +54,8 @@ import { useExpanded } from './useExpanded'; export const TEST_ADDON_ID = 'storybook/test'; export const TEST_PROVIDER_ID = `${TEST_ADDON_ID}/test-provider`; +type ExcludesNull = (x: T | null) => x is T; + const Container = styled.div<{ hasOrphans: boolean }>((props) => ({ marginTop: props.hasOrphans ? 20 : 0, marginBottom: 20, @@ -145,6 +148,36 @@ interface NodeProps { collapsedData: Record; } +const SuccessStatusIcon: FC> = (props) => { + const theme = useTheme(); + return ; +}; + +const ErrorStatusIcon: FC> = (props) => { + const theme = useTheme(); + return ; +}; + +const WarnStatusIcon: FC> = (props) => { + const theme = useTheme(); + return ; +}; + +const PendingStatusIcon: FC> = (props) => { + const theme = useTheme(); + return ; +}; + +const StatusIconMap = { + success: , + error: , + warn: , + pending: , + unknown: null, +}; + +const statusOrder: API_StatusValue[] = ['success', 'error', 'warn', 'pending', 'unknown']; + const Node = React.memo(function Node({ item, status, @@ -159,70 +192,82 @@ const Node = React.memo(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 StatusIconMap = { - success: , - error: , - warn: , - pending: , - unknown: null, - }; + const statusLinks = useMemo(() => { + 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']; - - function createLinks(onHide: () => void): Link[] | Link[][] { - const elements = api.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER); - const links: Link[] = Object.entries(elements) - .filter(([k, e]) => e.contextMenu) - .map(([k, e]) => { - const R = e.contextMenu; - - const state = api.getTestproviderState(k); - console.log({ R, k, e, s: state }); - - return { - id: k, - content: R && state ? : null, - }; - }); - - links.push( - ...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?.(); - onHide(); - }, - })) - ); - - return links; - } - return ( (function Node({ data-parent-id={item.parent} data-nodetype={item.type === 'docs' ? 'document' : 'story'} data-highlightable={isDisplayed} + onMouseEnter={contextMenu.onMouseEnter} + onMouseLeave={contextMenu.onMouseLeave} > (function Node({ Skip to canvas )} - {icon ? ( - event.stopPropagation()} - placement="bottom" - tooltip={({ onHide }) => } - > + {contextMenu.node || + (icon ? ( (function Node({ > {icon} - - ) : null} + ) : null)} ); } @@ -327,50 +367,6 @@ const Node = React.memo(function Node({ const color = itemStatus ? statusMapping[itemStatus][1] : null; const BranchNode = item.type === 'component' ? ComponentNode : GroupNode; - function createLinks(onHide: () => void): Link[] | Link[][] { - const elements = api.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER); - const links: Link[] = Object.entries(elements) - .filter(([k, e]) => e.contextMenu) - .map(([k, e]) => { - const R = e.contextMenu; - - const state = api.getTestproviderState(k); - console.log({ R, k, e, s: state }); - - return { - id: k, - content: R && state ? : null, - }; - }); - if (counts.error) { - links.push({ - id: 'errors', - icon: , - 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: , - 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 ( (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} + onMouseLeave={contextMenu.onMouseLeave} > (function Node({ {(item.renderLabel as (i: typeof item, api: API) => React.ReactNode)?.(item, api) || item.name} - {['error', 'warn'].includes(itemStatus) && ( - event.stopPropagation()} - placement="bottom" - tooltip={({ onHide }) => } - > + {contextMenu.node || + (['error', 'warn'].includes(itemStatus) && ( - - )} + ))} ); } @@ -431,6 +423,76 @@ const Node = React.memo(function Node({ return null; }); +const useContextMenu = (context: API_HashEntry, links: Link[], api: API) => { + const [isItemHovered, setIsItemHovered] = useState(false); + const [isOpen, setIsOpen] = useState(false); + + const handlers = useMemo(() => { + return { + onMouseEnter: () => { + setIsItemHovered(true); + }, + onMouseLeave: () => { + setIsItemHovered(false); + }, + onOpen: (event: any) => { + event.stopPropagation(); + setIsOpen(true); + }, + onClose: () => { + setIsOpen(false); + }, + }; + }, []); + + return useMemo(() => { + const testProviders = api.getElements( + Addon_TypesEnum.experimental_TEST_PROVIDER + ) as any as TestProviders; + const providerLinks = generateTestProviderLinks(testProviders, context); + const shouldDisplayLinks = + (isItemHovered || isOpen) && (providerLinks.length > 0 || links.length > 0); + return { + onMouseEnter: handlers.onMouseEnter, + onMouseLeave: handlers.onMouseLeave, + node: shouldDisplayLinks ? ( + { + if (!visisble) { + handlers.onClose(); + } else { + setIsOpen(true); + } + }} + tooltip={({ onHide }) => ( + + )} + > + + + + + ) : null, + }; + }, [api, context, handlers, isItemHovered, isOpen, links]); +}; + +const LiveContextMenu: FC<{ context: API_HashEntry } & ComponentProps> = ({ + 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 ; +}; + const Root = React.memo(function Root({ setExpanded, isFullyExpanded, @@ -674,3 +736,26 @@ export const Tree = React.memo<{ ); }); + +function generateTestProviderLinks(testProviders: TestProviders, context: API_HashEntry): Link[] { + return Object.entries(testProviders) + .map(([k, e]) => { + const state = e; + + if (!state) { + return null; + } + + const content = e.contextMenu?.({ context, state }); + + if (!content) { + return null; + } + + return { + id: k, + content, + }; + }) + .filter(Boolean as any as ExcludesNull); +} diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index 118f4953b9a9..1be2e09126e1 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -29,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 >; @@ -330,7 +331,7 @@ export type Addon_Type = | Addon_WrapperType | Addon_SidebarBottomType | Addon_SidebarTopType - | Addon_TestProviderType; + | Addon_TestProviderType; export interface Addon_BaseType { /** * The title of the addon. This can be a simple string, but it can also be a @@ -475,7 +476,10 @@ export interface Addon_TestProviderType< name: string; title: (state: Addon_TestProviderState
) => ReactNode; description: (state: Addon_TestProviderState
) => ReactNode; - contextMenu?: FC<{ context: API_HashEntry; state: Addon_TestProviderState
}>; + contextMenu?: (options: { + context: API_HashEntry; + state: Addon_TestProviderState
; + }) => ReactNode; mapStatusUpdate?: ( state: Addon_TestProviderState
) => API_StatusUpdate | ((state: API_StatusState) => API_StatusUpdate); @@ -514,7 +518,7 @@ export interface Addon_TypesMapping extends Record; } export type Addon_Loader = (api: API) => void; From 82c5cde97ebb6e1e365ae7ed1b1a18c5fab17a12 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 12 Nov 2024 14:55:15 +0100 Subject: [PATCH 04/47] add UI in context --- code/addons/test/src/manager.tsx | 85 +++++++++++++------ .../components/tooltip/TooltipLinkList.tsx | 3 +- .../src/manager/components/sidebar/Tree.tsx | 14 ++- code/core/src/types/modules/addons.ts | 12 ++- 4 files changed, 81 insertions(+), 33 deletions(-) diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 4f47bd2de63c..08aa2ca74253 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -1,16 +1,34 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { type FC, type SyntheticEvent, useCallback, useEffect, useState } from 'react'; -import { AddonPanel, Badge, Link as LinkComponent, Spaced } from 'storybook/internal/components'; +import { + AddonPanel, + Badge, + Button, + Link as LinkComponent, + type ListItem, + Spaced, +} 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, + useAddonState, + useStorybookApi, +} from 'storybook/internal/manager-api'; +import { + type API_HashEntry, type API_StatusObject, type API_StatusValue, + type Addon_TestProviderState, type Addon_TestProviderType, Addon_TypesEnum, } from 'storybook/internal/types'; +import { PlayIcon } from '@storybook/icons'; +import { useTheme } from '@storybook/theming'; + import { Panel } from './Panel'; import { GlobalErrorModal } from './components/GlobalErrorModal'; import { ADDON_ID, PANEL_ID, TEST_PROVIDER_ID } from './constants'; @@ -79,18 +97,38 @@ const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: nu ); }; -const COunter = () => { - const [count, setCount] = useState(0); +const ContextMenuItem: FC<{ + context: API_HashEntry; + state: Addon_TestProviderState<{ + testResults: TestResult[]; + }>; + ListItem: typeof ListItem; +}> = ({ context, state, ListItem }) => { + const api = useStorybookApi(); - useEffect(() => { - const interval = setInterval(() => { - setCount((prev) => prev + 1); - }, 1000); + const onClick = useCallback( + (event: SyntheticEvent) => { + event.stopPropagation(); + // TODO - actually send along a sub-set based on `context` to test. + api.getChannel().emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: TEST_PROVIDER_ID }); + }, + [api] + ); - return () => clearInterval(interval); - }, []); + const theme = useTheme(); - return
{count}
; + return ( + + + + } + center={state.running ? 'Running...' : 'Run tests'} + onClick={onClick} + /> + ); }; addons.register(ADDON_ID, (api) => { @@ -107,17 +145,12 @@ addons.register(ADDON_ID, (api) => { watchable: true, name: 'Component tests', - contextMenu: ({ context, state }) => { + contextMenu: ({ context, state }, { ListItem }) => { if (context.type === 'docs') { return null; } - return ( -
- Testing {state?.progress?.percentageCompleted} {state.running ? '!' : '?'} -
- ); - // return ; + return ; }, title: ({ crashed, failed }) => crashed || failed ? 'Component tests failed' : 'Component tests', @@ -207,20 +240,20 @@ addons.register(ADDON_ID, (api) => { }>); } + const filter = ({ state }: Combo) => { + return { + storyId: state.storyId, + }; + }; + addons.add(PANEL_ID, { type: types.PANEL, title: Title, match: ({ viewMode }) => viewMode === 'story', render: ({ active }) => { - const newLocal = useCallback(({ state }: Combo) => { - return { - storyId: state.storyId, - }; - }, []); - return ( - {({ storyId }) => } + {({ storyId }) => } ); }, diff --git a/code/core/src/components/components/tooltip/TooltipLinkList.tsx b/code/core/src/components/components/tooltip/TooltipLinkList.tsx index c18fe5476d95..4fa393ee3724 100644 --- a/code/core/src/components/components/tooltip/TooltipLinkList.tsx +++ b/code/core/src/components/components/tooltip/TooltipLinkList.tsx @@ -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 }) => ({ diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index dd76352d34ee..8275f585d872 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -1,7 +1,13 @@ import type { ComponentProps, FC, MutableRefObject } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { Button, IconButton, TooltipLinkList, WithTooltip } from '@storybook/core/components'; +import { + Button, + IconButton, + ListItem, + TooltipLinkList, + WithTooltip, +} from '@storybook/core/components'; import { styled, useTheme } from '@storybook/core/theming'; import { type API_HashEntry, @@ -176,6 +182,10 @@ const StatusIconMap = { unknown: null, }; +const ContextMenu = { + ListItem, +}; + const statusOrder: API_StatusValue[] = ['success', 'error', 'warn', 'pending', 'unknown']; const Node = React.memo(function Node({ @@ -746,7 +756,7 @@ function generateTestProviderLinks(testProviders: TestProviders, context: API_Ha return null; } - const content = e.contextMenu?.({ context, state }); + const content = e.contextMenu?.({ context, state }, ContextMenu); if (!content) { return null; diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index 1be2e09126e1..18c7a6c1f833 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { FC, PropsWithChildren, ReactElement, ReactNode } from 'react'; +import type { ListItem } from '../../components'; import type { TestingModuleProgressReportProgress } from '../../core-events'; import type { Addon, StoryEntry } from '../../manager-api'; import type { RenderData as RouterData } from '../../router/types'; @@ -476,10 +477,13 @@ export interface Addon_TestProviderType< name: string; title: (state: Addon_TestProviderState
) => ReactNode; description: (state: Addon_TestProviderState
) => ReactNode; - contextMenu?: (options: { - context: API_HashEntry; - state: Addon_TestProviderState
; - }) => ReactNode; + contextMenu?: ( + options: { + context: API_HashEntry; + state: Addon_TestProviderState
; + }, + components: { ListItem: typeof ListItem } + ) => ReactNode; mapStatusUpdate?: ( state: Addon_TestProviderState
) => API_StatusUpdate | ((state: API_StatusState) => API_StatusUpdate); From 7bfc2add3dcc3fb9d552b5dbb6ee82ae8174173e Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 12 Nov 2024 15:00:38 +0100 Subject: [PATCH 05/47] cleanup --- code/.storybook/manager.tsx | 47 ------------------------------------- 1 file changed, 47 deletions(-) diff --git a/code/.storybook/manager.tsx b/code/.storybook/manager.tsx index 5b6650ba4d75..1a06234f6e2e 100644 --- a/code/.storybook/manager.tsx +++ b/code/.storybook/manager.tsx @@ -1,10 +1,4 @@ -import React, { useEffect, useState } from 'react'; - -import { IconButton } from 'storybook/internal/components'; import { addons } from 'storybook/internal/manager-api'; -import { Addon_TypesEnum } from 'storybook/internal/types'; - -import { AdminIcon } from '@storybook/icons'; import { startCase } from 'es-toolkit/compat'; @@ -13,44 +7,3 @@ addons.setConfig({ renderLabel: ({ name, type }) => (type === 'story' ? name : startCase(name)), }, }); - -// // TEMP, to demo new api -// addons.add('my-addon', { -// type: Addon_TypesEnum.experimental_CONTEXT, -// render(props) { -// console.log({ props }); - -// if (props.entry.type === 'docs') { -// return null; -// } - -// return
My Test
; -// }, -// }); - -// TEMP, to set status once, to have the status bullet show up -addons.register('my-addon', (api) => { - addons.add('my-addon2', { - type: Addon_TypesEnum.TOOL, - title: 'My Addon 2', - render() { - return ( - { - const x = api.getCurrentStoryData(); - api.experimental_updateStatus('my-addon', { - [x.id]: { - description: 'This is a test', - status: 'error', - title: 'Test', - data: { foo: 'bar' }, - }, - }); - }} - > - - - ); - }, - }); -}); From 15e0f2cce57fe2da1abdfda9cf899b0ff6494efc Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 12 Nov 2024 15:01:23 +0100 Subject: [PATCH 06/47] cleanup --- code/addons/test/src/manager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 08aa2ca74253..59a7962162dc 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -17,6 +17,7 @@ import { useAddonState, useStorybookApi, } from 'storybook/internal/manager-api'; +import { useTheme } from 'storybook/internal/theming'; import { type API_HashEntry, type API_StatusObject, @@ -27,7 +28,6 @@ import { } from 'storybook/internal/types'; import { PlayIcon } from '@storybook/icons'; -import { useTheme } from '@storybook/theming'; import { Panel } from './Panel'; import { GlobalErrorModal } from './components/GlobalErrorModal'; From 9faaa5925f8393f97122776616a408438d861b31 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 12 Nov 2024 15:10:22 +0100 Subject: [PATCH 07/47] cleanup --- code/core/src/manager-api/modules/addons.ts | 1 - code/core/src/types/modules/addons.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/code/core/src/manager-api/modules/addons.ts b/code/core/src/manager-api/modules/addons.ts index 50a2379c14ef..16a35fcfc9ad 100644 --- a/code/core/src/manager-api/modules/addons.ts +++ b/code/core/src/manager-api/modules/addons.ts @@ -28,7 +28,6 @@ export interface SubAPI { getElements: < T extends | Addon_Types - | Addon_TypesEnum.experimental_CONTEXT | Addon_TypesEnum.experimental_PAGE | Addon_TypesEnum.experimental_SIDEBAR_BOTTOM | Addon_TypesEnum.experimental_TEST_PROVIDER diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index 18c7a6c1f833..ebacb981b9b9 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -510,7 +510,6 @@ export type Addon_TestProviderState
Date: Tue, 12 Nov 2024 16:04:44 +0100 Subject: [PATCH 08/47] fixes --- .../components/sidebar/Refs.stories.tsx | 13 ++++++- .../components/sidebar/Sidebar.stories.tsx | 1 + .../sidebar/SidebarBottom.stories.tsx | 35 +++++++++++++++++-- .../components/sidebar/SidebarBottom.tsx | 2 +- .../components/sidebar/Tree.stories.tsx | 25 +++++++++++-- 5 files changed, 70 insertions(+), 6 deletions(-) diff --git a/code/core/src/manager/components/sidebar/Refs.stories.tsx b/code/core/src/manager/components/sidebar/Refs.stories.tsx index a26cad976dcb..a042970beadc 100644 --- a/code/core/src/manager/components/sidebar/Refs.stories.tsx +++ b/code/core/src/manager/components/sidebar/Refs.stories.tsx @@ -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) => ( - + {storyFn()} diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx index dcc6b06096c2..53f22957f17d 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx @@ -33,6 +33,7 @@ const managerContext: any = { autodocs: 'tag', docsMode: false, }, + testProviders: {}, }, api: { emit: fn().mockName('api::emit'), diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx index 8d7d2c42f488..4e611ddd31a0 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx @@ -1,14 +1,40 @@ +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: {}, + }, + api: { + on: fn().mockName('api::on'), + off: fn().mockName('api::off'), + getElements: fn(() => ({})), + updateTestproviderState: fn(), + }, +}; + export default { component: SidebarBottomBase, args: { isDevelopment: true, + api: { + on: fn(), + off: fn(), clearNotification: fn(), + updateTestproviderState: fn(), emit: fn(), experimental_setFilter: fn(), getChannel: fn(), @@ -29,9 +55,14 @@ export default { runnable: true, }, })), - }, + } as any as API, }, -}; + decorators: [ + (storyFn) => ( + {storyFn()} + ), + ], +} as Meta; export const Errors = { args: { diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index b7a329df934e..9bddc0f1269c 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -190,7 +190,7 @@ export const SidebarBottomBase = ({ }; }, [api, testProviders, updateTestProvider, clearState]); - const testProvidersArray = Object.values(testProviders); + const testProvidersArray = Object.values(testProviders || {}); if (!hasWarnings && !hasErrors && !testProvidersArray.length && !notifications.length) { return null; } diff --git a/code/core/src/manager/components/sidebar/Tree.stories.tsx b/code/core/src/manager/components/sidebar/Tree.stories.tsx index a8f3a227b2dc..fec1ec648057 100644 --- a/code/core/src/manager/components/sidebar/Tree.stories.tsx +++ b/code/core/src/manager/components/sidebar/Tree.stories.tsx @@ -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, 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,22 @@ 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: {}, + }, + api: { + on: fn().mockName('api::on'), + off: fn().mockName('api::off'), + getElements: fn(() => ({})), + }, +}; + const meta = { component: Tree, title: 'Sidebar/Tree', @@ -35,6 +51,11 @@ const meta = { }, chromatic: { viewports: [380] }, }, + decorators: [ + (storyFn) => ( + {storyFn()} + ), + ], } as Meta; export default meta; From 9d4eae2cfb55f2fe801702fa3246a1cbeb64ce68 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 13 Nov 2024 10:20:12 +0100 Subject: [PATCH 09/47] remove contextMenu from test addon until it's ready for primetime --- code/addons/test/src/manager.tsx | 13 +++++++------ .../modules/experimental_testmodule.ts | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 59a7962162dc..727b421cc42c 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -145,13 +145,14 @@ addons.register(ADDON_ID, (api) => { watchable: true, name: 'Component tests', - contextMenu: ({ context, state }, { ListItem }) => { - if (context.type === 'docs') { - return null; - } + // disabled for now + // contextMenu: ({ context, state }, { ListItem }) => { + // if (context.type === 'docs') { + // return null; + // } - return ; - }, + // return ; + // }, title: ({ crashed, failed }) => crashed || failed ? 'Component tests failed' : 'Component tests', description: ({ failed, running, watching, progress, crashed, error }) => { diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts index ed46e454be49..c7fb7eeec785 100644 --- a/code/core/src/manager-api/modules/experimental_testmodule.ts +++ b/code/core/src/manager-api/modules/experimental_testmodule.ts @@ -26,11 +26,15 @@ const initialTestProviderState: TestProviderState = { crashed: false, }; +interface RunOptions { + selection?: string[]; +} + export type SubAPI = { getTestproviderState(id: string): TestProviderState | undefined; updateTestproviderState(id: TestProviderId, update: Partial): void; clearTestproviderState(id: TestProviderId): void; - runTestprovider(id: TestProviderId): void; + runTestprovider(id: TestProviderId, options?: RunOptions): void; cancelTestprovider(id: TestProviderId): void; }; @@ -75,8 +79,13 @@ export const init: ModuleFn = ({ store, fullAPI }) => { { persistence: 'session' } ); }, - runTestprovider(id) { - fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id }); + runTestprovider(id, options) { + if (options?.selection) { + const listOfFiles = []; + // fullAPI.emit(TESTING_MODULE_RUN_REQUEST, { providerId: id, selection: [] }); + } else { + fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id }); + } return () => api.cancelTestprovider(id); }, From 7ad41a75b18e34672277124cf8a0c80816748c00 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 13 Nov 2024 12:05:10 +0100 Subject: [PATCH 10/47] improvements --- code/addons/test/src/manager.tsx | 3 +- .../components/tooltip/TooltipLinkList.tsx | 3 +- .../modules/experimental_testmodule.ts | 34 ++++++++++++------- .../src/manager/components/sidebar/Tree.tsx | 16 ++++----- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 727b421cc42c..00d9bb32d03e 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -109,8 +109,7 @@ const ContextMenuItem: FC<{ const onClick = useCallback( (event: SyntheticEvent) => { event.stopPropagation(); - // TODO - actually send along a sub-set based on `context` to test. - api.getChannel().emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: TEST_PROVIDER_ID }); + api.runTestprovider(TEST_PROVIDER_ID, { selection: [context.id] }); }, [api] ); diff --git a/code/core/src/components/components/tooltip/TooltipLinkList.tsx b/code/core/src/components/components/tooltip/TooltipLinkList.tsx index 4fa393ee3724..844a4ed228b2 100644 --- a/code/core/src/components/components/tooltip/TooltipLinkList.tsx +++ b/code/core/src/components/components/tooltip/TooltipLinkList.tsx @@ -42,7 +42,6 @@ export type Link = CustomLink | NormalLink; */ interface CustomLink { id: string; - icon?: any; content: ReactNode; } @@ -68,7 +67,7 @@ export interface TooltipLinkListProps extends ComponentProps { 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 ( {groups diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts index c7fb7eeec785..b9b2185590cd 100644 --- a/code/core/src/manager-api/modules/experimental_testmodule.ts +++ b/code/core/src/manager-api/modules/experimental_testmodule.ts @@ -14,8 +14,6 @@ export type SubState = { testProviders: TestProviders; }; -const STORAGE_KEY = '@storybook/manager/test-providers'; - const initialTestProviderState: TestProviderState = { details: {} as { [key: string]: any }, cancellable: false, @@ -39,15 +37,8 @@ export type SubAPI = { }; export const init: ModuleFn = ({ store, fullAPI }) => { - let sessionState: TestProviders = {}; - try { - sessionState = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}'); - } catch (_) { - // - } - const state: SubState = { - testProviders: sessionState, + testProviders: store.getState().testProviders, }; const api: SubAPI = { @@ -81,7 +72,17 @@ export const init: ModuleFn = ({ store, fullAPI }) => { }, runTestprovider(id, options) { if (options?.selection) { - const listOfFiles = []; + const listOfFiles: string[] = []; + + // TODO: get actual list and emit, this notification is for development purposes + fullAPI.addNotification({ + id: 'testing-module', + + content: { + headline: 'Running tests', + subHeadline: `Running tests for ${listOfFiles} stories`, + }, + }); // fullAPI.emit(TESTING_MODULE_RUN_REQUEST, { providerId: id, selection: [] }); } else { fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id }); @@ -96,9 +97,16 @@ export const init: ModuleFn = ({ store, fullAPI }) => { }; const initModule = async () => { - const initialState = Object.fromEntries( + const initialState: TestProviders = Object.fromEntries( Object.entries(fullAPI.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER)).map( - ([id, config]) => [id, { ...config, ...initialTestProviderState, ...sessionState[id] }] + ([id, config]) => [ + id, + { + ...config, + ...initialTestProviderState, + ...state.testProviders[id], + } as TestProviders[0], + ] ) ); diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index 8275f585d872..a4fe88248f19 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -1,4 +1,4 @@ -import type { ComponentProps, FC, MutableRefObject } from 'react'; +import type { ComponentProps, FC, MutableRefObject, SyntheticEvent } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { @@ -445,7 +445,7 @@ const useContextMenu = (context: API_HashEntry, links: Link[], api: API) => { onMouseLeave: () => { setIsItemHovered(false); }, - onOpen: (event: any) => { + onOpen: (event: SyntheticEvent) => { event.stopPropagation(); setIsOpen(true); }, @@ -470,8 +470,8 @@ const useContextMenu = (context: API_HashEntry, links: Link[], api: API) => { closeOnOutsideClick onClick={handlers.onOpen} placement="bottom-end" - onVisibleChange={(visisble) => { - if (!visisble) { + onVisibleChange={(visible) => { + if (!visible) { handlers.onClose(); } else { setIsOpen(true); @@ -749,21 +749,19 @@ export const Tree = React.memo<{ function generateTestProviderLinks(testProviders: TestProviders, context: API_HashEntry): Link[] { return Object.entries(testProviders) - .map(([k, e]) => { - const state = e; - + .map(([testProviderId, state]) => { if (!state) { return null; } - const content = e.contextMenu?.({ context, state }, ContextMenu); + const content = state.contextMenu?.({ context, state }, ContextMenu); if (!content) { return null; } return { - id: k, + id: testProviderId, content, }; }) From 4ea557eaeb3ba37bead8f1b3f1cbde3ee286385c Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 13 Nov 2024 12:23:00 +0100 Subject: [PATCH 11/47] fix build race condition --- code/core/scripts/helpers/generateTypesFiles.ts | 6 ++++++ code/core/scripts/prep.ts | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/code/core/scripts/helpers/generateTypesFiles.ts b/code/core/scripts/helpers/generateTypesFiles.ts index fd7a7eff789d..e3a46d3567be 100644 --- a/code/core/scripts/helpers/generateTypesFiles.ts +++ b/code/core/scripts/helpers/generateTypesFiles.ts @@ -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, @@ -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.ts')) { + console.log('Modifying theme types'); + await modifyThemeTypes(); + } } }); }) diff --git a/code/core/scripts/prep.ts b/code/core/scripts/prep.ts index f8726e79df48..4d81c65b1d7e 100644 --- a/code/core/scripts/prep.ts +++ b/code/core/scripts/prep.ts @@ -66,7 +66,6 @@ async function run() { await generateTypesMapperFiles(entries); await modifyThemeTypes(); await generateTypesFiles(entries, isOptimized, cwd); - await modifyThemeTypes(); }) ); From 74972a78e39bbd4d891739d8d5362c3e23d51590 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 13 Nov 2024 13:09:33 +0100 Subject: [PATCH 12/47] refactor --- .../components/sidebar/ContextMenu.tsx | 114 ++++++++++++++++ .../manager/components/sidebar/Sidebar.tsx | 1 - .../src/manager/components/sidebar/Tree.tsx | 123 ++---------------- 3 files changed, 123 insertions(+), 115 deletions(-) create mode 100644 code/core/src/manager/components/sidebar/ContextMenu.tsx diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx new file mode 100644 index 000000000000..7d965005497d --- /dev/null +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -0,0 +1,114 @@ +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 [isItemHovered, setIsItemHovered] = useState(false); + const [isOpen, setIsOpen] = useState(false); + + const handlers = useMemo(() => { + return { + onMouseEnter: () => { + setIsItemHovered(true); + }, + onMouseLeave: () => { + setIsItemHovered(false); + }, + onOpen: (event: SyntheticEvent) => { + event.stopPropagation(); + setIsOpen(true); + }, + onClose: () => { + setIsOpen(false); + }, + }; + }, []); + + return useMemo(() => { + const testProviders = api.getElements( + Addon_TypesEnum.experimental_TEST_PROVIDER + ) as any as TestProviders; + const providerLinks = generateTestProviderLinks(testProviders, context); + const shouldDisplayLinks = + (isItemHovered || isOpen) && (providerLinks.length > 0 || links.length > 0); + return { + onMouseEnter: handlers.onMouseEnter, + onMouseLeave: handlers.onMouseLeave, + node: shouldDisplayLinks ? ( + { + if (!visible) { + handlers.onClose(); + } else { + setIsOpen(true); + } + }} + tooltip={({ onHide }) => ( + + )} + > + + + + + ) : null, + }; + }, [api, context, handlers, isItemHovered, isOpen, 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> = ({ + 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 ; +}; + +export function generateTestProviderLinks( + testProviders: TestProviders, + context: API_HashEntry +): Link[] { + return Object.entries(testProviders) + .map(([testProviderId, state]) => { + if (!state) { + return null; + } + + const content = state.contextMenu?.({ context, state }, ContextMenu); + + if (!content) { + return null; + } + + return { + id: testProviderId, + content, + }; + }) + .filter(Boolean as any as ExcludesNull); +} diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index bb3be73f425d..d4784cc8c562 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -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'; diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index a4fe88248f19..599d7046697a 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -1,23 +1,11 @@ -import type { ComponentProps, FC, MutableRefObject, SyntheticEvent } from 'react'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import type { ComponentProps, FC, MutableRefObject } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; -import { - Button, - IconButton, - ListItem, - 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, - Addon_TypesEnum, - type StoryId, -} from '@storybook/core/types'; +import { type API_HashEntry, type API_StatusValue, type StoryId } from '@storybook/core/types'; import { CollapseIcon as CollapseIconSvg, - EllipsisIcon, ExpandAltIcon, StatusFailIcon, StatusPassIcon, @@ -25,8 +13,8 @@ import { SyncIcon, } from '@storybook/icons'; -import { PRELOAD_ENTRIES, type TestProviders } from '@storybook/core/core-events'; -import { useStorybookApi, useStorybookState } from '@storybook/core/manager-api'; +import { PRELOAD_ENTRIES } from '@storybook/core/core-events'; +import { useStorybookApi } from '@storybook/core/manager-api'; import type { API, ComponentEntry, @@ -48,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'; @@ -57,10 +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`; - -type ExcludesNull = (x: T | null) => x is T; +export type ExcludesNull = (x: T | null) => x is T; const Container = styled.div<{ hasOrphans: boolean }>((props) => ({ marginTop: props.hasOrphans ? 20 : 0, @@ -182,7 +168,7 @@ const StatusIconMap = { unknown: null, }; -const ContextMenu = { +export const ContextMenu = { ListItem, }; @@ -433,76 +419,6 @@ const Node = React.memo(function Node({ return null; }); -const useContextMenu = (context: API_HashEntry, links: Link[], api: API) => { - const [isItemHovered, setIsItemHovered] = useState(false); - const [isOpen, setIsOpen] = useState(false); - - const handlers = useMemo(() => { - return { - onMouseEnter: () => { - setIsItemHovered(true); - }, - onMouseLeave: () => { - setIsItemHovered(false); - }, - onOpen: (event: SyntheticEvent) => { - event.stopPropagation(); - setIsOpen(true); - }, - onClose: () => { - setIsOpen(false); - }, - }; - }, []); - - return useMemo(() => { - const testProviders = api.getElements( - Addon_TypesEnum.experimental_TEST_PROVIDER - ) as any as TestProviders; - const providerLinks = generateTestProviderLinks(testProviders, context); - const shouldDisplayLinks = - (isItemHovered || isOpen) && (providerLinks.length > 0 || links.length > 0); - return { - onMouseEnter: handlers.onMouseEnter, - onMouseLeave: handlers.onMouseLeave, - node: shouldDisplayLinks ? ( - { - if (!visible) { - handlers.onClose(); - } else { - setIsOpen(true); - } - }} - tooltip={({ onHide }) => ( - - )} - > - - - - - ) : null, - }; - }, [api, context, handlers, isItemHovered, isOpen, links]); -}; - -const LiveContextMenu: FC<{ context: API_HashEntry } & ComponentProps> = ({ - 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 ; -}; - const Root = React.memo(function Root({ setExpanded, isFullyExpanded, @@ -746,24 +662,3 @@ export const Tree = React.memo<{ ); }); - -function generateTestProviderLinks(testProviders: TestProviders, context: API_HashEntry): Link[] { - return Object.entries(testProviders) - .map(([testProviderId, state]) => { - if (!state) { - return null; - } - - const content = state.contextMenu?.({ context, state }, ContextMenu); - - if (!content) { - return null; - } - - return { - id: testProviderId, - content, - }; - }) - .filter(Boolean as any as ExcludesNull); -} From cab4b71a11cf66f2d55846919ab6ebdbbc5354d9 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 13 Nov 2024 13:21:20 +0100 Subject: [PATCH 13/47] fixing the core prep script race condition --- code/core/package.json | 32 +++++++++---------- code/core/scripts/entries.ts | 5 +-- .../scripts/helpers/generateTypesFiles.ts | 2 +- code/core/scripts/helpers/modifyThemeTypes.ts | 2 +- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/code/core/package.json b/code/core/package.json index 9418fe776214..6c3df123a489 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -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" ], diff --git a/code/core/scripts/entries.ts b/code/core/scripts/entries.ts index e3b78e483990..bcd1d1b94572 100644 --- a/code/core/scripts/entries.ts +++ b/code/core/scripts/entries.ts @@ -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), diff --git a/code/core/scripts/helpers/generateTypesFiles.ts b/code/core/scripts/helpers/generateTypesFiles.ts index e3a46d3567be..4c04a20d41c3 100644 --- a/code/core/scripts/helpers/generateTypesFiles.ts +++ b/code/core/scripts/helpers/generateTypesFiles.ts @@ -72,7 +72,7 @@ export async function generateTypesFiles( } else { console.log('Generated types for', picocolors.cyan(relative(cwd, dtsEntries[index]))); - if (dtsEntries[index].includes('src/theming.index.ts')) { + if (dtsEntries[index].includes('src/theming/index')) { console.log('Modifying theme types'); await modifyThemeTypes(); } diff --git a/code/core/scripts/helpers/modifyThemeTypes.ts b/code/core/scripts/helpers/modifyThemeTypes.ts index 5b35ca6be329..1751e4f89473 100644 --- a/code/core/scripts/helpers/modifyThemeTypes.ts +++ b/code/core/scripts/helpers/modifyThemeTypes.ts @@ -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/index';` : dedent` interface Theme extends StorybookTheme {} export type { Theme }; From 17b28903ce12df982d437acd95a16fdb604f1cd3 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 13 Nov 2024 13:22:58 +0100 Subject: [PATCH 14/47] fix for dev-mode --- code/core/scripts/helpers/modifyThemeTypes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/core/scripts/helpers/modifyThemeTypes.ts b/code/core/scripts/helpers/modifyThemeTypes.ts index 1751e4f89473..85d78a2cb07b 100644 --- a/code/core/scripts/helpers/modifyThemeTypes.ts +++ b/code/core/scripts/helpers/modifyThemeTypes.ts @@ -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 }; From 76687a738af352402a2d5cea16413b5baa664dc2 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 13 Nov 2024 13:31:37 +0100 Subject: [PATCH 15/47] move components into components dir --- .../test/src/components/ContextMenuItem.tsx | 44 +++++++++++ .../test/src/{ => components}/Panel.test.ts | 0 .../test/src/{ => components}/Panel.tsx | 4 +- code/addons/test/src/components/Title.tsx | 23 ++++++ code/addons/test/src/manager.tsx | 77 ++----------------- 5 files changed, 74 insertions(+), 74 deletions(-) create mode 100644 code/addons/test/src/components/ContextMenuItem.tsx rename code/addons/test/src/{ => components}/Panel.test.ts (100%) rename code/addons/test/src/{ => components}/Panel.tsx (98%) create mode 100644 code/addons/test/src/components/Title.tsx diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx new file mode 100644 index 000000000000..ec455ab0c3cf --- /dev/null +++ b/code/addons/test/src/components/ContextMenuItem.tsx @@ -0,0 +1,44 @@ +import React, { type FC, type SyntheticEvent, useCallback } 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 { PlayIcon } from '@storybook/icons'; + +import { TEST_PROVIDER_ID } from '../constants'; +import type { TestResult } from '../node/reporter'; + +const ContextMenuItem: FC<{ + context: API_HashEntry; + state: Addon_TestProviderState<{ + testResults: TestResult[]; + }>; + ListItem: typeof ListItem; +}> = ({ context, state, ListItem }) => { + const api = useStorybookApi(); + + const onClick = useCallback( + (event: SyntheticEvent) => { + event.stopPropagation(); + api.runTestprovider(TEST_PROVIDER_ID, { selection: [context.id] }); + }, + [api] + ); + + const theme = useTheme(); + + return ( + + + + } + center={state.running ? 'Running...' : 'Run tests'} + onClick={onClick} + /> + ); +}; diff --git a/code/addons/test/src/Panel.test.ts b/code/addons/test/src/components/Panel.test.ts similarity index 100% rename from code/addons/test/src/Panel.test.ts rename to code/addons/test/src/components/Panel.test.ts diff --git a/code/addons/test/src/Panel.tsx b/code/addons/test/src/components/Panel.tsx similarity index 98% rename from code/addons/test/src/Panel.tsx rename to code/addons/test/src/components/Panel.tsx index 0cc93575b35a..584872bad8c3 100644 --- a/code/addons/test/src/Panel.tsx +++ b/code/addons/test/src/components/Panel.tsx @@ -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']; diff --git a/code/addons/test/src/components/Title.tsx b/code/addons/test/src/components/Title.tsx new file mode 100644 index 000000000000..2e7c063f4e4c --- /dev/null +++ b/code/addons/test/src/components/Title.tsx @@ -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 Title() { + const [addonState = {}] = useAddonState(ADDON_ID); + const { hasException, interactionsCount } = addonState as any; + + return ( +
+ + Component tests + {interactionsCount && !hasException ? ( + {interactionsCount} + ) : null} + {hasException ? {interactionsCount} : null} + +
+ ); +} diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 00d9bb32d03e..2120dc0fd936 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -1,56 +1,22 @@ -import React, { type FC, type SyntheticEvent, useCallback, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; -import { - AddonPanel, - Badge, - Button, - Link as LinkComponent, - type ListItem, - Spaced, -} from 'storybook/internal/components'; +import { AddonPanel, 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 } from 'storybook/internal/manager-api'; import { - Consumer, - addons, - types, - useAddonState, - useStorybookApi, -} from 'storybook/internal/manager-api'; -import { useTheme } from 'storybook/internal/theming'; -import { - type API_HashEntry, type API_StatusObject, type API_StatusValue, - type Addon_TestProviderState, type Addon_TestProviderType, Addon_TypesEnum, } from 'storybook/internal/types'; -import { PlayIcon } from '@storybook/icons'; - -import { Panel } from './Panel'; import { GlobalErrorModal } from './components/GlobalErrorModal'; +import { Panel } from './components/Panel'; +import { Title } from './components/Title'; 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 ( -
- - Component tests - {interactionsCount && !hasException ? ( - {interactionsCount} - ) : null} - {hasException ? {interactionsCount} : null} - -
- ); -} - const statusMap: Record = { failed: 'error', passed: 'success', @@ -97,39 +63,6 @@ const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: nu ); }; -const ContextMenuItem: FC<{ - context: API_HashEntry; - state: Addon_TestProviderState<{ - testResults: TestResult[]; - }>; - ListItem: typeof ListItem; -}> = ({ context, state, ListItem }) => { - const api = useStorybookApi(); - - const onClick = useCallback( - (event: SyntheticEvent) => { - event.stopPropagation(); - api.runTestprovider(TEST_PROVIDER_ID, { selection: [context.id] }); - }, - [api] - ); - - const theme = useTheme(); - - return ( - - - - } - center={state.running ? 'Running...' : 'Run tests'} - onClick={onClick} - /> - ); -}; - addons.register(ADDON_ID, (api) => { const storybookBuilder = (globalThis as any).STORYBOOK_BUILDER || ''; if (storybookBuilder.includes('vite')) { From 0a0c36bc230819e20f787e80beec0cb931c28666 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 13 Nov 2024 13:42:36 +0100 Subject: [PATCH 16/47] renames --- .../test/src/components/ContextMenuItem.tsx | 9 ++++--- code/addons/test/src/manager.tsx | 21 +++++++++------- .../modules/experimental_testmodule.ts | 24 +++++++++---------- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx index ec455ab0c3cf..0fe8cd2b6881 100644 --- a/code/addons/test/src/components/ContextMenuItem.tsx +++ b/code/addons/test/src/components/ContextMenuItem.tsx @@ -1,4 +1,4 @@ -import React, { type FC, type SyntheticEvent, useCallback } from 'react'; +import React, { type FC, type SyntheticEvent, useCallback, useRef } from 'react'; import { Button, type ListItem } from 'storybook/internal/components'; import { useStorybookApi } from 'storybook/internal/manager-api'; @@ -10,7 +10,7 @@ import { PlayIcon } from '@storybook/icons'; import { TEST_PROVIDER_ID } from '../constants'; import type { TestResult } from '../node/reporter'; -const ContextMenuItem: FC<{ +export const ContextMenuItem: FC<{ context: API_HashEntry; state: Addon_TestProviderState<{ testResults: TestResult[]; @@ -18,11 +18,14 @@ const ContextMenuItem: FC<{ ListItem: typeof ListItem; }> = ({ context, state, ListItem }) => { const api = useStorybookApi(); + const id = useRef(context.id); + + id.current = context.id; const onClick = useCallback( (event: SyntheticEvent) => { event.stopPropagation(); - api.runTestprovider(TEST_PROVIDER_ID, { selection: [context.id] }); + api.runTestProvider(TEST_PROVIDER_ID, { selection: [id.current] }); }, [api] ); diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 2120dc0fd936..2ed1da40ee9c 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -11,6 +11,7 @@ import { Addon_TypesEnum, } from 'storybook/internal/types'; +import { ContextMenuItem } from './components/ContextMenuItem'; import { GlobalErrorModal } from './components/GlobalErrorModal'; import { Panel } from './components/Panel'; import { Title } from './components/Title'; @@ -77,14 +78,18 @@ addons.register(ADDON_ID, (api) => { watchable: true, name: 'Component tests', - // disabled for now - // contextMenu: ({ context, state }, { ListItem }) => { - // if (context.type === 'docs') { - // return null; - // } - - // return ; - // }, + contextMenu: ({ context, state }, { ListItem }) => { + if (context.type === 'docs') { + return null; + } + + // TODO: remove this... right now: always returns false, to disable the feature + if ('true') { + return false; + } + + return ; + }, title: ({ crashed, failed }) => crashed || failed ? 'Component tests failed' : 'Component tests', description: ({ failed, running, watching, progress, crashed, error }) => { diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts index b9b2185590cd..8f2a45f3aae9 100644 --- a/code/core/src/manager-api/modules/experimental_testmodule.ts +++ b/code/core/src/manager-api/modules/experimental_testmodule.ts @@ -29,11 +29,11 @@ interface RunOptions { } export type SubAPI = { - getTestproviderState(id: string): TestProviderState | undefined; - updateTestproviderState(id: TestProviderId, update: Partial): void; - clearTestproviderState(id: TestProviderId): void; - runTestprovider(id: TestProviderId, options?: RunOptions): void; - cancelTestprovider(id: TestProviderId): void; + getTestProviderState(id: string): TestProviderState | undefined; + updateTestProviderState(id: TestProviderId, update: Partial): void; + clearTestProviderState(id: TestProviderId): void; + runTestProvider(id: TestProviderId, options?: RunOptions): void; + cancelTestProvider(id: TestProviderId): void; }; export const init: ModuleFn = ({ store, fullAPI }) => { @@ -42,12 +42,12 @@ export const init: ModuleFn = ({ store, fullAPI }) => { }; const api: SubAPI = { - getTestproviderState(id) { + getTestProviderState(id) { const { testProviders } = store.getState(); return testProviders?.[id]; }, - updateTestproviderState(id, update) { + updateTestProviderState(id, update) { return store.setState( ({ testProviders }) => { return { testProviders: { ...testProviders, [id]: { ...testProviders[id], ...update } } }; @@ -55,7 +55,7 @@ export const init: ModuleFn = ({ store, fullAPI }) => { { persistence: 'session' } ); }, - clearTestproviderState(id) { + clearTestProviderState(id) { const update = { cancelling: false, running: true, @@ -70,7 +70,7 @@ export const init: ModuleFn = ({ store, fullAPI }) => { { persistence: 'session' } ); }, - runTestprovider(id, options) { + runTestProvider(id, options) { if (options?.selection) { const listOfFiles: string[] = []; @@ -88,10 +88,10 @@ export const init: ModuleFn = ({ store, fullAPI }) => { fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id }); } - return () => api.cancelTestprovider(id); + return () => api.cancelTestProvider(id); }, - cancelTestprovider(id) { - api.updateTestproviderState(id, { cancelling: true }); + cancelTestProvider(id) { + api.updateTestProviderState(id, { cancelling: true }); fullAPI.emit(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, { providerId: id }); }, }; From 4d8c845c9b7d2fa867d586797ca7b3290f128adf Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 13 Nov 2024 17:05:49 +0100 Subject: [PATCH 17/47] fixes --- .../src/manager/components/sidebar/Menu.tsx | 29 ++++--------------- .../components/sidebar/RefIndicator.tsx | 3 +- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/code/core/src/manager/components/sidebar/Menu.tsx b/code/core/src/manager/components/sidebar/Menu.tsx index 583a2b982616..3abca4ce2ff2 100644 --- a/code/core/src/manager/components/sidebar/Menu.tsx +++ b/code/core/src/manager/components/sidebar/Menu.tsx @@ -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 ; + onClick: () => void; +}> = ({ menu, onClick }) => { + return ; }; export interface SidebarMenuProps { @@ -118,7 +101,7 @@ export const SidebarMenu: FC = ({ menu, isHighlighted, onClick } + tooltip={({ onHide }) => } onVisibleChange={setIsTooltipVisible} > ; } From 4d4bc9478b64a7b946b247c77ce718db492fada1 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 14 Nov 2024 08:59:19 +0100 Subject: [PATCH 18/47] fix tooltip not hiding after click in the sidebar gear menu --- code/core/src/manager/components/sidebar/Menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/core/src/manager/components/sidebar/Menu.tsx b/code/core/src/manager/components/sidebar/Menu.tsx index 3abca4ce2ff2..aef385c2de1d 100644 --- a/code/core/src/manager/components/sidebar/Menu.tsx +++ b/code/core/src/manager/components/sidebar/Menu.tsx @@ -59,7 +59,7 @@ const SidebarMenuList: FC<{ menu: MenuList; onClick: () => void; }> = ({ menu, onClick }) => { - return ; + return ; }; export interface SidebarMenuProps { From d99f0e6d274ea38a2f03168047872618e6d1af37 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 14 Nov 2024 11:10:13 +0100 Subject: [PATCH 19/47] fixes --- .../components/sidebar/SidebarBottom.stories.tsx | 6 +++--- .../src/manager/components/sidebar/SidebarBottom.tsx | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx index 4e611ddd31a0..e8b3f506de8f 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Addon_TypesEnum } from '@storybook/core/types'; -import type { Meta } from '@storybook/react/*'; +import type { Meta } from '@storybook/react'; import { fn } from '@storybook/test'; import { type API, ManagerContext } from '@storybook/core/manager-api'; @@ -21,7 +21,7 @@ const managerContext: any = { on: fn().mockName('api::on'), off: fn().mockName('api::off'), getElements: fn(() => ({})), - updateTestproviderState: fn(), + updateTestProviderState: fn(), }, }; @@ -34,7 +34,7 @@ export default { on: fn(), off: fn(), clearNotification: fn(), - updateTestproviderState: fn(), + updateTestProviderState: fn(), emit: fn(), experimental_setFilter: fn(), getChannel: fn(), diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index 9bddc0f1269c..f3b48fb550f6 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -106,10 +106,10 @@ export const SidebarBottomBase = ({ const [warningsActive, setWarningsActive] = useState(false); const { testProviders } = useStorybookState(); const { - updateTestproviderState: updateTestProvider, - clearTestproviderState, - runTestprovider: onRunTests, - cancelTestprovider: onCancelTests, + updateTestProviderState: updateTestProvider, + clearTestProviderState, + runTestProvider: onRunTests, + cancelTestProvider: onCancelTests, } = useStorybookApi(); const [errorsActive, setErrorsActive] = useState(false); @@ -124,12 +124,12 @@ export const SidebarBottomBase = ({ const clearState = useCallback( ({ providerId }: { providerId: TestProviderId }) => { - clearTestproviderState(providerId); + clearTestProviderState(providerId); api.experimental_updateStatus(providerId, (state = {}) => Object.fromEntries(Object.keys(state).map((key) => [key, null])) ); }, - [api, clearTestproviderState] + [api, clearTestProviderState] ); const onSetWatchMode = useCallback( From 350cee78b9d2434f05c2ab357bc3d878ff0ddeec Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 14 Nov 2024 11:40:03 +0100 Subject: [PATCH 20/47] fix initial state --- code/core/src/manager-api/modules/experimental_testmodule.ts | 4 ++-- code/core/src/manager/components/sidebar/SidebarBottom.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts index 8f2a45f3aae9..375ca4c59f1c 100644 --- a/code/core/src/manager-api/modules/experimental_testmodule.ts +++ b/code/core/src/manager-api/modules/experimental_testmodule.ts @@ -38,7 +38,7 @@ export type SubAPI = { export const init: ModuleFn = ({ store, fullAPI }) => { const state: SubState = { - testProviders: store.getState().testProviders, + testProviders: store.getState().testProviders || {}, }; const api: SubAPI = { @@ -104,7 +104,7 @@ export const init: ModuleFn = ({ store, fullAPI }) => { { ...config, ...initialTestProviderState, - ...state.testProviders[id], + ...(state?.testProviders?.[id] || {}), } as TestProviders[0], ] ) diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index f3b48fb550f6..3668efde215e 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -105,6 +105,7 @@ export const SidebarBottomBase = ({ const wrapperRef = useRef(null); const [warningsActive, setWarningsActive] = useState(false); const { testProviders } = useStorybookState(); + const { updateTestProviderState: updateTestProvider, clearTestProviderState, From d23899ed06efdbb53f5db8e4273a929860624639 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 14 Nov 2024 11:43:32 +0100 Subject: [PATCH 21/47] Replace test provider title and description with render function --- code/addons/test/src/manager.tsx | 119 ++++++++++++++---- .../modules/experimental_testmodule.ts | 6 + .../components/sidebar/LegacyRender.tsx | 89 +++++++++++++ .../components/sidebar/SidebarBottom.tsx | 50 +++----- .../sidebar/TestingModule.stories.tsx | 36 ++++-- .../components/sidebar/TestingModule.tsx | 108 ++-------------- code/core/src/types/modules/addons.ts | 10 +- 7 files changed, 253 insertions(+), 165 deletions(-) create mode 100644 code/core/src/manager/components/sidebar/LegacyRender.tsx diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 2ed1da40ee9c..81bdd2830a0b 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from 'react'; -import { AddonPanel, Link as LinkComponent } 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 } from 'storybook/internal/manager-api'; +import { styled } from 'storybook/internal/theming'; import { type API_StatusObject, type API_StatusValue, @@ -11,10 +12,11 @@ import { Addon_TypesEnum, } from 'storybook/internal/types'; +import { EyeIcon, PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons'; + import { ContextMenuItem } from './components/ContextMenuItem'; import { GlobalErrorModal } from './components/GlobalErrorModal'; import { Panel } from './components/Panel'; -import { Title } from './components/Title'; import { ADDON_ID, PANEL_ID, TEST_PROVIDER_ID } from './constants'; import type { TestResult } from './node/reporter'; @@ -64,6 +66,28 @@ const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: nu ); }; +const Info = styled.div({ + display: 'flex', + flexDirection: 'column', + marginLeft: 6, +}); + +const Title = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({ + fontSize: theme.typography.size.s1, + fontWeight: crashed ? 'bold' : 'normal', + color: crashed ? theme.color.negativeText : theme.color.defaultText, +})); + +const Description = styled.div(({ theme }) => ({ + fontSize: theme.typography.size.s1, + color: theme.barTextColor, +})); + +const Actions = styled.div({ + display: 'flex', + gap: 6, +}); + addons.register(ADDON_ID, (api) => { const storybookBuilder = (globalThis as any).STORYBOOK_BUILDER || ''; if (storybookBuilder.includes('vite')) { @@ -76,37 +100,36 @@ addons.register(ADDON_ID, (api) => { type: Addon_TypesEnum.experimental_TEST_PROVIDER, runnable: true, watchable: true, - name: 'Component tests', + contextMenu: ({ context, state }, { ListItem }) => { if (context.type === 'docs') { return null; } // TODO: remove this... right now: always returns false, to disable the feature - if ('true') { + if (Date.now()) { return false; } return ; }, - title: ({ crashed, failed }) => - crashed || failed ? 'Component tests failed' : 'Component tests', - description: ({ failed, running, watching, progress, crashed, error }) => { - const [isModalOpen, setIsModalOpen] = useState(false); - const errorMessage = error?.message; + render: (state) => { + const [isModalOpen, setIsModalOpen] = useState(false); - let message: string | React.ReactNode = 'Not run'; + const title = state.crashed || state.failed ? 'Component tests failed' : 'Component tests'; + const errorMessage = state.error?.message; + let description: 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 = ( <> { setIsModalOpen(true); }} > - {error?.name || 'View full error'} + {state.error?.name || 'View full error'} ); - } else if (progress?.finishedAt) { - message = ( + } else if (state.progress?.finishedAt) { + description = ( ); - } else if (watching) { - message = 'Watching for file changes'; + } else if (state.watching) { + description = 'Watching for file changes'; } return ( <> - {message} + + + {title} + + {description} + + + + {state.watchable && ( + + )} + {state.runnable && ( + <> + {state.running && state.cancellable ? ( + + ) : ( + + )} + + )} + + ): void; clearTestProviderState(id: TestProviderId): void; runTestProvider(id: TestProviderId, options?: RunOptions): void; + setTestProviderWatchMode(id: TestProviderId, watchMode: boolean): void; cancelTestProvider(id: TestProviderId): void; }; @@ -90,6 +92,10 @@ export const init: ModuleFn = ({ store, fullAPI }) => { 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 }); diff --git a/code/core/src/manager/components/sidebar/LegacyRender.tsx b/code/core/src/manager/components/sidebar/LegacyRender.tsx new file mode 100644 index 000000000000..da5eaef29820 --- /dev/null +++ b/code/core/src/manager/components/sidebar/LegacyRender.tsx @@ -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 ( + <> + + + + </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.onSetWatchMode(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.onCancelTests(state.id)} + disabled={state.cancelling} + > + <StopAltHollowIcon /> + </Button> + ) : ( + <Button + aria-label={`Start ${state.name}`} + variant="ghost" + padding="small" + onClick={() => api.onRunTests(state.id)} + disabled={state.crashed || state.running} + > + <PlayHollowIcon /> + </Button> + )} + </> + )} + </Actions> + </> + ); +}; diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index 3668efde215e..9dfb6d4e72d9 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -104,15 +104,8 @@ export const SidebarBottomBase = ({ const spacerRef = useRef<HTMLDivElement | null>(null); const wrapperRef = useRef<HTMLDivElement | null>(null); const [warningsActive, setWarningsActive] = useState(false); - const { testProviders } = useStorybookState(); - - const { - updateTestProviderState: updateTestProvider, - clearTestProviderState, - runTestProvider: onRunTests, - cancelTestProvider: onCancelTests, - } = useStorybookApi(); const [errorsActive, setErrorsActive] = useState(false); + const { testProviders } = useStorybookState(); const warnings = Object.values(status).filter((statusByAddonId) => Object.values(statusByAddonId).some((value) => value?.status === 'warn') @@ -123,24 +116,6 @@ export const SidebarBottomBase = ({ const hasWarnings = warnings.length > 0; const hasErrors = errors.length > 0; - const clearState = useCallback( - ({ providerId }: { providerId: TestProviderId }) => { - clearTestProviderState(providerId); - api.experimental_updateStatus(providerId, (state = {}) => - Object.fromEntries(Object.keys(state).map((key) => [key, null])) - ); - }, - [api, clearTestProviderState] - ); - - 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; @@ -162,15 +137,27 @@ export const SidebarBottomBase = ({ useEffect(() => { const onCrashReport = ({ providerId, ...details }: TestingModuleCrashReportPayload) => { - updateTestProvider(providerId, { details, running: false, crashed: true, watching: false }); + api.updateTestProvider(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.updateTestProvider(providerId, { ...result, running: false, failed: true }); } else { const update = { ...result, running: result.status === 'pending' }; - updateTestProvider(providerId, update); + api.updateTestProvider(providerId, update); const { mapStatusUpdate, ...state } = testProviders[providerId]; const statusUpdate = mapStatusUpdate?.({ ...state, ...update }); @@ -189,7 +176,7 @@ export const SidebarBottomBase = ({ 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 || {}); if (!hasWarnings && !hasErrors && !testProvidersArray.length && !notifications.length) { @@ -210,9 +197,6 @@ export const SidebarBottomBase = ({ warningCount: warnings.length, warningsActive, setWarningsActive, - onRunTests, - onCancelTests, - onSetWatchMode, }} /> )} diff --git a/code/core/src/manager/components/sidebar/TestingModule.stories.tsx b/code/core/src/manager/components/sidebar/TestingModule.stories.tsx index bd24177ebefb..03ad13051bb0 100644 --- a/code/core/src/manager/components/sidebar/TestingModule.stories.tsx +++ b/code/core/src/manager/components/sidebar/TestingModule.stories.tsx @@ -23,8 +23,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 +38,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,8 +52,13 @@ 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, }, @@ -180,8 +195,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), diff --git a/code/core/src/manager/components/sidebar/TestingModule.tsx b/code/core/src/manager/components/sidebar/TestingModule.tsx index 510fdc24212c..740fc91f7f5c 100644 --- a/code/core/src/manager/components/sidebar/TestingModule.tsx +++ b/code/core/src/manager/components/sidebar/TestingModule.tsx @@ -2,17 +2,13 @@ import React, { type SyntheticEvent, useEffect, useRef, useState } from 'react'; import { Button, TooltipNote } 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 { WithTooltip } from '../../../components/components/tooltip/WithTooltip'; +import { useStorybookApi } from '../../../manager-api'; +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.onRunTests(id)); }} disabled={running} > diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index ebacb981b9b9..4fb2de2f2891 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -2,8 +2,7 @@ import type { FC, PropsWithChildren, ReactElement, ReactNode } from 'react'; import type { ListItem } from '../../components'; -import type { TestingModuleProgressReportProgress } from '../../core-events'; -import type { Addon, StoryEntry } from '../../manager-api'; +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'; @@ -475,8 +474,11 @@ 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; contextMenu?: ( options: { context: API_HashEntry; From 4582440514eba84dee0214c065a8f4f7d10a9ac6 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld <info@ghengeveld.nl> Date: Thu, 14 Nov 2024 11:58:29 +0100 Subject: [PATCH 22/47] Fix story mocks and api call --- .../components/sidebar/TestingModule.stories.tsx | 16 +++++++++++++--- .../manager/components/sidebar/TestingModule.tsx | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/code/core/src/manager/components/sidebar/TestingModule.stories.tsx b/code/core/src/manager/components/sidebar/TestingModule.stories.tsx index 03ad13051bb0..3278254c3b12 100644 --- a/code/core/src/manager/components/sidebar/TestingModule.stories.tsx +++ b/code/core/src/manager/components/sidebar/TestingModule.stories.tsx @@ -6,6 +6,7 @@ import { fn, userEvent } from '@storybook/test'; import type { TestProviders } from '@storybook/core/core-events'; +import { ManagerContext } from '../../../manager-api'; import { TestingModule } from './TestingModule'; const baseState = { @@ -64,6 +65,15 @@ const testProviders: TestProviders[keyof TestProviders][] = [ }, ]; +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, args: { @@ -74,11 +84,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 /> diff --git a/code/core/src/manager/components/sidebar/TestingModule.tsx b/code/core/src/manager/components/sidebar/TestingModule.tsx index 740fc91f7f5c..2b3ef24f17a3 100644 --- a/code/core/src/manager/components/sidebar/TestingModule.tsx +++ b/code/core/src/manager/components/sidebar/TestingModule.tsx @@ -217,7 +217,7 @@ export const TestingModule = ({ e.stopPropagation(); testProviders .filter((state) => !state.crashed && !state.running && state.runnable) - .forEach(({ id }) => api.onRunTests(id)); + .forEach(({ id }) => api.runTestProvider(id)); }} disabled={running} > From 6c411d4f5bb3adf64efb65d69794666f9c8561bf Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 14 Nov 2024 13:52:18 +0100 Subject: [PATCH 23/47] fix condition hooks rendering bug --- code/core/src/manager/components/sidebar/Tree.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index 599d7046697a..f4e33af457f1 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -617,6 +617,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} From 011b854e3e0b2f6f8e22540cec38eb090cacf097 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 14 Nov 2024 13:53:04 +0100 Subject: [PATCH 24/47] fix bug with persisting state with function --- code/core/src/manager-api/lib/store-setup.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/code/core/src/manager-api/lib/store-setup.ts b/code/core/src/manager-api/lib/store-setup.ts index 291898bede0a..9928ae09c8fc 100644 --- a/code/core/src/manager-api/lib/store-setup.ts +++ b/code/core/src/manager-api/lib/store-setup.ts @@ -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') From 1c8538707c694e0112ba1d149ed09cac5b33302b Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 14 Nov 2024 14:20:23 +0100 Subject: [PATCH 25/47] use correct title component in panel --- code/addons/test/src/manager.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 81bdd2830a0b..751e1bac7abd 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -17,6 +17,7 @@ import { EyeIcon, PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons'; import { ContextMenuItem } from './components/ContextMenuItem'; import { GlobalErrorModal } from './components/GlobalErrorModal'; import { Panel } from './components/Panel'; +import { Title } from './components/Title'; import { ADDON_ID, PANEL_ID, TEST_PROVIDER_ID } from './constants'; import type { TestResult } from './node/reporter'; @@ -72,7 +73,7 @@ const Info = styled.div({ marginLeft: 6, }); -const Title = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({ +const Title2 = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({ fontSize: theme.typography.size.s1, fontWeight: crashed ? 'bold' : 'normal', color: crashed ? theme.color.negativeText : theme.color.defaultText, @@ -155,9 +156,9 @@ addons.register(ADDON_ID, (api) => { return ( <> <Info> - <Title crashed={state.crashed} id="testing-module-title"> + <Title2 crashed={state.crashed} id="testing-module-title"> {title} - + {description} @@ -255,7 +256,7 @@ addons.register(ADDON_ID, (api) => { addons.add(PANEL_ID, { type: types.PANEL, - title: Title, + title: () => , match: ({ viewMode }) => viewMode === 'story', render: ({ active }) => { return ( From f8cbd83381bc02c7898d34f8f5b9e23a11311695 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 14 Nov 2024 14:27:07 +0100 Subject: [PATCH 26/47] fix --- code/core/src/manager/components/sidebar/SidebarBottom.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index 9dfb6d4e72d9..9eea4a4855b8 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -137,7 +137,7 @@ export const SidebarBottomBase = ({ useEffect(() => { const onCrashReport = ({ providerId, ...details }: TestingModuleCrashReportPayload) => { - api.updateTestProvider(providerId, { + api.updateTestProviderState(providerId, { details, running: false, crashed: true, @@ -154,10 +154,10 @@ export const SidebarBottomBase = ({ const onProgressReport = ({ providerId, ...result }: TestingModuleProgressReportPayload) => { if (result.status === 'failed') { - api.updateTestProvider(providerId, { ...result, running: false, failed: true }); + api.updateTestProviderState(providerId, { ...result, running: false, failed: true }); } else { const update = { ...result, running: result.status === 'pending' }; - api.updateTestProvider(providerId, update); + api.updateTestProviderState(providerId, update); const { mapStatusUpdate, ...state } = testProviders[providerId]; const statusUpdate = mapStatusUpdate?.({ ...state, ...update }); From a35701d292d371c53abb5b9ea4eb1e02ab8b10b4 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 14 Nov 2024 14:32:18 +0100 Subject: [PATCH 27/47] fixes --- code/addons/test/src/manager.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 751e1bac7abd..ed8a3053f2fe 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -169,7 +169,7 @@ addons.register(ADDON_ID, (api) => { variant="ghost" padding="small" active={state.watching} - onClick={() => api.onSetWatchMode(state.id, !state.watching)} + onClick={() => api.setTestProviderWatchMode(state.id, !state.watching)} disabled={state.crashed || state.running} > <EyeIcon /> @@ -182,7 +182,7 @@ addons.register(ADDON_ID, (api) => { aria-label={`Stop ${state.name}`} variant="ghost" padding="small" - onClick={() => api.onCancelTests(state.id)} + onClick={() => api.cancelTestProvider(state.id)} disabled={state.cancelling} > <StopAltHollowIcon /> @@ -192,7 +192,7 @@ addons.register(ADDON_ID, (api) => { aria-label={`Start ${state.name}`} variant="ghost" padding="small" - onClick={() => api.onRunTests(state.id)} + onClick={() => api.runTestProvider(state.id)} disabled={state.crashed || state.running} > <PlayHollowIcon /> From 166aeb6d2de3735d8bdc5d1999dbcf2645d5d19a Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 14 Nov 2024 14:44:21 +0100 Subject: [PATCH 28/47] fix api method renames --- code/core/src/manager/components/sidebar/LegacyRender.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/core/src/manager/components/sidebar/LegacyRender.tsx b/code/core/src/manager/components/sidebar/LegacyRender.tsx index da5eaef29820..f0e74f460219 100644 --- a/code/core/src/manager/components/sidebar/LegacyRender.tsx +++ b/code/core/src/manager/components/sidebar/LegacyRender.tsx @@ -52,7 +52,7 @@ export const LegacyRender = ({ ...state }: TestProviders[keyof TestProviders]) = variant="ghost" padding="small" active={state.watching} - onClick={() => api.onSetWatchMode(state.id, !state.watching)} + onClick={() => api.setTestProviderWatchMode(state.id, !state.watching)} disabled={state.crashed || state.running} > <EyeIcon /> @@ -65,7 +65,7 @@ export const LegacyRender = ({ ...state }: TestProviders[keyof TestProviders]) = aria-label={`Stop ${name}`} variant="ghost" padding="small" - onClick={() => api.onCancelTests(state.id)} + onClick={() => api.cancelTestProvider(state.id)} disabled={state.cancelling} > <StopAltHollowIcon /> @@ -75,7 +75,7 @@ export const LegacyRender = ({ ...state }: TestProviders[keyof TestProviders]) = aria-label={`Start ${state.name}`} variant="ghost" padding="small" - onClick={() => api.onRunTests(state.id)} + onClick={() => api.runTestProvider(state.id)} disabled={state.crashed || state.running} > <PlayHollowIcon /> From dfc147ad4711f89ecf4fa9f8619aede41e9c0dae Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Thu, 14 Nov 2024 15:49:26 +0100 Subject: [PATCH 29/47] send specific stories when running from context menu --- .../test/src/components/ContextMenuItem.tsx | 2 +- .../addons/test/src/node/test-manager.test.ts | 4 - .../src/core-events/data/testing-module.ts | 6 +- .../modules/experimental_testmodule.ts | 109 +++++++++++++++--- 4 files changed, 96 insertions(+), 25 deletions(-) diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx index 0fe8cd2b6881..ff7eb58619d8 100644 --- a/code/addons/test/src/components/ContextMenuItem.tsx +++ b/code/addons/test/src/components/ContextMenuItem.tsx @@ -25,7 +25,7 @@ export const ContextMenuItem: FC<{ const onClick = useCallback( (event: SyntheticEvent) => { event.stopPropagation(); - api.runTestProvider(TEST_PROVIDER_ID, { selection: [id.current] }); + api.runTestProvider(TEST_PROVIDER_ID, { entryId: id.current }); }, [api] ); diff --git a/code/addons/test/src/node/test-manager.test.ts b/code/addons/test/src/node/test-manager.test.ts index 415c9bbc7bb6..32a404cfc385 100644 --- a/code/addons/test/src/node/test-manager.test.ts +++ b/code/addons/test/src/node/test-manager.test.ts @@ -84,12 +84,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 +105,6 @@ describe('TestManager', () => { { stories: [], importPath: 'path/to/unknown/file', - componentPath: 'path/to/unknown/component', }, ], }); @@ -119,7 +116,6 @@ describe('TestManager', () => { { stories: [], importPath: 'path/to/file', - componentPath: 'path/to/component', }, ], }); diff --git a/code/core/src/core-events/data/testing-module.ts b/code/core/src/core-events/data/testing-module.ts index 93d7d4545c87..ee483364d93e 100644 --- a/code/core/src/core-events/data/testing-module.ts +++ b/code/core/src/core-events/data/testing-module.ts @@ -8,7 +8,7 @@ export type TestProviderState = Addon_TestProviderState; export type TestProviders = Record<TestProviderId, TestProviderConfig & TestProviderState>; -export type TestingModuleRunRequestStories = { +export type TestingModuleRunRequestStory = { id: string; // button--primary name: string; // Primary }; @@ -16,9 +16,9 @@ export type TestingModuleRunRequestStories = { export type TestingModuleRunRequestPayload = { providerId: TestProviderId; payload: { - stories: TestingModuleRunRequestStories[]; importPath: string; // ./.../button.stories.tsx - componentPath: string; // ./.../button.tsx + stories?: TestingModuleRunRequestStory[]; + componentPath?: string; // ./.../button.tsx }[]; }; diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts index e5d46b703a78..caa9826d369a 100644 --- a/code/core/src/manager-api/modules/experimental_testmodule.ts +++ b/code/core/src/manager-api/modules/experimental_testmodule.ts @@ -1,12 +1,15 @@ -import { Addon_TypesEnum } from '@storybook/core/types'; +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 type { ModuleFn } from '../lib/types'; @@ -26,7 +29,7 @@ const initialTestProviderState: TestProviderState = { }; interface RunOptions { - selection?: string[]; + entryId?: StoryId; } export type SubAPI = { @@ -73,23 +76,95 @@ export const init: ModuleFn = ({ store, fullAPI }) => { ); }, runTestProvider(id, options) { - if (options?.selection) { - const listOfFiles: string[] = []; - - // TODO: get actual list and emit, this notification is for development purposes - fullAPI.addNotification({ - id: 'testing-module', - - content: { - headline: 'Running tests', - subHeadline: `Running tests for ${listOfFiles} stories`, - }, - }); - // fullAPI.emit(TESTING_MODULE_RUN_REQUEST, { providerId: id, selection: [] }); - } else { - fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id }); + console.log('LOG: runTestProvider', id, options); + if (!options?.entryId) { + console.log('LOG: runTestProvider: no entryId, running all tests'); + const payload: TestingModuleRunAllRequestPayload = { providerId: id }; + fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, payload); + return () => api.cancelTestProvider(id); } + const index = store.getState().index; + if (!index) { + throw new Error('no index?'); + } + + const entry = index[options.entryId]; + + if (!entry) { + throw new Error('no entry?'); + } + + if (entry.type === 'story') { + console.log('LOG: runTestProvider: running single story', entry); + 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]; + console.log(`Processing entry: ${entryId}`, foundEntry); + switch (foundEntry.type) { + case 'component': + console.log(`Adding component entry: ${entryId}`); + const firstStoryId = foundEntry.children.find( + (childId) => index[childId].type === 'story' + ); + if (!firstStoryId) { + // TODO: can this happen? docs only in a component or something? + throw new Error('No story found for component?'); + } + payloads.add({ importPath: (index[firstStoryId] as API_StoryEntry).importPath }); + return; + case 'story': { + // this really shouldn't happen because we don't visit components' children. + // unless groups can have stories directly? + console.log(`Adding story entry: ${entryId}`); + payloads.add({ + importPath: foundEntry.importPath, + stories: [ + { + id: foundEntry.id, + name: foundEntry.name, + }, + ], + }); + return; + } + case 'docs': { + return; + } + default: + console.log(`Processing children of entry: ${entryId}`); + foundEntry.children.forEach(findComponents); + } + }; + console.log(`Starting to find components for entryId:`, options.entryId); + findComponents(options.entryId); + + const payload: TestingModuleRunRequestPayload = { + providerId: id, + payload: Array.from(payloads), + }; + console.log('LOG: payload', payload); + fullAPI.emit(TESTING_MODULE_RUN_REQUEST, payload); + return () => api.cancelTestProvider(id); }, setTestProviderWatchMode(id, watchMode) { From e5e4499e974fde4448da22b9bcae298b55394d26 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Thu, 14 Nov 2024 16:51:25 +0100 Subject: [PATCH 30/47] set testNamePattern from story names --- code/addons/test/src/node/vitest-manager.ts | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/code/addons/test/src/node/vitest-manager.ts b/code/addons/test/src/node/vitest-manager.ts index ab31e1fc7694..f69f065aa42a 100644 --- a/code/addons/test/src/node/vitest-manager.ts +++ b/code/addons/test/src/node/vitest-manager.ts @@ -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__; From 81473552ba2a2f821c4dcfaeb840180d7d327c0c Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Thu, 14 Nov 2024 22:39:49 +0100 Subject: [PATCH 31/47] update test run message, allow canceling --- .../test/src/components/ContextMenuItem.tsx | 22 ++++++++++++------- .../modules/experimental_testmodule.ts | 11 +++++----- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx index ff7eb58619d8..082d05e0a4c8 100644 --- a/code/addons/test/src/components/ContextMenuItem.tsx +++ b/code/addons/test/src/components/ContextMenuItem.tsx @@ -5,7 +5,7 @@ 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 { PlayIcon } from '@storybook/icons'; +import { PlayHollowIcon, PlayIcon, StopAltHollowIcon } from '@storybook/icons'; import { TEST_PROVIDER_ID } from '../constants'; import type { TestResult } from '../node/reporter'; @@ -19,29 +19,35 @@ export const ContextMenuItem: FC<{ }> = ({ context, state, ListItem }) => { const api = useStorybookApi(); const id = useRef(context.id); + const cancelRun = useRef<() => void>(); + + const Icon = state.running ? StopAltHollowIcon : PlayHollowIcon; id.current = context.id; const onClick = useCallback( (event: SyntheticEvent) => { event.stopPropagation(); - api.runTestProvider(TEST_PROVIDER_ID, { entryId: id.current }); + if (state.running) { + cancelRun.current?.(); + return; + } else { + cancelRun.current = api.runTestProvider(TEST_PROVIDER_ID, { entryId: id.current }); + } }, - [api] + [api, state.running] ); const theme = useTheme(); return ( <ListItem - title={'Component tests'} + title={'Run local tests'} right={ - <Button variant="ghost" padding="small" disabled={state.crashed || state.running}> - <PlayIcon fill={theme.barTextColor} /> + <Button onClick={onClick} variant="ghost" padding="small" disabled={state.crashed}> + <Icon fill={theme.barTextColor} /> </Button> } - center={state.running ? 'Running...' : 'Run tests'} - onClick={onClick} /> ); }; diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts index caa9826d369a..b16956be7a83 100644 --- a/code/core/src/manager-api/modules/experimental_testmodule.ts +++ b/code/core/src/manager-api/modules/experimental_testmodule.ts @@ -36,7 +36,7 @@ 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; + runTestProvider(id: TestProviderId, options?: RunOptions): () => void; setTestProviderWatchMode(id: TestProviderId, watchMode: boolean): void; cancelTestProvider(id: TestProviderId): void; }; @@ -127,14 +127,15 @@ export const init: ModuleFn = ({ store, fullAPI }) => { (childId) => index[childId].type === 'story' ); if (!firstStoryId) { - // TODO: can this happen? docs only in a component or something? - throw new Error('No story found for component?'); + // happens when there are only docs in the component + return; } payloads.add({ importPath: (index[firstStoryId] as API_StoryEntry).importPath }); return; case 'story': { - // this really shouldn't happen because we don't visit components' children. - // unless groups can have stories directly? + // this shouldn't happen because we don't visit components' children. + // so we never get to a story directly. + // unless groups can have direct stories without components? console.log(`Adding story entry: ${entryId}`); payloads.add({ importPath: foundEntry.importPath, From 7e43ecd708d5d76f7473df35b89cf5728b2c5a7d Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Thu, 14 Nov 2024 23:00:38 +0100 Subject: [PATCH 32/47] use root cancelTestProvider function --- code/addons/test/src/components/ContextMenuItem.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx index 082d05e0a4c8..7a7cc7b15752 100644 --- a/code/addons/test/src/components/ContextMenuItem.tsx +++ b/code/addons/test/src/components/ContextMenuItem.tsx @@ -19,7 +19,6 @@ export const ContextMenuItem: FC<{ }> = ({ context, state, ListItem }) => { const api = useStorybookApi(); const id = useRef(context.id); - const cancelRun = useRef<() => void>(); const Icon = state.running ? StopAltHollowIcon : PlayHollowIcon; @@ -29,10 +28,9 @@ export const ContextMenuItem: FC<{ (event: SyntheticEvent) => { event.stopPropagation(); if (state.running) { - cancelRun.current?.(); - return; + api.cancelTestProvider(TEST_PROVIDER_ID); } else { - cancelRun.current = api.runTestProvider(TEST_PROVIDER_ID, { entryId: id.current }); + api.runTestProvider(TEST_PROVIDER_ID, { entryId: id.current }); } }, [api, state.running] From c912e0e26faec8c37228da6fb10450a683ecc54c Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Fri, 15 Nov 2024 12:37:04 +0100 Subject: [PATCH 33/47] do not depend on mouseLeave event, as it's often missed when user quickly moves their mouse around --- .../components/sidebar/ContextMenu.tsx | 33 +++++++++++-------- .../src/manager/components/sidebar/Tree.tsx | 10 ++++-- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index 7d965005497d..b09ce7909e8a 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -15,16 +15,13 @@ import type { ExcludesNull } from './Tree'; import { ContextMenu } from './Tree'; export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) => { - const [isItemHovered, setIsItemHovered] = useState(false); + const [hoverCount, setHoverCount] = useState(0); const [isOpen, setIsOpen] = useState(false); const handlers = useMemo(() => { return { onMouseEnter: () => { - setIsItemHovered(true); - }, - onMouseLeave: () => { - setIsItemHovered(false); + setHoverCount((c) => c + 1); }, onOpen: (event: SyntheticEvent) => { event.stopPropagation(); @@ -36,17 +33,23 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) }; }, []); - return useMemo(() => { + const providerLinks = useMemo(() => { const testProviders = api.getElements( Addon_TypesEnum.experimental_TEST_PROVIDER ) as any as TestProviders; - const providerLinks = generateTestProviderLinks(testProviders, context); - const shouldDisplayLinks = - (isItemHovered || isOpen) && (providerLinks.length > 0 || links.length > 0); + + if (hoverCount) { + return generateTestProviderLinks(testProviders, context); + } + return []; + }, [api, context, hoverCount]); + + return useMemo(() => { + const rendered = providerLinks.length > 0 || links.length > 0; + const forceDisplay = isOpen; return { onMouseEnter: handlers.onMouseEnter, - onMouseLeave: handlers.onMouseLeave, - node: shouldDisplayLinks ? ( + node: rendered ? ( <WithTooltip closeOnOutsideClick onClick={handlers.onOpen} @@ -62,13 +65,17 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) <LiveContextMenu context={context} links={links} onClick={onHide} /> )} > - <StatusButton type="button" status={'pending'}> + <StatusButton + type="button" + status={'pending'} + data-displayed={forceDisplay ? 'on' : 'off'} + > <EllipsisIcon /> </StatusButton> </WithTooltip> ) : null, }; - }, [api, context, handlers, isItemHovered, isOpen, links]); + }, [context, handlers, isOpen, links, providerLinks.length]); }; /** diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index f4e33af457f1..7de14d18caa8 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -85,6 +85,14 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({ outline: 'none', }, + '& [data-displayed="off"]': { + visibility: 'hidden', + }, + + '&:hover [data-displayed="off"]': { + visibility: 'visible', + }, + '&[data-selected="true"]': { color: theme.color.lightest, background: theme.color.secondary, @@ -275,7 +283,6 @@ const Node = React.memo<NodeProps>(function Node({ data-nodetype={item.type === 'docs' ? 'document' : 'story'} data-highlightable={isDisplayed} onMouseEnter={contextMenu.onMouseEnter} - onMouseLeave={contextMenu.onMouseLeave} > <LeafNode // @ts-expect-error (non strict) @@ -373,7 +380,6 @@ const Node = React.memo<NodeProps>(function Node({ data-nodetype={item.type} data-highlightable={isDisplayed} onMouseEnter={contextMenu.onMouseEnter} - onMouseLeave={contextMenu.onMouseLeave} > <BranchNode id={id} From 9e841f8a4ac07b4df89b5f76e053fd1ab8cdcd36 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Fri, 15 Nov 2024 12:42:51 +0100 Subject: [PATCH 34/47] simplify, add comment for clarity --- .../manager/components/sidebar/ContextMenu.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index b09ce7909e8a..d78d0451d8f3 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -33,6 +33,10 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) }; }, []); + /** + * 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 @@ -44,12 +48,12 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) return []; }, [api, context, hoverCount]); + const isRendered = providerLinks.length > 0 || links.length > 0; + return useMemo(() => { - const rendered = providerLinks.length > 0 || links.length > 0; - const forceDisplay = isOpen; return { onMouseEnter: handlers.onMouseEnter, - node: rendered ? ( + node: isRendered ? ( <WithTooltip closeOnOutsideClick onClick={handlers.onOpen} @@ -65,17 +69,13 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) <LiveContextMenu context={context} links={links} onClick={onHide} /> )} > - <StatusButton - type="button" - status={'pending'} - data-displayed={forceDisplay ? 'on' : 'off'} - > + <StatusButton type="button" status={'pending'} data-displayed={isOpen ? 'on' : 'off'}> <EllipsisIcon /> </StatusButton> </WithTooltip> ) : null, }; - }, [context, handlers, isOpen, links, providerLinks.length]); + }, [context, handlers, isOpen, isRendered, links]); }; /** From 3a2415b120a3c90632c1bc2b5878d67aadb2a0e4 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Fri, 15 Nov 2024 15:29:06 +0100 Subject: [PATCH 35/47] ensure the status icons are visible again, but hidden during hover --- code/addons/test/src/RelativeTime.tsx | 24 +++++++ .../test/src/components/ContextMenuItem.tsx | 67 ++++++++++++++++--- code/addons/test/src/manager.tsx | 24 +------ .../components/sidebar/ContextMenu.tsx | 4 +- .../src/manager/components/sidebar/Tree.tsx | 48 +++++++------ 5 files changed, 112 insertions(+), 55 deletions(-) create mode 100644 code/addons/test/src/RelativeTime.tsx diff --git a/code/addons/test/src/RelativeTime.tsx b/code/addons/test/src/RelativeTime.tsx new file mode 100644 index 000000000000..9225b29fe2c0 --- /dev/null +++ b/code/addons/test/src/RelativeTime.tsx @@ -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}` + ); +}; diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx index 7a7cc7b15752..7640c0018bb5 100644 --- a/code/addons/test/src/components/ContextMenuItem.tsx +++ b/code/addons/test/src/components/ContextMenuItem.tsx @@ -1,12 +1,20 @@ -import React, { type FC, type SyntheticEvent, useCallback, useRef } from 'react'; +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, PlayIcon, StopAltHollowIcon } from '@storybook/icons'; +import { PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons'; +import { RelativeTime } from '../RelativeTime'; import { TEST_PROVIDER_ID } from '../constants'; import type { TestResult } from '../node/reporter'; @@ -18,14 +26,20 @@ export const ContextMenuItem: FC<{ 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; - id.current = context.id; + useEffect(() => { + setDisabled(false); + }, [state.running]); const onClick = useCallback( (event: SyntheticEvent) => { + setDisabled(true); event.stopPropagation(); if (state.running) { api.cancelTestProvider(TEST_PROVIDER_ID); @@ -38,14 +52,45 @@ export const ContextMenuItem: FC<{ 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 ( - <ListItem - title={'Run local tests'} - right={ - <Button onClick={onClick} variant="ghost" padding="small" disabled={state.crashed}> - <Icon fill={theme.barTextColor} /> - </Button> - } - /> + <div onClick={(event) => 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> ); }; diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index ed8a3053f2fe..9a3d9312fd36 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { AddonPanel, Button, Link as LinkComponent } from 'storybook/internal/components'; import { TESTING_MODULE_RUN_ALL_REQUEST } from 'storybook/internal/core-events'; @@ -14,6 +14,7 @@ import { import { EyeIcon, PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons'; +import { RelativeTime } from './RelativeTime'; import { ContextMenuItem } from './components/ContextMenuItem'; import { GlobalErrorModal } from './components/GlobalErrorModal'; import { Panel } from './components/Panel'; @@ -46,27 +47,6 @@ 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); - - 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}` - ); -}; - const Info = styled.div({ display: 'flex', flexDirection: 'column', diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index d78d0451d8f3..3348da2657b5 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -55,8 +55,8 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) onMouseEnter: handlers.onMouseEnter, node: isRendered ? ( <WithTooltip + data-displayed={isOpen ? 'on' : 'off'} closeOnOutsideClick - onClick={handlers.onOpen} placement="bottom-end" onVisibleChange={(visible) => { if (!visible) { @@ -69,7 +69,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) <LiveContextMenu context={context} links={links} onClick={onHide} /> )} > - <StatusButton type="button" status={'pending'} data-displayed={isOpen ? 'on' : 'off'}> + <StatusButton type="button" status={'pending'}> <EllipsisIcon /> </StatusButton> </WithTooltip> diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index 7de14d18caa8..38f1e7449118 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -93,6 +93,14 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({ visibility: 'visible', }, + '& [data-displayed="on"] + *': { + display: 'none', + }, + + '&:hover [data-displayed="off"] + *': { + display: 'none', + }, + '&[data-selected="true"]': { color: theme.color.lightest, background: theme.color.secondary, @@ -308,18 +316,18 @@ const Node = React.memo<NodeProps>(function Node({ <a href="#storybook-preview-wrapper">Skip to canvas</a> </SkipToContentLink> )} - {contextMenu.node || - (icon ? ( - <StatusButton - aria-label={`Test status: ${statusValue}`} - role="status" - type="button" - status={statusValue} - selectedItem={isSelected} - > - {icon} - </StatusButton> - ) : null)} + {contextMenu.node} + {icon ? ( + <StatusButton + aria-label={`Test status: ${statusValue}`} + role="status" + type="button" + status={statusValue} + selectedItem={isSelected} + > + {icon} + </StatusButton> + ) : null} </LeafNodeStyleWrapper> ); } @@ -410,14 +418,14 @@ 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) && ( - <StatusButton type="button" status={itemStatus}> - <svg key="icon" viewBox="0 0 6 6" width="6" height="6" type="dot"> - <UseSymbol type="dot" /> - </svg> - </StatusButton> - ))} + {contextMenu.node} + {['error', 'warn'].includes(itemStatus) && ( + <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> ); } From 5811cdba48cf924b188b43b4d8bcf23aed32479f Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Mon, 18 Nov 2024 11:25:06 +0100 Subject: [PATCH 36/47] fix duplicate keys in stories --- .../ArgsTable/ArgsTable.stories.tsx | 26 +++++++++++-------- .../blocks/src/examples/Button.stories.tsx | 1 + 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/code/lib/blocks/src/components/ArgsTable/ArgsTable.stories.tsx b/code/lib/blocks/src/components/ArgsTable/ArgsTable.stories.tsx index ab867ff74b5c..8d44e5f7cb43 100644 --- a/code/lib/blocks/src/components/ArgsTable/ArgsTable.stories.tsx +++ b/code/lib/blocks/src/components/ArgsTable/ArgsTable.stories.tsx @@ -90,10 +90,14 @@ export const SectionsAndSubsections = { export const SubsectionsOnly = { args: { rows: { - a: { ...stringType, table: { ...stringType.table, ...componentSubsection } }, + a: { ...stringType, key: 'stringA', table: { ...stringType.table, ...componentSubsection } }, b: { ...numberType, table: { ...stringType.table, ...componentSubsection } }, - c: { ...stringType, table: { ...stringType.table, ...componentSubsection } }, - d: { ...stringType, table: { ...stringType.table, ...htmlElementSubsection } }, + c: { ...stringType, key: 'stringB', table: { ...stringType.table, ...componentSubsection } }, + d: { + ...stringType, + key: 'stringC', + table: { ...stringType.table, ...htmlElementSubsection }, + }, }, }, }; @@ -106,14 +110,14 @@ export const AllControls = { color: ArgRow.Color.args.row, date: ArgRow.Date.args.row, string: ArgRow.String.args.row, - number: ArgRow.Number.args.row, - range: ArgRow.Number.args.row, - radio: ArgRow.Radio.args.row, - inlineRadio: ArgRow.InlineRadio.args.row, - check: ArgRow.Check.args.row, - inlineCheck: ArgRow.InlineCheck.args.row, - select: ArgRow.Select.args.row, - multiSelect: ArgRow.MultiSelect.args.row, + number: { ...ArgRow.Number.args.row, key: 'number' }, + range: { ...ArgRow.Number.args.row, key: 'range' }, + radio: { ...ArgRow.Radio.args.row, key: 'radio' }, + inlineRadio: { ...ArgRow.InlineRadio.args.row, key: 'inlineRadio' }, + check: { ...ArgRow.Check.args.row, key: 'check' }, + inlineCheck: { ...ArgRow.InlineCheck.args.row, key: 'inlineCheck' }, + select: { ...ArgRow.Select.args.row, key: 'select' }, + multiSelect: { ...ArgRow.MultiSelect.args.row, key: 'multiSelect' }, object: ArgRow.ObjectOf.args.row, func: ArgRow.Func.args.row, }, diff --git a/code/lib/blocks/src/examples/Button.stories.tsx b/code/lib/blocks/src/examples/Button.stories.tsx index 31f28a568c51..c22d141f541c 100644 --- a/code/lib/blocks/src/examples/Button.stories.tsx +++ b/code/lib/blocks/src/examples/Button.stories.tsx @@ -137,4 +137,5 @@ export const ErrorStory: Story = { parameters: { chromatic: { disable: true }, }, + tags: ['!test', '!vitest'], }; From 7454f3c3a71ea225b706af58797e79be31487188 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Mon, 18 Nov 2024 11:27:18 +0100 Subject: [PATCH 37/47] properly disable docs context loader in PS --- code/.storybook/preview.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/code/.storybook/preview.tsx b/code/.storybook/preview.tsx index 60135ac5a8c5..fac0c9bdb2b1 100644 --- a/code/.storybook/preview.tsx +++ b/code/.storybook/preview.tsx @@ -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( From 2b254b7c08118eeb89156a108cac2b49a181b4bf Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Mon, 18 Nov 2024 11:32:53 +0100 Subject: [PATCH 38/47] add testNamePattern to vitest mock --- code/addons/test/src/node/test-manager.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/addons/test/src/node/test-manager.test.ts b/code/addons/test/src/node/test-manager.test.ts index 32a404cfc385..20763641d217 100644 --- a/code/addons/test/src/node/test-manager.test.ts +++ b/code/addons/test/src/node/test-manager.test.ts @@ -17,6 +17,9 @@ const vitest = vi.hoisted(() => ({ cancelCurrentRun: vi.fn(), globTestSpecs: vi.fn(), getModuleProjects: vi.fn(() => []), + configOverride: { + testNamePattern: undefined, + }, })); vi.mock('vitest/node', () => ({ From ec28fa2bfa7677fe291b8dbe0844379e81672c70 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Mon, 18 Nov 2024 11:52:49 +0100 Subject: [PATCH 39/47] fix sidebarbottom story --- .../sidebar/SidebarBottom.stories.tsx | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx index e8b3f506de8f..444f0c185dbf 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx @@ -15,12 +15,27 @@ const managerContext: any = { autodocs: 'tag', docsMode: false, }, - testProviders: {}, + 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'), - getElements: fn(() => ({})), updateTestProviderState: fn(), }, }; @@ -38,23 +53,7 @@ export default { 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: [ From dbdd73a470a12243d1a0990f9bc3acfca26f6202 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Mon, 18 Nov 2024 14:24:15 +0100 Subject: [PATCH 40/47] move stories into the same index ancestor --- .../src/manager/components/sidebar/FileSearchList.stories.tsx | 1 + .../components/sidebar/FileSearchListSkeleton.stories.tsx | 1 + .../src/manager/components/sidebar/FileSearchModal.stories.tsx | 1 + code/core/src/manager/components/sidebar/FilterToggle.stories.ts | 1 + code/core/src/manager/components/sidebar/IconSymbols.stories.tsx | 1 + .../src/manager/components/sidebar/SidebarBottom.stories.tsx | 1 + code/core/src/manager/components/sidebar/TagsFilter.stories.tsx | 1 + .../src/manager/components/sidebar/TagsFilterPanel.stories.tsx | 1 + .../src/manager/components/sidebar/TestingModule.stories.tsx | 1 + 9 files changed, 9 insertions(+) diff --git a/code/core/src/manager/components/sidebar/FileSearchList.stories.tsx b/code/core/src/manager/components/sidebar/FileSearchList.stories.tsx index 2c5ec140ee86..7388bf3cb4ab 100644 --- a/code/core/src/manager/components/sidebar/FileSearchList.stories.tsx +++ b/code/core/src/manager/components/sidebar/FileSearchList.stories.tsx @@ -5,6 +5,7 @@ import { FileSearchList } from './FileSearchList'; const meta = { component: FileSearchList, + title: 'Sidebar/FileSearchList', args: { onNewStory: fn(), }, diff --git a/code/core/src/manager/components/sidebar/FileSearchListSkeleton.stories.tsx b/code/core/src/manager/components/sidebar/FileSearchListSkeleton.stories.tsx index eaeaca9c0f75..d8d38eeb6881 100644 --- a/code/core/src/manager/components/sidebar/FileSearchListSkeleton.stories.tsx +++ b/code/core/src/manager/components/sidebar/FileSearchListSkeleton.stories.tsx @@ -4,6 +4,7 @@ import { FileSearchListLoadingSkeleton } from './FileSearchListSkeleton'; const meta = { component: FileSearchListLoadingSkeleton, + title: 'Sidebar/FileSearchListLoadingSkeleton', } satisfies Meta<typeof FileSearchListLoadingSkeleton>; export default meta; diff --git a/code/core/src/manager/components/sidebar/FileSearchModal.stories.tsx b/code/core/src/manager/components/sidebar/FileSearchModal.stories.tsx index a2a10005b887..c9e1790ef118 100644 --- a/code/core/src/manager/components/sidebar/FileSearchModal.stories.tsx +++ b/code/core/src/manager/components/sidebar/FileSearchModal.stories.tsx @@ -8,6 +8,7 @@ import { FileSearchModal } from './FileSearchModal'; const meta = { component: FileSearchModal, + title: 'Sidebar/FileSearchModal', args: { open: true, setError: fn(), diff --git a/code/core/src/manager/components/sidebar/FilterToggle.stories.ts b/code/core/src/manager/components/sidebar/FilterToggle.stories.ts index 6710af80c37c..075b8c94dc9a 100644 --- a/code/core/src/manager/components/sidebar/FilterToggle.stories.ts +++ b/code/core/src/manager/components/sidebar/FilterToggle.stories.ts @@ -4,6 +4,7 @@ import { FilterToggle } from './FilterToggle'; export default { component: FilterToggle, + title: 'Sidebar/FilterToggle', args: { active: false, onClick: fn(), diff --git a/code/core/src/manager/components/sidebar/IconSymbols.stories.tsx b/code/core/src/manager/components/sidebar/IconSymbols.stories.tsx index 76136230d3d8..143c08932d37 100644 --- a/code/core/src/manager/components/sidebar/IconSymbols.stories.tsx +++ b/code/core/src/manager/components/sidebar/IconSymbols.stories.tsx @@ -4,6 +4,7 @@ import { IconSymbols } from './IconSymbols'; const meta = { component: IconSymbols, + title: 'Sidebar/IconSymbols', } satisfies Meta<typeof IconSymbols>; export default meta; diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx index 444f0c185dbf..e09d0cafb62b 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx @@ -42,6 +42,7 @@ const managerContext: any = { export default { component: SidebarBottomBase, + title: 'Sidebar/SidebarBottom', args: { isDevelopment: true, diff --git a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx index 4050986a91f1..89236acda68a 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx @@ -5,6 +5,7 @@ import { TagsFilter } from './TagsFilter'; const meta = { component: TagsFilter, + title: 'Sidebar/TagsFilter', tags: ['haha'], args: { api: { diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx index 999c5f3fdb04..0602d0ed7a43 100644 --- a/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.stories.tsx @@ -5,6 +5,7 @@ import { TagsFilterPanel } from './TagsFilterPanel'; const meta = { component: TagsFilterPanel, + title: 'Sidebar/TagsFilterPanel', args: { toggleTag: fn(), api: { diff --git a/code/core/src/manager/components/sidebar/TestingModule.stories.tsx b/code/core/src/manager/components/sidebar/TestingModule.stories.tsx index 3278254c3b12..3ad57e354c1a 100644 --- a/code/core/src/manager/components/sidebar/TestingModule.stories.tsx +++ b/code/core/src/manager/components/sidebar/TestingModule.stories.tsx @@ -76,6 +76,7 @@ const managerContext: any = { const meta = { component: TestingModule, + title: 'Sidebar/TestingModule', args: { testProviders, errorCount: 0, From 28730641a68d318d2c7d333e744b1401703890c8 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Mon, 18 Nov 2024 14:58:23 +0100 Subject: [PATCH 41/47] enable addon-test context menu --- code/addons/test/src/manager.tsx | 5 ----- code/core/src/manager-api/modules/experimental_testmodule.ts | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 9a3d9312fd36..db31ac1f2873 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -88,11 +88,6 @@ addons.register(ADDON_ID, (api) => { return null; } - // TODO: remove this... right now: always returns false, to disable the feature - if (Date.now()) { - return false; - } - return <ContextMenuItem context={context} state={state} ListItem={ListItem} />; }, diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts index b16956be7a83..7eb6203e972e 100644 --- a/code/core/src/manager-api/modules/experimental_testmodule.ts +++ b/code/core/src/manager-api/modules/experimental_testmodule.ts @@ -41,7 +41,7 @@ export type SubAPI = { cancelTestProvider(id: TestProviderId): void; }; -export const init: ModuleFn = ({ store, fullAPI }) => { +export const init: ModuleFn<SubAPI, SubState> = ({ store, fullAPI }) => { const state: SubState = { testProviders: store.getState().testProviders || {}, }; @@ -194,6 +194,5 @@ export const init: ModuleFn = ({ store, fullAPI }) => { store.setState({ testProviders: initialState }, { persistence: 'session' }); }; - return { init: initModule, state, api }; }; From 3d70d51a52a729d206362ab4fd82de2a683415db Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Mon, 18 Nov 2024 15:52:26 +0100 Subject: [PATCH 42/47] rename testproviders to testProviders & move RelativeTime component --- code/addons/test/src/components/ContextMenuItem.tsx | 2 +- code/addons/test/src/{ => components}/RelativeTime.tsx | 2 +- code/addons/test/src/manager.tsx | 2 +- code/core/src/manager-api/root.tsx | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) rename code/addons/test/src/{ => components}/RelativeTime.tsx (93%) diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx index 7640c0018bb5..8cece6004c53 100644 --- a/code/addons/test/src/components/ContextMenuItem.tsx +++ b/code/addons/test/src/components/ContextMenuItem.tsx @@ -14,9 +14,9 @@ import { type API_HashEntry, type Addon_TestProviderState } from 'storybook/inte import { PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons'; -import { RelativeTime } from '../RelativeTime'; import { TEST_PROVIDER_ID } from '../constants'; import type { TestResult } from '../node/reporter'; +import { RelativeTime } from './RelativeTime'; export const ContextMenuItem: FC<{ context: API_HashEntry; diff --git a/code/addons/test/src/RelativeTime.tsx b/code/addons/test/src/components/RelativeTime.tsx similarity index 93% rename from code/addons/test/src/RelativeTime.tsx rename to code/addons/test/src/components/RelativeTime.tsx index 9225b29fe2c0..d643960b06ed 100644 --- a/code/addons/test/src/RelativeTime.tsx +++ b/code/addons/test/src/components/RelativeTime.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -import { getRelativeTimeString } from './manager'; +import { getRelativeTimeString } from '../manager'; export const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: number }) => { const [relativeTimeString, setRelativeTimeString] = useState(null); diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 9a3d9312fd36..618d9a0aa19b 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -14,10 +14,10 @@ import { import { EyeIcon, PlayHollowIcon, StopAltHollowIcon } from '@storybook/icons'; -import { RelativeTime } from './RelativeTime'; import { ContextMenuItem } from './components/ContextMenuItem'; import { GlobalErrorModal } from './components/GlobalErrorModal'; import { Panel } from './components/Panel'; +import { RelativeTime } from './components/RelativeTime'; import { Title } from './components/Title'; import { ADDON_ID, PANEL_ID, TEST_PROVIDER_ID } from './constants'; import type { TestResult } from './node/reporter'; diff --git a/code/core/src/manager-api/root.tsx b/code/core/src/manager-api/root.tsx index a314e9372eb6..c2784329ab8f 100644 --- a/code/core/src/manager-api/root.tsx +++ b/code/core/src/manager-api/root.tsx @@ -50,7 +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 testProviders from './modules/experimental_testmodule'; import * as globals from './modules/globals'; import * as layout from './modules/layout'; import * as notifications from './modules/notifications'; @@ -80,7 +80,7 @@ export type State = layout.SubState & stories.SubState & refs.SubState & notifications.SubState & - testproviders.SubState & + testProviders.SubState & version.SubState & url.SubState & shortcuts.SubState & @@ -100,7 +100,7 @@ export type API = addons.SubAPI & globals.SubAPI & layout.SubAPI & notifications.SubAPI & - testproviders.SubAPI & + testProviders.SubAPI & shortcuts.SubAPI & settings.SubAPI & version.SubAPI & @@ -181,7 +181,7 @@ class ManagerProvider extends Component<ManagerProviderProps, State> { addons, layout, notifications, - testproviders, + testProviders, settings, shortcuts, stories, From adf61801704e3a99e09fa133caec28d7aba19729 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Mon, 18 Nov 2024 16:18:51 +0100 Subject: [PATCH 43/47] add interaction component test --- .../components/sidebar/ContextMenu.tsx | 1 + .../components/sidebar/Tree.stories.tsx | 77 ++++++++++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index 3348da2657b5..8134ac10757c 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -58,6 +58,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) data-displayed={isOpen ? 'on' : 'off'} closeOnOutsideClick placement="bottom-end" + data-testid="context-menu" onVisibleChange={(visible) => { if (!visible) { handlers.onClose(); diff --git a/code/core/src/manager/components/sidebar/Tree.stories.tsx b/code/core/src/manager/components/sidebar/Tree.stories.tsx index fec1ec648057..38a35e16be33 100644 --- a/code/core/src/manager/components/sidebar/Tree.stories.tsx +++ b/code/core/src/manager/components/sidebar/Tree.stories.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { expect, fn, within } from '@storybook/test'; +import { expect, fn, userEvent, within } from '@storybook/test'; import { type ComponentEntry, type IndexHash, ManagerContext } from '@storybook/core/manager-api'; @@ -19,12 +19,45 @@ const managerContext: any = { autodocs: 'tag', docsMode: false, }, - testProviders: {}, + testProviders: { + 'component-tests': { + type: 'experimental_TEST_PROVIDER', + id: 'component-tests', + render: () => 'Component tests', + contextMenu: () => <div>TEST_PROVIDER_CONTEXT_CONTENT</div>, + runnable: true, + watchable: true, + }, + 'visual-tests': { + type: 'experimental_TEST_PROVIDER', + id: 'visual-tests', + render: () => 'Visual tests', + contextMenu: () => null, + runnable: true, + }, + }, }, api: { on: fn().mockName('api::on'), off: fn().mockName('api::off'), - getElements: fn(() => ({})), + emit: fn().mockName('api::emit'), + getElements: fn(() => ({ + 'component-tests': { + type: 'experimental_TEST_PROVIDER', + id: 'component-tests', + render: () => 'Component tests', + contextMenu: () => <div>TEST_PROVIDER_CONTEXT_CONTENT</div>, + runnable: true, + watchable: true, + }, + 'visual-tests': { + type: 'experimental_TEST_PROVIDER', + id: 'visual-tests', + render: () => 'Visual tests', + contextMenu: () => null, + runnable: true, + }, + })), }, }; @@ -254,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'); + }, +}; From ac49cf16ca95dc5db7569d475ab0edd64b432eb9 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Tue, 19 Nov 2024 10:41:40 +0100 Subject: [PATCH 44/47] cleanup --- .../test/src/components/ContextMenuItem.tsx | 7 ++++++- .../components/{Title.tsx => PanelTitle.tsx} | 2 +- code/addons/test/src/manager.tsx | 10 +++++----- .../modules/experimental_testmodule.ts | 20 ++++--------------- 4 files changed, 16 insertions(+), 23 deletions(-) rename code/addons/test/src/components/{Title.tsx => PanelTitle.tsx} (95%) diff --git a/code/addons/test/src/components/ContextMenuItem.tsx b/code/addons/test/src/components/ContextMenuItem.tsx index 8cece6004c53..5cc6b58297a1 100644 --- a/code/addons/test/src/components/ContextMenuItem.tsx +++ b/code/addons/test/src/components/ContextMenuItem.tsx @@ -76,7 +76,12 @@ export const ContextMenuItem: FC<{ } return ( - <div onClick={(event) => event.stopPropagation()}> + <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} diff --git a/code/addons/test/src/components/Title.tsx b/code/addons/test/src/components/PanelTitle.tsx similarity index 95% rename from code/addons/test/src/components/Title.tsx rename to code/addons/test/src/components/PanelTitle.tsx index 2e7c063f4e4c..4ef1ddedd302 100644 --- a/code/addons/test/src/components/Title.tsx +++ b/code/addons/test/src/components/PanelTitle.tsx @@ -5,7 +5,7 @@ import { useAddonState } from 'storybook/internal/manager-api'; import { ADDON_ID } from '../constants'; -export function Title() { +export function PanelTitle() { const [addonState = {}] = useAddonState(ADDON_ID); const { hasException, interactionsCount } = addonState as any; diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index fadef63f49e7..7294cedfdf8d 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -17,8 +17,8 @@ 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 { Title } from './components/Title'; import { ADDON_ID, PANEL_ID, TEST_PROVIDER_ID } from './constants'; import type { TestResult } from './node/reporter'; @@ -53,7 +53,7 @@ const Info = styled.div({ marginLeft: 6, }); -const Title2 = styled.div<{ crashed?: boolean }>(({ crashed, theme }) => ({ +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, @@ -131,9 +131,9 @@ addons.register(ADDON_ID, (api) => { return ( <> <Info> - <Title2 crashed={state.crashed} id="testing-module-title"> + <SidebarContextMenuTitle crashed={state.crashed} id="testing-module-title"> {title} - </Title2> + </SidebarContextMenuTitle> <Description id="testing-module-description">{description}</Description> </Info> @@ -231,7 +231,7 @@ addons.register(ADDON_ID, (api) => { addons.add(PANEL_ID, { type: types.PANEL, - title: () => <Title />, + title: () => <PanelTitle />, match: ({ viewMode }) => viewMode === 'story', render: ({ active }) => { return ( diff --git a/code/core/src/manager-api/modules/experimental_testmodule.ts b/code/core/src/manager-api/modules/experimental_testmodule.ts index 7eb6203e972e..54bfdc437da2 100644 --- a/code/core/src/manager-api/modules/experimental_testmodule.ts +++ b/code/core/src/manager-api/modules/experimental_testmodule.ts @@ -12,6 +12,8 @@ import { type TestingModuleRunRequestPayload, } from '@storybook/core/core-events'; +import invariant from 'tiny-invariant'; + import type { ModuleFn } from '../lib/types'; export type SubState = { @@ -76,27 +78,20 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, fullAPI }) => { ); }, runTestProvider(id, options) { - console.log('LOG: runTestProvider', id, options); if (!options?.entryId) { - console.log('LOG: runTestProvider: no entryId, running all tests'); const payload: TestingModuleRunAllRequestPayload = { providerId: id }; fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, payload); return () => api.cancelTestProvider(id); } const index = store.getState().index; - if (!index) { - throw new Error('no index?'); - } + invariant(index, 'The index is currently unavailable'); const entry = index[options.entryId]; - if (!entry) { - throw new Error('no entry?'); - } + invariant(entry, `No entry found in the index for id '${options.entryId}'`); if (entry.type === 'story') { - console.log('LOG: runTestProvider: running single story', entry); const payload: TestingModuleRunRequestPayload = { providerId: id, payload: [ @@ -119,10 +114,8 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, fullAPI }) => { const findComponents = (entryId: StoryId) => { const foundEntry = index[entryId]; - console.log(`Processing entry: ${entryId}`, foundEntry); switch (foundEntry.type) { case 'component': - console.log(`Adding component entry: ${entryId}`); const firstStoryId = foundEntry.children.find( (childId) => index[childId].type === 'story' ); @@ -135,8 +128,6 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, fullAPI }) => { case 'story': { // this shouldn't happen because we don't visit components' children. // so we never get to a story directly. - // unless groups can have direct stories without components? - console.log(`Adding story entry: ${entryId}`); payloads.add({ importPath: foundEntry.importPath, stories: [ @@ -152,18 +143,15 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, fullAPI }) => { return; } default: - console.log(`Processing children of entry: ${entryId}`); foundEntry.children.forEach(findComponents); } }; - console.log(`Starting to find components for entryId:`, options.entryId); findComponents(options.entryId); const payload: TestingModuleRunRequestPayload = { providerId: id, payload: Array.from(payloads), }; - console.log('LOG: payload', payload); fullAPI.emit(TESTING_MODULE_RUN_REQUEST, payload); return () => api.cancelTestProvider(id); From fe2cec75e1a73bdade071ca52aef95ac4b06affe Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Tue, 19 Nov 2024 10:43:39 +0100 Subject: [PATCH 45/47] rename contextMenu > sidebarContextMenu in addon API --- code/addons/test/src/manager.tsx | 2 +- code/core/src/manager/components/sidebar/ContextMenu.tsx | 3 +-- code/core/src/manager/components/sidebar/Tree.stories.tsx | 8 ++++---- code/core/src/types/modules/addons.ts | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 7294cedfdf8d..b53f4bdfb1d7 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -83,7 +83,7 @@ addons.register(ADDON_ID, (api) => { watchable: true, name: 'Component tests', - contextMenu: ({ context, state }, { ListItem }) => { + sidebarContextMenu: ({ context, state }, { ListItem }) => { if (context.type === 'docs') { return null; } diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index 8134ac10757c..76118ffc8711 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -106,8 +106,7 @@ export function generateTestProviderLinks( if (!state) { return null; } - - const content = state.contextMenu?.({ context, state }, ContextMenu); + const content = state.sidebarContextMenu?.({ context, state }, ContextMenu); if (!content) { return null; diff --git a/code/core/src/manager/components/sidebar/Tree.stories.tsx b/code/core/src/manager/components/sidebar/Tree.stories.tsx index 38a35e16be33..baadd2e60fec 100644 --- a/code/core/src/manager/components/sidebar/Tree.stories.tsx +++ b/code/core/src/manager/components/sidebar/Tree.stories.tsx @@ -24,7 +24,7 @@ const managerContext: any = { type: 'experimental_TEST_PROVIDER', id: 'component-tests', render: () => 'Component tests', - contextMenu: () => <div>TEST_PROVIDER_CONTEXT_CONTENT</div>, + sidebarContextMenu: () => <div>TEST_PROVIDER_CONTEXT_CONTENT</div>, runnable: true, watchable: true, }, @@ -32,7 +32,7 @@ const managerContext: any = { type: 'experimental_TEST_PROVIDER', id: 'visual-tests', render: () => 'Visual tests', - contextMenu: () => null, + sidebarContextMenu: () => null, runnable: true, }, }, @@ -46,7 +46,7 @@ const managerContext: any = { type: 'experimental_TEST_PROVIDER', id: 'component-tests', render: () => 'Component tests', - contextMenu: () => <div>TEST_PROVIDER_CONTEXT_CONTENT</div>, + sidebarContextMenu: () => <div>TEST_PROVIDER_CONTEXT_CONTENT</div>, runnable: true, watchable: true, }, @@ -54,7 +54,7 @@ const managerContext: any = { type: 'experimental_TEST_PROVIDER', id: 'visual-tests', render: () => 'Visual tests', - contextMenu: () => null, + sidebarContextMenu: () => null, runnable: true, }, })), diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index 4fb2de2f2891..d01563530bb7 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -479,7 +479,7 @@ export interface Addon_TestProviderType< /** @deprecated Use render instead */ description?: (state: TestProviderConfig & Addon_TestProviderState<Details>) => ReactNode; render?: (state: TestProviderConfig & Addon_TestProviderState<Details>) => ReactNode; - contextMenu?: ( + sidebarContextMenu?: ( options: { context: API_HashEntry; state: Addon_TestProviderState<Details>; From b6aa053ce5a39aee81b5ca73efcb16dc2341827a Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Tue, 19 Nov 2024 11:13:13 +0100 Subject: [PATCH 46/47] hide context menu from test-less stories --- code/addons/test/src/manager.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index b53f4bdfb1d7..9b560cdd8cee 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -87,6 +87,9 @@ addons.register(ADDON_ID, (api) => { if (context.type === 'docs') { return null; } + if (context.type === 'story' && !context.tags.includes('test')) { + return null; + } return <ContextMenuItem context={context} state={state} ListItem={ListItem} />; }, From f771bdffa68d8988934d3d96429f4658aea2d3f4 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold <jeppe@reinhold.is> Date: Tue, 19 Nov 2024 11:13:22 +0100 Subject: [PATCH 47/47] fix React key warning --- .../src/components/components/tooltip/TooltipLinkList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/core/src/components/components/tooltip/TooltipLinkList.tsx b/code/core/src/components/components/tooltip/TooltipLinkList.tsx index 844a4ed228b2..91e86eae71af 100644 --- a/code/core/src/components/components/tooltip/TooltipLinkList.tsx +++ b/code/core/src/components/components/tooltip/TooltipLinkList.tsx @@ -1,5 +1,5 @@ import type { ComponentProps, ReactNode, SyntheticEvent } from 'react'; -import React, { useCallback } from 'react'; +import React, { Fragment, useCallback } from 'react'; import { styled } from '@storybook/core/theming'; @@ -77,7 +77,7 @@ export const TooltipLinkList = ({ links, LinkWrapper, ...props }: TooltipLinkLis <Group key={group.map((link) => link.id).join(`~${index}~`)}> {group.map((link) => { if ('content' in link) { - return link.content; + return <Fragment key={link.id}>{link.content}</Fragment>; } return ( <Item key={link.id} isIndented={isIndented} LinkWrapper={LinkWrapper} {...link} />