Skip to content
9 changes: 7 additions & 2 deletions code/addons/vitest/src/use-test-provider-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import {
import { ADDON_ID, STATUS_TYPE_ID_A11Y, STATUS_TYPE_ID_COMPONENT_TEST } from './constants';
import type { StoreState } from './types';

export type StatusValueToStoryIds = Record<StatusValue, StoryId[]>;
type TestStatusValue = Extract<
StatusValue,
`status-value:${'pending' | 'success' | 'error' | 'warning' | 'unknown'}`
>;

export type StatusValueToStoryIds = Record<TestStatusValue, StoryId[]>;

Comment thread
valentinpalkovic marked this conversation as resolved.
const statusValueToStoryIds = (
allStatuses: StatusesByStoryIdAndTypeId,
Expand All @@ -43,7 +48,7 @@ const statusValueToStoryIds = (
if (!status) {
return;
}
statusValueToStoryIdsMap[status.value].push(status.storyId);
statusValueToStoryIdsMap[status.value as TestStatusValue].push(status.storyId);
});

return statusValueToStoryIdsMap;
Expand Down
68 changes: 67 additions & 1 deletion code/core/src/manager/components/sidebar/IconSymbols.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const SUCCESS_ID = 'icon--success';
const ERROR_ID = 'icon--error';
const WARNING_ID = 'icon--warning';
const DOT_ID = 'icon--dot';
const NEW_ID = 'icon--new';
const MODIFIED_ID = 'icon--modified';
const AFFECTED_ID = 'icon--affected';

export const IconSymbols: FC = () => {
return (
Expand Down Expand Up @@ -104,6 +107,53 @@ export const IconSymbols: FC = () => {
<symbol id={DOT_ID}>
<circle cx="3" cy="3" r="3" fill="currentColor" />
</symbol>
<symbol id={NEW_ID}>
Comment thread
valentinpalkovic marked this conversation as resolved.
<path
d="M7 3.5L6.96971 3.68173C6.68873 5.36762 5.36762 6.68873 3.68173 6.96971L3.5 7"
stroke="currentColor"
strokeLinecap="round"
fill="none"
/>
<path
d="M7 3.5L7.03029 3.68173C7.31127 5.36762 8.63238 6.68873 10.3183 6.96971L10.5 7"
stroke="currentColor"
strokeLinecap="round"
fill="none"
/>
<path
d="M7 10.5L6.96971 10.3183C6.68873 8.63238 5.36762 7.31127 3.68173 7.03029L3.5 7"
stroke="currentColor"
strokeLinecap="round"
fill="none"
/>
<path
d="M7 10.5L7.03029 10.3183C7.31127 8.63238 8.63238 7.31127 10.3183 7.03029L10.5 7"
stroke="currentColor"
strokeLinecap="round"
fill="none"
/>
<path d="M7 4.5L4.5 7L7 9.5L9.5 7L7 4.5Z" fill="currentColor" />
</symbol>
<symbol id={MODIFIED_ID}>
<circle cx="7" cy="7" r="3" fill="currentColor" />
</symbol>
<symbol id={AFFECTED_ID}>
<circle cx="7" cy="7" r="2" fill="currentColor" />
<path
d="M7 3.5A3.5 3.5 0 0 0 7 10.5"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
fill="none"
/>
<path
d="M7 3.5A3.5 3.5 0 0 1 7 10.5"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
fill="none"
/>
</symbol>
</Svg>
);
};
Expand All @@ -118,7 +168,10 @@ export const UseSymbol: FC<{
| 'success'
| 'error'
| 'warning'
| 'dot';
| 'dot'
| 'new'
| 'modified'
| 'affected';
}> = ({ type }) => {
if (type === 'group') {
return <use xlinkHref={`#${GROUP_ID}`} />;
Expand Down Expand Up @@ -155,5 +208,18 @@ export const UseSymbol: FC<{
if (type === 'dot') {
return <use xlinkHref={`#${DOT_ID}`} />;
}

if (type === 'new') {
return <use xlinkHref={`#${NEW_ID}`} />;
}

if (type === 'modified') {
return <use xlinkHref={`#${MODIFIED_ID}`} />;
}

if (type === 'affected') {
return <use xlinkHref={`#${AFFECTED_ID}`} />;
}

return null;
};
129 changes: 128 additions & 1 deletion code/core/src/manager/components/sidebar/Sidebar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from 'react';

import type { DecoratorFunction, StatusesByStoryIdAndTypeId } from 'storybook/internal/types';
import type {
DecoratorFunction,
StatusValue,
StatusesByStoryIdAndTypeId,
} from 'storybook/internal/types';

import { global } from '@storybook/global';

Expand Down Expand Up @@ -530,3 +534,126 @@ export const Scrolled: Story = {
await expect(scrollable.scrollTop).toBe(scrollable.scrollHeight - scrollable.clientHeight);
},
};

export const StatusesNew: Story = {
args: {
allStatuses: Object.entries(index).reduce((acc, [id, item]) => {
if (item.type !== 'story') return acc;
return {
...acc,
[id]: {
addonA: {
typeId: 'addonA',
storyId: id,
value: 'status-value:new' as StatusValue,
title: 'Change Detection',
description: 'This story is new',
},
},
} satisfies StatusesByStoryIdAndTypeId;
}, {} as StatusesByStoryIdAndTypeId),
},
play: waitForChecklistWidget,
};

export const StatusesModified: Story = {
args: {
allStatuses: Object.entries(index).reduce((acc, [id, item]) => {
if (item.type !== 'story') return acc;
return {
...acc,
[id]: {
addonA: {
typeId: 'addonA',
storyId: id,
value: 'status-value:modified' as StatusValue,
title: 'Change Detection',
description: 'This story was modified',
},
},
} satisfies StatusesByStoryIdAndTypeId;
}, {} as StatusesByStoryIdAndTypeId),
},
play: waitForChecklistWidget,
};

export const StatusesAffected: Story = {
args: {
allStatuses: Object.entries(index).reduce((acc, [id, item]) => {
if (item.type !== 'story') return acc;
return {
...acc,
[id]: {
addonA: {
typeId: 'addonA',
storyId: id,
value: 'status-value:affected' as StatusValue,
title: 'Change Detection',
description: 'This story is affected by a change',
},
},
} satisfies StatusesByStoryIdAndTypeId;
}, {} as StatusesByStoryIdAndTypeId),
},
play: waitForChecklistWidget,
};

export const StatusesMixed: Story = {
args: {
allStatuses: Object.entries(index).reduce((acc, [id, item]) => {
if (item.type !== 'story') return acc;
const values: StatusValue[] = [
'status-value:new',
'status-value:modified',
'status-value:affected',
'status-value:success',
'status-value:warning',
];
const value = values[Object.keys(acc).length % values.length];
return {
...acc,
[id]: {
addonA: {
typeId: 'addonA',
storyId: id,
value,
title: 'Change Detection',
description: '',
},
},
} satisfies StatusesByStoryIdAndTypeId;
}, {} as StatusesByStoryIdAndTypeId),
},
play: waitForChecklistWidget,
};

export const StatusesChangeDetectionPriority: Story = {
args: {
allStatuses: Object.entries(index).reduce((acc, [id, item]) => {
if (item.type !== 'story') return acc;
// Cycles through all change-detection variants + warning/error to verify
// priority ordering (most critical wins): error > warning > affected > modified > new
const priorityValues: StatusValue[] = [
'status-value:new',
'status-value:modified',
'status-value:affected',
'status-value:warning',
'status-value:error',
];
const value = priorityValues[Object.keys(acc).length % priorityValues.length];
return {
...acc,
[id]: {
addonA: {
typeId: 'addonA',
storyId: id,
value,
title: 'Change Detection',
description: `Priority test: ${value}`,
},
},
} satisfies StatusesByStoryIdAndTypeId;
}, {} as StatusesByStoryIdAndTypeId),
},
play: waitForChecklistWidget,
};
3 changes: 3 additions & 0 deletions code/core/src/manager/components/sidebar/StatusButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const withStatusColor = ({ theme, status }: { theme: Theme; status: StatusValue
color: {
'status-value:pending': defaultColor,
'status-value:success': theme.color.positive,
'status-value:new': theme.fgColor.accent,
'status-value:modified': theme.fgColor.accent,
'status-value:affected': theme.fgColor.accent,
Comment thread
valentinpalkovic marked this conversation as resolved.
'status-value:error': theme.color.negative,
'status-value:warning': theme.color.warning,
'status-value:unknown': defaultColor,
Expand Down
17 changes: 14 additions & 3 deletions code/core/src/manager/components/sidebar/StatusContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,19 @@ export const useStatusSummary = (item: Item) => {
counts: {
'status-value:pending': 0,
'status-value:success': 0,
'status-value:new': 0,
'status-value:modified': 0,
'status-value:affected': 0,
'status-value:error': 0,
'status-value:warning': 0,
'status-value:unknown': 0,
},
statusesByValue: {
'status-value:pending': {},
'status-value:success': {},
'status-value:new': {},
'status-value:modified': {},
'status-value:affected': {},
'status-value:error': {},
'status-value:warning': {},
'status-value:unknown': {},
Expand All @@ -44,9 +50,14 @@ export const useStatusSummary = (item: Item) => {
data &&
allStatuses &&
groupStatus &&
['status-value:pending', 'status-value:warning', 'status-value:error'].includes(
groupStatus[item.id]
)
[
'status-value:pending',
'status-value:new',
'status-value:modified',
'status-value:affected',
'status-value:warning',
'status-value:error',
].includes(groupStatus[item.id])
) {
for (const storyId of getDescendantIds(data, item.id, false)) {
for (const status of Object.values(allStatuses[storyId] ?? {})) {
Expand Down
31 changes: 23 additions & 8 deletions code/core/src/manager/components/sidebar/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
} from '../../utils/tree';
import { useLayout } from '../layout/LayoutProvider';
import { useContextMenu } from './ContextMenu';
import { IconSymbols, UseSymbol } from './IconSymbols';
import { UseSymbol } from './IconSymbols';
import { StatusButton } from './StatusButton';
import { StatusContext } from './StatusContext';
import {
Expand Down Expand Up @@ -190,6 +190,9 @@ const StatusIconMap: Record<StatusValue, React.ReactNode | null> = {
'status-value:error': <ErrorStatusIcon />,
'status-value:warning': <WarnStatusIcon />,
'status-value:pending': <PendingStatusIcon />,
'status-value:new': null,
'status-value:modified': null,
'status-value:affected': null,
'status-value:unknown': null,
};

Expand Down Expand Up @@ -308,7 +311,7 @@ const Node = React.memo<NodeProps>(function Node(props) {
{contextMenu.node}
{icon ? (
<StatusButton
ariaLabel={`Test status: ${statusValue.replace('status-value:', '')}`}
ariaLabel={`Status: ${statusValue.replace('status-value:', '')}`}
data-testid="tree-status-button"
type="button"
status={statusValue}
Expand Down Expand Up @@ -369,7 +372,7 @@ const Node = React.memo<NodeProps>(function Node(props) {
const [itemIcon, itemColor] = getStatus(theme, itemStatus);
const itemStatusButton = itemIcon ? (
<StatusButton
ariaLabel={`Test status: ${itemStatus.replace('status-value:', '')}`}
ariaLabel={`Status: ${itemStatus.replace('status-value:', '')}`}
data-testid="tree-status-button"
role="status"
type="button"
Expand All @@ -391,7 +394,15 @@ const Node = React.memo<NodeProps>(function Node(props) {
];
const status = getMostCriticalStatusValue([itemStatus, groupStatus?.[item.id]]);
const color = status ? getStatus(theme, status)[1] : null;
const showBranchStatus = status === 'status-value:error' || status === 'status-value:warning';
const showBranchStatus = (
[
'status-value:modified',
'status-value:affected',
'status-value:new',
'status-value:warning',
'status-value:error',
] as StatusValue[]
).includes(status);

return (
<LeafNodeStyleWrapper
Expand Down Expand Up @@ -449,15 +460,19 @@ const Node = React.memo<NodeProps>(function Node(props) {
{contextMenu.node}
{showBranchStatus ? (
<StatusButton
ariaLabel={`Test status: ${status.replace('status-value:', '')}`}
ariaLabel={`Status: ${status.replace('status-value:', '')}`}
data-testid="tree-status-button"
type="button"
status={status}
selectedItem={isSelected}
>
<svg key="icon" viewBox="0 0 6 6" width="6" height="6" type="dot">
<UseSymbol type="dot" />
</svg>
{status === 'status-value:error' || status === 'status-value:warning' ? (
<svg key="icon" viewBox="0 0 6 6" width="6" height="6" type="dot">
<UseSymbol type="dot" />
</svg>
) : (
getStatus(theme, status)[0]
)}
</StatusButton>
) : (
itemStatusButton
Expand Down
Loading
Loading