mirror of
https://github.com/coder/coder.git
synced 2026-09-21 12:44:32 +08:00
fix(site): repair the locally-run storybook vitest suite (#28261)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { preloadHighlighter } from "@pierre/diffs";
|
||||
import { setProjectAnnotations } from "@storybook/react-vite";
|
||||
import { beforeAll, beforeEach } from "vitest";
|
||||
import * as previewAnnotations from "./preview";
|
||||
@@ -6,6 +7,19 @@ const annotations = setProjectAnnotations([previewAnnotations]);
|
||||
|
||||
beforeAll(annotations.beforeAll);
|
||||
|
||||
// Stories render diff viewers without the app's worker pool, so @pierre/diffs
|
||||
// falls back to the shared in-page highlighter. Under StrictMode's dev-only
|
||||
// remount, a cold highlighter loses the first render: the remounted instance
|
||||
// re-hydrates the empty <pre> left behind and never schedules another render,
|
||||
// leaving the diff blank. Warming the themes here makes the first render
|
||||
// synchronous so re-hydration always finds content.
|
||||
beforeAll(async () => {
|
||||
await preloadHighlighter({
|
||||
themes: ["github-dark-high-contrast", "github-light"],
|
||||
langs: [],
|
||||
});
|
||||
});
|
||||
|
||||
// Radix DismissableLayer sets document.body.style.pointerEvents = "none" while
|
||||
// a modal layer is active. When a story unmounts, the useEffect cleanup that
|
||||
// restores body.pointerEvents can race with the next story's play function,
|
||||
|
||||
@@ -38,7 +38,9 @@ export const CanViewLicenses: Story = {
|
||||
await expect(pricing).toHaveAttribute("target", "_blank");
|
||||
|
||||
await expect(
|
||||
canvas.getByRole("heading", { name: /Workspace proxies provide/ }),
|
||||
canvas.getByRole("heading", {
|
||||
name: /You need a Premium license/,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(canvas.getAllByRole("listitem")).toHaveLength(4);
|
||||
await expect(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
|
||||
import {
|
||||
MockFailedWorkspace,
|
||||
MockTaskWorkspace,
|
||||
@@ -97,9 +97,17 @@ export const FilledWrong: Story = {
|
||||
|
||||
await userEvent.type(confirm, "wrong-name");
|
||||
await userEvent.tab();
|
||||
await expect(
|
||||
body.getByText("wrong-name does not match the name of this workspace"),
|
||||
).toBeVisible();
|
||||
// The validation error renders asynchronously after blur, so wait for
|
||||
// visibility instead of asserting it once.
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(
|
||||
body.getByText(
|
||||
"wrong-name does not match the name of this workspace",
|
||||
),
|
||||
).toBeVisible(),
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
await expect(body.getByRole("button", { name: "Delete" })).toBeDisabled();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -225,7 +225,7 @@ const selectModelInSection = async (
|
||||
section: HTMLElement,
|
||||
canvasElement: HTMLElement,
|
||||
currentSelectionName: string | RegExp,
|
||||
optionName: string,
|
||||
optionName: string | RegExp,
|
||||
) => {
|
||||
const trigger = within(section).getByRole("combobox", {
|
||||
name: currentSelectionName,
|
||||
@@ -389,7 +389,7 @@ export const EachOverrideSetToEnabledModel: Story = {
|
||||
generalSection,
|
||||
canvasElement,
|
||||
/gpt 4\.1 mini/i,
|
||||
"Claude Sonnet 4",
|
||||
/^Claude Sonnet 4\b/,
|
||||
);
|
||||
const generalSaveButton = within(generalSection).getByRole("button", {
|
||||
name: "Save",
|
||||
@@ -409,7 +409,7 @@ export const EachOverrideSetToEnabledModel: Story = {
|
||||
titleSection,
|
||||
canvasElement,
|
||||
/gpt 4o mini/i,
|
||||
"Claude Sonnet 4",
|
||||
/^Claude Sonnet 4\b/,
|
||||
);
|
||||
const titleSaveButton = within(titleSection).getByRole("button", {
|
||||
name: "Save",
|
||||
@@ -429,7 +429,7 @@ export const EachOverrideSetToEnabledModel: Story = {
|
||||
compactionSection,
|
||||
canvasElement,
|
||||
/claude sonnet 4/i,
|
||||
"GPT 4o Mini",
|
||||
/^GPT 4o Mini\b/,
|
||||
);
|
||||
const compactionSaveButton = within(compactionSection).getByRole("button", {
|
||||
name: "Save",
|
||||
|
||||
@@ -3117,10 +3117,14 @@ export const SlashCompactYieldsToPersonalSkill: Story = {
|
||||
await userEvent.click(editor);
|
||||
await userEvent.keyboard("/compact");
|
||||
// The menu offers only the personal skill (the built-in command
|
||||
// yields); first Enter accepts it, second Enter submits.
|
||||
expect(
|
||||
await within(document.body).findByText("Personal compact skill"),
|
||||
).toBeVisible();
|
||||
// yields); first Enter accepts it, second Enter submits. The menu item
|
||||
// exists before the popover finishes positioning, so wait for
|
||||
// visibility rather than asserting it once.
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(document.body).getByText("Personal compact skill"),
|
||||
).toBeVisible();
|
||||
});
|
||||
await userEvent.keyboard("{Enter}");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
|
||||
@@ -3309,7 +3313,13 @@ export const SendResponseAfterChatSwitch: Story = {
|
||||
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Switch chat" }));
|
||||
const timeline = within(await canvas.findByTestId("conversation-timeline"));
|
||||
expect(await timeline.findByText("Current chat message")).toBeVisible();
|
||||
// The switched chat's messages render slowly under pixel's parallel
|
||||
// load, so extend the default 1s lookup timeout.
|
||||
expect(
|
||||
await timeline.findByText("Current chat message", undefined, {
|
||||
timeout: 10_000,
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
releaseSend?.();
|
||||
await waitFor(() => {
|
||||
|
||||
+3
-3
@@ -45,7 +45,7 @@ export const Entitled: Story = {
|
||||
canvas.getByRole("heading", { name: "Announcement Banners" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
canvas.queryByRole("link", { name: "Learn about Premium" }),
|
||||
canvas.queryByRole("link", { name: "Start trial for free" }),
|
||||
).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
@@ -54,7 +54,7 @@ export const NotEntitled: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const cta = canvas.getByRole("link", { name: "Learn about Premium" });
|
||||
const cta = canvas.getByRole("link", { name: "Start trial for free" });
|
||||
await expect(cta).toHaveAttribute("href", "/deployment/premium");
|
||||
await expect(
|
||||
canvas.queryByRole("form", { name: "Appearance settings" }),
|
||||
@@ -76,7 +76,7 @@ export const NotEntitledWithoutLicenseAccess: Story = {
|
||||
canvas.getByText(/contact your deployment administrator/i),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
canvas.queryByRole("link", { name: "Learn about Premium" }),
|
||||
canvas.queryByRole("link", { name: "Start trial for free" }),
|
||||
).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,16 +36,10 @@ const SecretsPage: FC = () => {
|
||||
secrets={secretsQuery.data}
|
||||
isLoading={!secretsQuery.isFetched && secretsQuery.isFetching}
|
||||
hasLoaded={secretsQuery.isSuccess}
|
||||
isRefreshing={secretsQuery.isFetching && secretsQuery.isFetched}
|
||||
isCreating={createSecretMutation.isPending}
|
||||
isUpdating={updateSecretMutation.isPending}
|
||||
isDeleting={deleteSecretMutation.isPending}
|
||||
getSecretsError={secretsQuery.error}
|
||||
onRefresh={() => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: secretsQueryOptions.queryKey,
|
||||
});
|
||||
}}
|
||||
onCreateSecret={async (request) => {
|
||||
const secret = await createSecretMutation.mutateAsync(request);
|
||||
toast.success(`Created secret "${secret.name}" successfully.`);
|
||||
|
||||
@@ -28,11 +28,9 @@ const meta: Meta<typeof SecretsPageView> = {
|
||||
secrets: visibleSecrets,
|
||||
isLoading: false,
|
||||
hasLoaded: true,
|
||||
isRefreshing: false,
|
||||
isCreating: false,
|
||||
isUpdating: false,
|
||||
isDeleting: false,
|
||||
onRefresh: fn(),
|
||||
onCreateSecret: fn(),
|
||||
onUpdateSecret: fn(),
|
||||
onImportSecrets: fn(),
|
||||
@@ -111,7 +109,7 @@ export const Loaded: Story = {
|
||||
await expect(canvas.getByText("env var + file")).toBeInTheDocument();
|
||||
await expect(canvas.getByText("not injected")).toBeInTheDocument();
|
||||
|
||||
const docsLink = canvas.getByRole("link", { name: "View docs" });
|
||||
const docsLink = canvas.getByRole("link", { name: "Read the docs" });
|
||||
await expect(docsLink).toHaveAttribute(
|
||||
"href",
|
||||
expect.stringContaining("/user-guides/user-secrets"),
|
||||
@@ -135,25 +133,9 @@ export const Loading: Story = {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: /Refresh/ }),
|
||||
).toBeDisabled();
|
||||
},
|
||||
};
|
||||
|
||||
export const RefreshingWithRows: Story = {
|
||||
args: {
|
||||
secrets: visibleSecrets,
|
||||
isLoading: false,
|
||||
hasLoaded: true,
|
||||
isRefreshing: true,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(canvas.getAllByText(visibleSecrets[0].name)[0]).toBeVisible();
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: /Refresh/ }),
|
||||
).toBeDisabled();
|
||||
canvas.getByRole("status", { name: "Loading" }),
|
||||
).toBeInTheDocument();
|
||||
await expect(canvas.queryByText("No secrets yet")).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -21,12 +21,10 @@ type SecretsPageViewProps = {
|
||||
secrets?: readonly UserSecret[];
|
||||
isLoading: boolean;
|
||||
hasLoaded: boolean;
|
||||
isRefreshing: boolean;
|
||||
isCreating: boolean;
|
||||
isUpdating: boolean;
|
||||
isDeleting: boolean;
|
||||
getSecretsError?: unknown;
|
||||
onRefresh: () => void;
|
||||
onCreateSecret: (
|
||||
request: CreateUserSecretRequest,
|
||||
) => Promise<UserSecret> | UserSecret;
|
||||
|
||||
@@ -217,7 +217,6 @@ const EnabledToggle: FC<EnabledToggleProps> = ({
|
||||
// An enabled secret must have at least one injection target. Prevent
|
||||
// enabling a target-less secret; the user must add a target first.
|
||||
const cannotEnable = !secret.enabled && !hasTarget;
|
||||
const stateLabel = secret.enabled ? "Enabled" : "Disabled";
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
@@ -230,7 +229,7 @@ const EnabledToggle: FC<EnabledToggleProps> = ({
|
||||
*/}
|
||||
<span tabIndex={0} className="inline-flex">
|
||||
<Switch
|
||||
aria-label={stateLabel}
|
||||
aria-label={`Toggle secret ${secret.name}`}
|
||||
checked={secret.enabled}
|
||||
disabled={isPending || cannotEnable}
|
||||
onCheckedChange={(checked) => onToggle(secret, checked)}
|
||||
|
||||
Reference in New Issue
Block a user