mirror of
https://github.com/coder/coder.git
synced 2026-09-22 13:10:21 +08:00
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 <danielle@themaywoods.com>
This commit is contained in:
co-authored by
Danielle Maywood
parent
7a4ae2649e
commit
05baba1e63
@@ -86,12 +86,10 @@ export const DurationField: FC<DurationFieldProps> = ({
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<typeof NavbarView>;
|
||||
|
||||
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,
|
||||
|
||||
+3
-1
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: () => <HeaderStateHarness />,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
viewport: { defaultViewport: "mobile1" },
|
||||
pixel: { matrix: pixelWithPhone },
|
||||
reactRouter: {
|
||||
location: {
|
||||
path: "/agents",
|
||||
|
||||
+2
-2
@@ -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" }),
|
||||
|
||||
+8
-4
@@ -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(),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+3
-1
@@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,6 +46,7 @@ export const UpdateAutomaticUpdatesPolicy: Story = {
|
||||
await waitFor(() =>
|
||||
expect(args.onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ automatic_updates: "always" }),
|
||||
expect.anything(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -5,3 +5,11 @@
|
||||
export const pixelWithTablet = {
|
||||
viewports: ["tablet", "desktop"],
|
||||
};
|
||||
|
||||
export const pixelWithDesktop = {
|
||||
viewports: ["desktop"],
|
||||
};
|
||||
|
||||
export const pixelWithPhone = {
|
||||
viewports: ["phone"],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user