From 05baba1e63534fd0b4255e2a54885d971683cb68 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Wed, 5 Aug 2026 20:06:34 +1000 Subject: [PATCH] test(site): fix failing Storybook play functions (#27874) Fixes several Storybook play function failures that show up in Pixel (and some that fail under `vitest --project=storybook`). ### Component / behavior - **DurationField**: restore digit-only filtering so non-numeric input is stripped again. A demui pass had stopped filtering and relied on `pattern`, which does not prevent typing. ### Pixel viewport mismatches Pixel ignores Storybook viewport params. Shared helpers `pixelWithDesktop` / `pixelWithPhone` live in `testHelpers/pixel.ts`. - **NavbarView**: admin dropdown stories are desktop-only. Pixel tablet is 744px, below Tailwind `md` (768px), so Admin settings is hidden and the play click fails. `MobileMenu` already covers the collapsed navbar. - **AgentPageHeader**: mobile meatball menu story is phone-only. At laptop width `sm:hidden` keeps "More options" out of the a11y tree even when `matchMedia` is mocked as mobile. ### Story assertion fixes - **WorkspaceSettingsPageView**: Formik passes helpers as a second `onSubmit` argument; the assertion now allows that. - **AgentChatPageView**: sidebar tab persistence stories use Git instead of Terminal. Clicking Terminal mounts xterm, which throws an unhandled `dimensions` error and fails the run even when tab assertions pass. - **NetworkCallsTable / Blocked Badge**: badge copy is split across an `sr-only` span and the count; assertion matches combined `textContent`. - **CreateOAuth2AppPageView / Default**: wait for Formik validate-on-mount before asserting the submit button is disabled. - **NotificationEvents / Change Method** (+ Error): combobox accessible name is now `Notification method for ${template}`; match with a regex. - **DynamicClientRegistrationSetting / Keeps Focus While Updating**: wait for the Enable label after the harness finishes the request. Pixel logs play failures but still exits 0, so these can stay green in CI while failing in the Pixel report. --------- Co-authored-by: Danielle Maywood --- .../DurationField/DurationField.tsx | 8 ++---- .../dashboard/Navbar/NavbarView.stories.tsx | 6 +++- .../NetworkCallsTable.stories.tsx | 4 ++- .../AgentsPage/AgentChatPageView.stories.tsx | 28 +++++++++---------- .../components/AgentPageHeader.stories.tsx | 3 ++ .../NotificationEvents.stories.tsx | 4 +-- .../CreateOAuth2AppPageView.stories.tsx | 12 +++++--- ...namicClientRegistrationSetting.stories.tsx | 4 ++- .../WorkspaceSettingsPageView.stories.tsx | 1 + site/src/testHelpers/pixel.ts | 8 ++++++ 10 files changed, 50 insertions(+), 28 deletions(-) diff --git a/site/src/components/DurationField/DurationField.tsx b/site/src/components/DurationField/DurationField.tsx index bd2a1fd22b..728039931d 100644 --- a/site/src/components/DurationField/DurationField.tsx +++ b/site/src/components/DurationField/DurationField.tsx @@ -86,12 +86,10 @@ export const DurationField: FC = ({ } const handleTextChange = (raw: string) => { - // Preserve exactly what the user typed rather than silently stripping - // non-digits (which surprisingly turned "ABC123DEF" into "123"). The - // pattern constraint on the Input surfaces invalid entries instead. - setText(raw); + const digits = raw.replace(/\D/g, ""); + setText(digits); - const ms = toMs(raw, unit); + const ms = toMs(digits, unit); if (ms !== valueMs) { onChange(ms); } diff --git a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx index 80c0986f30..dd88e6cac5 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx @@ -7,7 +7,7 @@ import { MockUserMember, MockUserOwner, } from "#/testHelpers/entities"; -import { pixelWithTablet } from "#/testHelpers/pixel"; +import { pixelWithDesktop, pixelWithTablet } from "#/testHelpers/pixel"; import { withDashboardProvider } from "#/testHelpers/storybook"; import { NavbarView } from "./NavbarView"; @@ -49,6 +49,7 @@ export default meta; type Story = StoryObj; export const ForAdmin: Story = { + parameters: { pixel: { matrix: pixelWithDesktop } }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click( @@ -58,6 +59,7 @@ export const ForAdmin: Story = { }; export const ForAuditor: Story = { + parameters: { pixel: { matrix: pixelWithDesktop } }, args: { user: MockUserMember, adminPermissions: { @@ -73,6 +75,7 @@ export const ForAuditor: Story = { }; export const ForOrgAdmin: Story = { + parameters: { pixel: { matrix: pixelWithDesktop } }, args: { user: MockUserMember, adminPermissions: { @@ -89,6 +92,7 @@ export const ForOrgAdmin: Story = { }; export const ForSingleOrgOSSAdmin: Story = { + parameters: { pixel: { matrix: pixelWithDesktop } }, args: { adminPermissions: { canViewDeployment: true, diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/NetworkCallsTable.stories.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/NetworkCallsTable.stories.tsx index de612d7036..c2ef4b23f8 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/NetworkCallsTable.stories.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/NetworkCallsTable.stories.tsx @@ -34,7 +34,9 @@ export const BlockedBadge: Story = { }, play: async ({ canvas }) => { await expect( - canvas.getByText("Blocked network calls: 9"), + canvas.getByText((_content, element) => { + return element?.textContent === "Blocked network calls: 9"; + }), ).toBeInTheDocument(); await expect(canvas.getAllByText("Blocked")).toHaveLength(2); }, diff --git a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx index 3381ad9012..537e6580a2 100644 --- a/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatPageView.stories.tsx @@ -1579,12 +1579,12 @@ const sidebarTabStorageKey = `${lastActiveSidebarTabStorageKeyPrefix}${AGENT_ID} /** * When localStorage contains a persisted tab ID for this chat, the sidebar - * should restore it on mount. Seed localStorage with "terminal" and verify - * that the Terminal tab is selected instead of the default Git tab. + * should restore it on mount. Seed localStorage with "git" and verify that + * the Git tab is selected instead of the default Summary tab. */ export const RestoresPersistedSidebarTab: Story = { beforeEach: () => { - localStorage.setItem(sidebarTabStorageKey, "terminal"); + localStorage.setItem(sidebarTabStorageKey, "git"); return () => { localStorage.removeItem(sidebarTabStorageKey); }; @@ -1601,12 +1601,12 @@ export const RestoresPersistedSidebarTab: Story = { const canvas = within(canvasElement); await waitFor(() => { - const terminalTab = canvas.getByRole("tab", { name: "Terminal" }); - expect(terminalTab).toHaveAttribute("aria-selected", "true"); + const gitTab = canvas.getByRole("tab", { name: "Git" }); + expect(gitTab).toHaveAttribute("aria-selected", "true"); }); - const gitTab = canvas.getByRole("tab", { name: "Git" }); - expect(gitTab).toHaveAttribute("aria-selected", "false"); + const summaryTab = canvas.getByRole("tab", { name: "Summary" }); + expect(summaryTab).toHaveAttribute("aria-selected", "false"); }, }; @@ -1637,14 +1637,14 @@ export const PersistsSidebarTabClick: Story = { expect(summaryTab).toHaveAttribute("aria-selected", "true"); }); - const terminalTab = canvas.getByRole("tab", { name: "Terminal" }); - await userEvent.click(terminalTab); + const gitTab = canvas.getByRole("tab", { name: "Git" }); + await userEvent.click(gitTab); await waitFor(() => { - expect(terminalTab).toHaveAttribute("aria-selected", "true"); + expect(gitTab).toHaveAttribute("aria-selected", "true"); }); - expect(localStorage.getItem(sidebarTabStorageKey)).toBe("terminal"); + expect(localStorage.getItem(sidebarTabStorageKey)).toBe("git"); }, }; @@ -1711,11 +1711,11 @@ export const DoesNotPersistForArchivedChat: Story = { expect(summaryTab).toHaveAttribute("aria-selected", "true"); }); - const terminalTab = canvas.getByRole("tab", { name: "Terminal" }); - await userEvent.click(terminalTab); + const gitTab = canvas.getByRole("tab", { name: "Git" }); + await userEvent.click(gitTab); await waitFor(() => { - expect(terminalTab).toHaveAttribute("aria-selected", "true"); + expect(gitTab).toHaveAttribute("aria-selected", "true"); }); expect(localStorage.getItem(sidebarTabStorageKey)).toBeNull(); diff --git a/site/src/pages/AgentsPage/components/AgentPageHeader.stories.tsx b/site/src/pages/AgentsPage/components/AgentPageHeader.stories.tsx index bb83b0d5a0..dd1dbbff5c 100644 --- a/site/src/pages/AgentsPage/components/AgentPageHeader.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentPageHeader.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { type FC, useMemo, useState } from "react"; import { Outlet } from "react-router"; import { expect, userEvent, waitFor, within } from "storybook/test"; +import { pixelWithPhone } from "#/testHelpers/pixel"; import { withDashboardProvider } from "#/testHelpers/storybook"; import { AgentPageHeader } from "./AgentPageHeader"; import { ChimeButton } from "./ChimeButton"; @@ -184,7 +185,9 @@ export const MobileActionsExcludeAnalytics: Story = { }, render: () => , parameters: { + layout: "fullscreen", viewport: { defaultViewport: "mobile1" }, + pixel: { matrix: pixelWithPhone }, reactRouter: { location: { path: "/agents", diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx index 202fccf483..131e29f122 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx @@ -67,7 +67,7 @@ export const ChangeMethod: Story = { throw new Error("Could not find notification template row"); } await user.click( - within(row).getByRole("combobox", { name: "Notification method" }), + within(row).getByRole("combobox", { name: /Notification method/ }), ); await user.click( await within(document.body).findByRole("option", { name: "Webhook" }), @@ -88,7 +88,7 @@ export const ChangeMethodError: Story = { throw new Error("Could not find notification template row"); } await user.click( - within(row).getByRole("combobox", { name: "Notification method" }), + within(row).getByRole("combobox", { name: /Notification method/ }), ); await user.click( await within(document.body).findByRole("option", { name: "Webhook" }), diff --git a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/CreateOAuth2AppPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/CreateOAuth2AppPageView.stories.tsx index 8197b2d1ea..72744a8ae4 100644 --- a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/CreateOAuth2AppPageView.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/CreateOAuth2AppPageView.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { expect, spyOn, userEvent, within } from "storybook/test"; +import { expect, spyOn, userEvent, waitFor, within } from "storybook/test"; import { reactRouterParameters } from "storybook-addon-remix-react-router"; import { API } from "#/api/api"; import { @@ -41,9 +41,13 @@ export const Default: Story = { name: /add an oauth2 application/i, }), ).toBeVisible(); - await expect( - canvas.getByRole("button", { name: /create application/i }), - ).toBeDisabled(); + // The submit button renders enabled for a frame until the form's + // validate-on-mount pass reports the empty required fields. + await waitFor(() => + expect( + canvas.getByRole("button", { name: /create application/i }), + ).toBeDisabled(), + ); }, }; diff --git a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/DynamicClientRegistrationSetting.stories.tsx b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/DynamicClientRegistrationSetting.stories.tsx index 8a42803df4..50c9772b3c 100644 --- a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/DynamicClientRegistrationSetting.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/DynamicClientRegistrationSetting.stories.tsx @@ -254,7 +254,9 @@ export const KeepsFocusWhileUpdating: Story = { // The same element becomes the opposite action once the request lands, and // focus rides along rather than resetting to the top of the document. - await expect(canvas.getByText("Enable")).toBeVisible(); + await waitFor(() => { + expect(canvas.getByRole("button", { name: "Enable" })).toBeVisible(); + }); await expect(button).toHaveFocus(); }, }; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx index 7c2550c6e3..1bbcd7889c 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx @@ -46,6 +46,7 @@ export const UpdateAutomaticUpdatesPolicy: Story = { await waitFor(() => expect(args.onSubmit).toHaveBeenCalledWith( expect.objectContaining({ automatic_updates: "always" }), + expect.anything(), ), ); }, diff --git a/site/src/testHelpers/pixel.ts b/site/src/testHelpers/pixel.ts index cc3d1611c1..61194a6081 100644 --- a/site/src/testHelpers/pixel.ts +++ b/site/src/testHelpers/pixel.ts @@ -5,3 +5,11 @@ export const pixelWithTablet = { viewports: ["tablet", "desktop"], }; + +export const pixelWithDesktop = { + viewports: ["desktop"], +}; + +export const pixelWithPhone = { + viewports: ["phone"], +};