diff --git a/code/core/src/component-testing/components/Panel.tsx b/code/core/src/component-testing/components/Panel.tsx index 977c37ea3c63..3ac770251dee 100644 --- a/code/core/src/component-testing/components/Panel.tsx +++ b/code/core/src/component-testing/components/Panel.tsx @@ -68,6 +68,8 @@ const playStatusMap: Record< aborted: 'aborted', }; +const terminalStatuses: PlayStatus[] = ['completed', 'errored', 'aborted']; + const storyStatusMap: Record = { [CallStates.DONE]: 'status-value:success', [CallStates.ERROR]: 'status-value:error', @@ -293,7 +295,7 @@ export const Panel = memo<{ refId?: string; storyId: string; storyUrl: string }> } else { set((state) => { const status = - event.newPhase in playStatusMap + event.newPhase in playStatusMap && !terminalStatuses.includes(state.status) ? playStatusMap[event.newPhase as keyof typeof playStatusMap] : state.status; return getPanelState( diff --git a/code/core/src/component-testing/components/StatusBadge.tsx b/code/core/src/component-testing/components/StatusBadge.tsx index 8d94c1eabece..23a3b26d13a1 100644 --- a/code/core/src/component-testing/components/StatusBadge.tsx +++ b/code/core/src/component-testing/components/StatusBadge.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { type Color, styled, typography } from 'storybook/theming'; +import { TooltipNote, WithTooltip } from '../../components'; + export type PlayStatus = 'rendering' | 'playing' | 'completed' | 'errored' | 'aborted'; export interface StatusBadgeProps { @@ -24,6 +26,14 @@ const StatusTextMapping: Record = { aborted: 'Bail', } as const; +const StatusNoteMapping: Record = { + rendering: 'Story is rendering', + playing: 'Interactions are running', + completed: 'Story ran successfully', + errored: 'Story failed to complete', + aborted: 'Interactions aborted due to file changes', +} as const; + const StyledBadge = styled.div(({ theme, status }) => { const backgroundColor = theme.color[StatusColorMapping[status]]; return { @@ -44,9 +54,17 @@ const StyledBadge = styled.div(({ theme, status }) => { export const StatusBadge: React.FC = ({ status }) => { const badgeText = StatusTextMapping[status]; + const badgeNote = StatusNoteMapping[status]; return ( - - {badgeText} - + } + > + + {badgeText} + + ); }; diff --git a/code/core/src/component-testing/components/Subnav.tsx b/code/core/src/component-testing/components/Subnav.tsx index 875d2a4d6eba..51729516fc2d 100644 --- a/code/core/src/component-testing/components/Subnav.tsx +++ b/code/core/src/component-testing/components/Subnav.tsx @@ -35,7 +35,7 @@ const SubnavWrapper = styled.div(({ theme }) => ({ })); const StyledSubnav = styled.nav({ - height: 40, + height: 39, display: 'flex', alignItems: 'center', justifyContent: 'space-between', @@ -102,7 +102,6 @@ const RewindButton = styled(StyledIconButton)({ const JumpToEndButton = styled(StyledButton)({ marginLeft: 9, marginRight: 9, - marginBottom: 1, lineHeight: '12px', }); diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx index 16694d57f876..cfd094f6a704 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -27,6 +27,20 @@ const BUILT_IN_TAGS = new Set([ 'test-fn', ]); +// Immutable set operations +const add = (set: Set, id: string) => { + const copy = new Set(set); + copy.add(id); + return copy; +}; +const remove = (set: Set, id: string) => { + const copy = new Set(set); + copy.delete(id); + return copy; +}; +const equal = (left: Set, right: Set) => + left.size === right.size && new Set([...left, ...right]).size === left.size; + const Wrapper = styled.div({ position: 'relative', }); @@ -58,17 +72,20 @@ export interface TagsFilterProps { export const TagsFilter = ({ api, indexJson, isDevelopment, tagPresets }: TagsFilterProps) => { const filtersById = useMemo<{ [id: string]: Filter }>(() => { - const userTagsCounts = Object.values(indexJson.entries).reduce((acc, entry) => { - entry.tags?.forEach((tag: Tag) => { - if (!BUILT_IN_TAGS.has(tag)) { - acc.set(tag, (acc.get(tag) || 0) + 1); - } - }); - return acc; - }, new Map()); + const userTagsCounts = Object.values(indexJson.entries).reduce<{ [key: Tag]: number }>( + (acc, entry) => { + entry.tags?.forEach((tag: Tag) => { + if (!BUILT_IN_TAGS.has(tag)) { + acc[tag] = (acc[tag] || 0) + 1; + } + }); + return acc; + }, + {} + ); const userFilters = Object.fromEntries( - userTagsCounts.entries().map(([tag, count]) => { + Object.entries(userTagsCounts).map(([tag, count]) => { const filterFn = (entry: API_PreparedIndexEntry, excluded?: boolean) => excluded ? !entry.tags?.includes(tag) : !!entry.tags?.includes(tag); return [tag, { id: tag, type: 'tag', title: tag, count, filterFn }]; @@ -163,19 +180,18 @@ export const TagsFilter = ({ api, indexJson, isDevelopment, tagPresets }: TagsFi const toggleFilter = useCallback( (id: string, selected: boolean, excluded?: boolean) => { - const set = new Set([id]); if (excluded === true) { - setExcludedFilters(excludedFilters.union(set)); - setIncludedFilters(includedFilters.difference(set)); + setExcludedFilters(add(excludedFilters, id)); + setIncludedFilters(remove(includedFilters, id)); } else if (excluded === false) { - setIncludedFilters(includedFilters.union(set)); - setExcludedFilters(excludedFilters.difference(set)); + setIncludedFilters(add(includedFilters, id)); + setExcludedFilters(remove(excludedFilters, id)); } else if (selected) { - setIncludedFilters(includedFilters.union(set)); - setExcludedFilters(excludedFilters.difference(set)); + setIncludedFilters(add(includedFilters, id)); + setExcludedFilters(remove(excludedFilters, id)); } else { - setIncludedFilters(includedFilters.difference(set)); - setExcludedFilters(excludedFilters.difference(set)); + setIncludedFilters(remove(includedFilters, id)); + setExcludedFilters(remove(excludedFilters, id)); } }, [includedFilters, excludedFilters] @@ -224,8 +240,7 @@ export const TagsFilter = ({ api, indexJson, isDevelopment, tagPresets }: TagsFi resetFilters={resetFilters} isDevelopment={isDevelopment} isDefaultSelection={ - includedFilters.symmetricDifference(defaultIncluded).size === 0 && - excludedFilters.symmetricDifference(defaultExcluded).size === 0 + equal(includedFilters, defaultIncluded) && equal(excludedFilters, defaultExcluded) } hasDefaultSelection={defaultIncluded.size > 0 || defaultExcluded.size > 0} /> diff --git a/code/core/src/manager/components/sidebar/useExpanded.ts b/code/core/src/manager/components/sidebar/useExpanded.ts index 878cc5f5f741..9ed62872df0b 100644 --- a/code/core/src/manager/components/sidebar/useExpanded.ts +++ b/code/core/src/manager/components/sidebar/useExpanded.ts @@ -41,18 +41,24 @@ const initializeExpanded = ({ initialExpanded, highlightedRef, rootIds, + selectedStoryId, }: { refId: string; data: StoriesHash; initialExpanded?: ExpandedState; highlightedRef: MutableRefObject; rootIds: string[]; + selectedStoryId: string | null; }) => { - const highlightedAncestors = - highlightedRef.current?.refId === refId - ? getAncestorIds(data, highlightedRef.current?.itemId) - : []; - return [...rootIds, ...highlightedAncestors].reduce( + const selectedStory = selectedStoryId && data[selectedStoryId]; + const candidates = [...rootIds]; + if (highlightedRef.current?.refId === refId) { + candidates.push(...getAncestorIds(data, highlightedRef.current?.itemId)); + } + if (selectedStory && 'children' in selectedStory && selectedStory.children?.length) { + candidates.push(selectedStoryId); + } + return candidates.reduce( // @ts-expect-error (non strict) (acc, id) => Object.assign(acc, { [id]: id in initialExpanded ? initialExpanded[id] : true }), {} @@ -90,7 +96,7 @@ export const useExpanded = ({ (state, { ids, value }) => ids.reduce((acc, id) => Object.assign(acc, { [id]: value }), { ...state }), // @ts-expect-error (non strict) - { refId, data, highlightedRef, rootIds, initialExpanded }, + { refId, data, highlightedRef, rootIds, initialExpanded, selectedStoryId }, initializeExpanded ); diff --git a/code/e2e-tests/component-tests.spec.ts b/code/e2e-tests/component-tests.spec.ts index 3059bda9d5a7..0af1a3f08b09 100644 --- a/code/e2e-tests/component-tests.spec.ts +++ b/code/e2e-tests/component-tests.spec.ts @@ -69,7 +69,7 @@ test.describe('interactions', () => { await expect(interactionsTab).toBeVisible(); const panel = sbPage.panelContent(); - const runStatusBadge = panel.locator('[aria-label="Status of the test run"]'); + const runStatusBadge = panel.locator('[aria-label="Story status"]'); await expect(runStatusBadge).toContainText(/Pass/); await expect(panel).toContainText(/"initial value"/); await expect(panel).toContainText(/clear/); @@ -139,7 +139,7 @@ test.describe('interactions', () => { await expect(button).toContainText('Button', { timeout: 50000 }); const panel = sbPage.panelContent(); - await expect(panel).toContainText(/Pass/); + await expect(panel).toContainText(/Fail/); await expect(panel).toContainText(/Found 1 unhandled error/); await expect(panel).toBeVisible(); }); diff --git a/code/e2e-tests/preview-api.spec.ts b/code/e2e-tests/preview-api.spec.ts index 997f8fc6eff2..3e3b9947e1b5 100644 --- a/code/e2e-tests/preview-api.spec.ts +++ b/code/e2e-tests/preview-api.spec.ts @@ -30,7 +30,7 @@ test.describe('preview-api', () => { const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel'); await expect(interactionsTab).toBeVisible(); const panel = sbPage.panelContent(); - const runStatusBadge = panel.locator('[aria-label="Status of the test run"]'); + const runStatusBadge = panel.locator('[aria-label="Story status"]'); await expect(runStatusBadge).toContainText(/Pass/); // click outside, to remove focus from the input of the story, then press S to toggle sidebar