From 7fe08229be40f540ccf8f0fc3657466be7ed242c Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 6 Nov 2025 13:07:31 +0100 Subject: [PATCH 01/42] UI: Improve status handling in sidebar nodes --- .../components/sidebar/ContextMenu.tsx | 80 +++++++++++++++++-- .../src/manager/components/sidebar/Tree.tsx | 37 +-------- 2 files changed, 75 insertions(+), 42 deletions(-) diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index 5f1ad015f6ba..45125ff6ef4f 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -1,5 +1,5 @@ import type { ComponentProps, FC, SyntheticEvent } from 'react'; -import React, { useMemo, useState } from 'react'; +import React, { useContext, useMemo, useState } from 'react'; import { TooltipLinkList, WithTooltip } from 'storybook/internal/components'; import { @@ -7,6 +7,7 @@ import { type Addon_Collection, type Addon_TestProviderType, Addon_TypesEnum, + type StatusValue, } from 'storybook/internal/types'; import { CopyIcon, EditorIcon, EllipsisIcon } from '@storybook/icons'; @@ -18,7 +19,10 @@ import { styled } from 'storybook/theming'; import type { Link } from '../../../components/components/tooltip/TooltipLinkList'; import { Shortcut } from '../../container/Menu'; +import { getMostCriticalStatusValue } from '../../utils/status'; +import { UseSymbol } from './IconSymbols'; import { StatusButton } from './StatusButton'; +import { StatusContext } from './StatusContext'; import type { ExcludesNull } from './Tree'; const empty = { @@ -41,6 +45,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) const [hoverCount, setHoverCount] = useState(0); const [isOpen, setIsOpen] = useState(false); const [copyText, setCopyText] = React.useState('Copy story name'); + const { allStatuses, groupStatus } = useContext(StatusContext); const shortcutKeys = api.getShortcutKeys(); const enableShortcuts = !!shortcutKeys; @@ -85,7 +90,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) } return defaultLinks; - }, [context, copyText, enableShortcuts, shortcutKeys]); + }, [api, context, copyText, enableShortcuts, shortcutKeys]); const handlers = useMemo(() => { return { @@ -118,6 +123,69 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) const shouldRender = !context.refId && (providerLinks.length > 0 || links.length > 0 || topLinks.length > 0); + const isLeafNode = context.type === 'story' || context.type === 'docs'; + + const itemStatus = useMemo(() => { + let status: StatusValue = 'status-value:unknown'; + if (!context) { + return status; + } + + if (isLeafNode) { + const values = Object.values(allStatuses?.[context.id] || {}).map((s) => s.value); + status = getMostCriticalStatusValue(values); + } + + if (!isLeafNode) { + // On component/groups we only show non-ellipsis on hover on non-success status colors + const groupValue = groupStatus && groupStatus[context.id]; + status = + groupValue === 'status-value:success' || groupValue === undefined + ? 'status-value:unknown' + : groupValue; + } + + return status; + }, [allStatuses, groupStatus, context, isLeafNode]); + + const MenuIcon = useMemo(() => { + // On component/groups we only show non-ellipsis on hover on non-success statuses + if (context.type !== 'story' && context.type !== 'docs') { + if (itemStatus !== 'status-value:success' && itemStatus !== 'status-value:unknown') { + return ( + + + + ); + } + + return ; + } + + if (itemStatus === 'status-value:error') { + return ( + + + + ); + } + if (itemStatus === 'status-value:warning') { + return ( + + + + ); + } + if (itemStatus === 'status-value:success') { + return ( + + + + ); + } + return ; + }, [itemStatus, context.type]); + return useMemo(() => { // Never show the SidebarContextMenu in production if (globalThis.CONFIG_TYPE !== 'DEVELOPMENT') { @@ -141,13 +209,13 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) }} tooltip={} > - - + + {MenuIcon} ) : null, }; - }, [context, handlers, isOpen, shouldRender, links, topLinks]); + }, [context, handlers, isOpen, shouldRender, links, topLinks, itemStatus, MenuIcon]); }; /** @@ -198,5 +266,5 @@ export function generateTestProviderLinks( content, }; }) - .filter(Boolean as any as ExcludesNull); + .filter(Boolean as unknown as ExcludesNull); } diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index 96281fd811d7..81f7f2459cca 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -231,7 +231,6 @@ const Node = React.memo(function Node(props) { api, } = props; const { isDesktop, isMobile, setMobileMenuOpen } = useLayout(); - const { counts, statusesByValue } = useStatusSummary(item); if (!isDisplayed) { return null; @@ -255,42 +254,8 @@ const Node = React.memo(function Node(props) { })); } - // TODO should this be updated for stories with tests? - if (item.type === 'component' || item.type === 'group') { - const links: Link[] = []; - const errorCount = counts['status-value:error']; - const warningCount = counts['status-value:warning']; - if (errorCount) { - links.push({ - id: 'errors', - icon: StatusIconMap['status-value:error'], - title: `${errorCount} ${errorCount === 1 ? 'story' : 'stories'} with errors`, - onClick: () => { - const [firstStoryId] = Object.entries(statusesByValue['status-value:error'])[0]; - onSelectStoryId(firstStoryId); - const errorStatuses = Object.values(statusesByValue['status-value:error']).flat(); - fullStatusStore.selectStatuses(errorStatuses); - }, - }); - } - if (warningCount) { - links.push({ - id: 'warnings', - icon: StatusIconMap['status-value:warning'], - title: `${warningCount} ${warningCount === 1 ? 'story' : 'stories'} with warnings`, - onClick: () => { - const [firstStoryId] = Object.entries(statusesByValue['status-value:warning'])[0]; - onSelectStoryId(firstStoryId); - const warningStatuses = Object.values(statusesByValue['status-value:warning']).flat(); - fullStatusStore.selectStatuses(warningStatuses); - }, - }); - } - return links; - } - return []; - }, [counts, item.id, item.type, onSelectStoryId, statuses, statusesByValue]); + }, [item.id, item.type, onSelectStoryId, statuses]); const id = createId(item.id, refId); const contextMenu = From 225c3295c847deb0072de8eedc110c32dfe669be Mon Sep 17 00:00:00 2001 From: Kyle Gach Date: Mon, 17 Nov 2025 07:52:46 -0700 Subject: [PATCH 02/42] Docs: Add migration guide for test-runner -> Vitest addon --- .../index.mdx} | 118 ++++++++------- .../vitest-addon/migration-guide.mdx | 143 ++++++++++++++++++ 2 files changed, 205 insertions(+), 56 deletions(-) rename docs/writing-tests/integrations/{vitest-addon.mdx => vitest-addon/index.mdx} (72%) create mode 100644 docs/writing-tests/integrations/vitest-addon/migration-guide.mdx diff --git a/docs/writing-tests/integrations/vitest-addon.mdx b/docs/writing-tests/integrations/vitest-addon/index.mdx similarity index 72% rename from docs/writing-tests/integrations/vitest-addon.mdx rename to docs/writing-tests/integrations/vitest-addon/index.mdx index 50148d992e2c..f52ce1e18c22 100644 --- a/docs/writing-tests/integrations/vitest-addon.mdx +++ b/docs/writing-tests/integrations/vitest-addon/index.mdx @@ -3,15 +3,19 @@ title: 'Vitest addon' sidebar: order: 1 title: Vitest addon +isTab: true +tab: + order: 1 + title: Guide --- -The Vitest addon is currently only supported in [React](?renderer=react), [Preact](?renderer=preact), [Vue](?renderer=vue), [Svelte](?renderer=svelte), and [Web Components](?renderer=web-components) projects, which use the [Vite builder](../builders/vite.mdx) (or the [Next.js framework with Vite](../get-started/frameworks/nextjs.mdx#with-vite)). +The Vitest addon is currently only supported in [React](?renderer=react), [Preact](?renderer=preact), [Vue](?renderer=vue), [Svelte](?renderer=svelte), and [Web Components](?renderer=web-components) projects, which use the [Vite builder](../../../builders/vite.mdx) (or the [Next.js framework with Vite](../../get-started/frameworks/nextjs.mdx#with-vite)). -If you are using a different renderer (such as Angular) or the Webpack builder, you can use the [Storyboook test runner](./test-runner.mdx) to test your stories. +If you are using a different renderer (such as Angular) or the Webpack builder, you can use the [Storyboook test runner](../test-runner.mdx) to test your stories. @@ -20,21 +24,21 @@ If you are using a different renderer (such as Angular) or the Webpack builder, -Storybook's Vitest addon allows you to test your components directly inside Storybook. On its own, it transforms your [stories](../../writing-stories/index.mdx) into component tests, which test the rendering and behavior of your components in a real browser environment. It can also calculate project [coverage](../test-coverage.mdx) provided by your stories. +Storybook's Vitest addon allows you to test your components directly inside Storybook. On its own, it transforms your [stories](../../../writing-stories/index.mdx) into component tests, which test the rendering and behavior of your components in a real browser environment. It can also calculate project [coverage](../../test-coverage.mdx) provided by your stories. -If your project is using other testing addons, such as the [Visual tests addon](../visual-testing.mdx) or the [Accessibility addon](../accessibility-testing.mdx), you can run those tests alongside your component tests. +If your project is using other testing addons, such as the [Visual tests addon](../../visual-testing.mdx) or the [Accessibility addon](../../accessibility-testing.mdx), you can run those tests alongside your component tests. When component tests are run for a story, the status is shown in the sidebar. The sidebar can be filtered to only show failing stories, and you can press the menu button on a failing story to see debugging options. You can also run tests in watch mode, which will automatically re-run tests when you make changes to your components or stories. To activate, press the watch mode toggle (the eye icon) in the testing widget. -