diff --git a/site/.storybook/vitest.setup.ts b/site/.storybook/vitest.setup.ts index f11a4e41f5..358c4b4ac6 100644 --- a/site/.storybook/vitest.setup.ts +++ b/site/.storybook/vitest.setup.ts @@ -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
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,
diff --git a/site/src/components/Paywall/PaywallPremium.stories.tsx b/site/src/components/Paywall/PaywallPremium.stories.tsx
index bb8dd000f9..a72e4fbbce 100644
--- a/site/src/components/Paywall/PaywallPremium.stories.tsx
+++ b/site/src/components/Paywall/PaywallPremium.stories.tsx
@@ -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(
diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx
index 4e13c72394..c584767199 100644
--- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx
+++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx
@@ -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();
},
};
diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx
index 2642433c0d..49aaea410e 100644
--- a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx
+++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx
@@ -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",
diff --git a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
index 983ab28cda..ae2f1b829b 100644
--- a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
+++ b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
@@ -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(() => {
diff --git a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx
index e9ec91cccc..c917ccd729 100644
--- a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx
+++ b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx
@@ -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();
},
};
diff --git a/site/src/pages/UserSettingsPage/SecretsPage/SecretsPage.tsx b/site/src/pages/UserSettingsPage/SecretsPage/SecretsPage.tsx
index 3f72521c33..b39abb6910 100644
--- a/site/src/pages/UserSettingsPage/SecretsPage/SecretsPage.tsx
+++ b/site/src/pages/UserSettingsPage/SecretsPage/SecretsPage.tsx
@@ -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.`);
diff --git a/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.stories.tsx b/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.stories.tsx
index 2668ca7e62..87e616027d 100644
--- a/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.stories.tsx
+++ b/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.stories.tsx
@@ -28,11 +28,9 @@ const meta: Meta = {
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();
},
};
diff --git a/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.tsx b/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.tsx
index 93d314699c..cfac6530ab 100644
--- a/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.tsx
+++ b/site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.tsx
@@ -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;
diff --git a/site/src/pages/UserSettingsPage/SecretsPage/SecretsTable.tsx b/site/src/pages/UserSettingsPage/SecretsPage/SecretsTable.tsx
index 2fb48ca214..4e17976f5b 100644
--- a/site/src/pages/UserSettingsPage/SecretsPage/SecretsTable.tsx
+++ b/site/src/pages/UserSettingsPage/SecretsPage/SecretsTable.tsx
@@ -217,7 +217,6 @@ const EnabledToggle: FC = ({
// 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 (
@@ -230,7 +229,7 @@ const EnabledToggle: FC = ({
*/}
onToggle(secret, checked)}