From bc0f7cf0aee8fa53b6f7a7632acf181827e89902 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:12:26 +1000 Subject: [PATCH] test: migrate schedule page tests to Storybook play stories (#26848) Migrates the WorkspaceSchedulePage tests from vitest to Storybook play-function stories. The old `WorkspaceSchedulePage.test.tsx` rendered the full settings layout through `renderWithWorkspaceSettingsLayout` and MSW, which is slow and contributes to `test-js` timeout flakes. The new stories seed the react-query cache directly and assert the same behavior in `play` functions, so they run as Storybook interaction tests instead of in the vitest `unit` project. Coverage is preserved across four flows: enabling autostop seeds the template's default TTL, changing autostop on a running workspace shows the restart dialog after a successful save, a stopped workspace skips that dialog, and changing only autostart skips it as well. The pure-logic schedule and TTL conversion tests stay in `WorkspaceSchedulePage.test.tsx`. Relates to CODAGT-686 --- .../WorkspaceSchedulePage.stories.tsx | 98 ++++++++++++++- .../WorkspaceSchedulePage.test.tsx | 118 ------------------ 2 files changed, 97 insertions(+), 119 deletions(-) diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx index a87935731e..d6a2fbcf82 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx @@ -1,8 +1,10 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, spyOn, userEvent, within } from "storybook/test"; import { reactRouterOutlet, reactRouterParameters, } from "storybook-addon-remix-react-router"; +import { API } from "#/api/api"; import { templateByNameKey } from "#/api/queries/templates"; import { workspaceByOwnerAndNameKey } from "#/api/queries/workspaces"; import type { Workspace } from "#/api/typesGenerated"; @@ -12,10 +14,12 @@ import { MockTemplate, MockUserOwner, MockWorkspace, + MockWorkspaceBuild, } from "#/testHelpers/entities"; import { withAuthProvider, withDashboardProvider, + withToaster, } from "#/testHelpers/storybook"; import { WorkspaceSettingsLayout } from "../WorkspaceSettingsLayout"; import WorkspaceSchedulePage from "./WorkspaceSchedulePage"; @@ -23,11 +27,15 @@ import WorkspaceSchedulePage from "./WorkspaceSchedulePage"; const meta = { title: "pages/WorkspaceSchedulePage", component: WorkspaceSettingsLayout, - decorators: [withAuthProvider, withDashboardProvider], + decorators: [withToaster, withAuthProvider, withDashboardProvider], parameters: { layout: "fullscreen", user: MockUserOwner, }, + beforeEach: () => { + spyOn(API, "putWorkspaceAutostart").mockResolvedValue(); + spyOn(API, "putWorkspaceAutostop").mockResolvedValue(); + }, } satisfies Meta; export default meta; @@ -47,6 +55,94 @@ export const PrebuiltWorkspace: Story = { }, }; +const autostopDisabledWorkspace: Workspace = { ...MockWorkspace, ttl_ms: 0 }; + +export const EnablingAutostopUsesTemplateDefault: Story = { + parameters: { + reactRouter: workspaceRouterParameters(autostopDisabledWorkspace), + queries: workspaceQueries(autostopDisabledWorkspace), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const user = userEvent.setup(); + const autostopToggle = await canvas.findByLabelText("Enable Autostop"); + await user.click(autostopToggle); + await canvas.findByText("Your workspace will shut down 1 day after", { + exact: false, + }); + }, +}; + +export const ChangingAutostopShowsRestartDialog: Story = { + parameters: { + reactRouter: workspaceRouterParameters(MockWorkspace), + queries: workspaceQueries(MockWorkspace), + }, + beforeEach: () => { + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(MockWorkspace); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const body = within(document.body); + const user = userEvent.setup(); + await user.click(await canvas.findByLabelText("Enable Autostop")); + await user.click(await canvas.findByRole("button", { name: /save/i })); + await body.findByText( + `Schedule for workspace "${MockWorkspace.name}" updated successfully.`, + ); + await body.findByText("Restart workspace?"); + }, +}; + +const stoppedWorkspace: Workspace = { + ...MockWorkspace, + latest_build: { ...MockWorkspaceBuild, status: "stopped" }, +}; + +export const ChangingAutostopWhileStoppedSkipsDialog: Story = { + parameters: { + reactRouter: workspaceRouterParameters(stoppedWorkspace), + queries: workspaceQueries(stoppedWorkspace), + }, + beforeEach: () => { + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( + stoppedWorkspace, + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const body = within(document.body); + const user = userEvent.setup(); + await user.click(await canvas.findByLabelText("Enable Autostop")); + await user.click(await canvas.findByRole("button", { name: /save/i })); + await body.findByText( + `Schedule for workspace "${MockWorkspace.name}" updated successfully.`, + ); + expect(body.queryByText("Restart workspace?")).not.toBeInTheDocument(); + }, +}; + +export const ChangingOnlyAutostartSkipsDialog: Story = { + parameters: { + reactRouter: workspaceRouterParameters(MockWorkspace), + queries: workspaceQueries(MockWorkspace), + }, + beforeEach: () => { + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(MockWorkspace); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const body = within(document.body); + const user = userEvent.setup(); + await user.click(await canvas.findByLabelText("Enable Autostart")); + await user.click(await canvas.findByRole("button", { name: /save/i })); + await body.findByText( + `Schedule for workspace "${MockWorkspace.name}" updated successfully.`, + ); + expect(body.queryByText("Restart workspace?")).not.toBeInTheDocument(); + }, +}; + function workspaceRouterParameters(workspace: Workspace) { return reactRouterParameters({ location: { diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx index 9b20d3e0ed..3d604b4f50 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx @@ -1,13 +1,3 @@ -import { screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { HttpResponse, http } from "msw"; -import { - MockUserOwner, - MockWorkspace, - MockWorkspaceBuild, -} from "#/testHelpers/entities"; -import { renderWithWorkspaceSettingsLayout } from "#/testHelpers/renderHelpers"; -import { server } from "#/testHelpers/server"; import { formValuesToAutostartRequest, formValuesToTTLRequest, @@ -15,7 +5,6 @@ import { import { scheduleToAutostart } from "./schedule"; import { ttlMsToAutostop } from "./ttl"; import type { WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm"; -import WorkspaceSchedulePage from "./WorkspaceSchedulePage"; const validValues: WorkspaceScheduleFormValues = { autostartEnabled: true, @@ -246,111 +235,4 @@ describe("WorkspaceSchedulePage", () => { expect(ttlMsToAutostop(ttlMs)).toEqual(autostop); }); }); - - describe("autostop", () => { - it("uses template default ttl when first enabled", async () => { - // have autostop disabled - server.use( - http.get("/api/v2/users/:userId/workspace/:workspaceName", () => { - return HttpResponse.json({ ...MockWorkspace, ttl_ms: 0 }); - }), - ); - renderWithWorkspaceSettingsLayout(, { - route: `/@${MockUserOwner.username}/${MockWorkspace.name}/schedule`, - path: "/:username/:workspace/schedule", - }); - const user = userEvent.setup(); - const autostopToggle = await screen.findByLabelText("Enable Autostop"); - // enable autostop - await user.click(autostopToggle); - // find helper text that describes the mock template's 24 hour default - const autostopHelperText = await screen.findByText( - "Your workspace will shut down 1 day after", - { exact: false }, - ); - expect(autostopHelperText).toBeDefined(); - }); - }); - - describe("autostop change dialog", () => { - it("shows if autostop is changed", async () => { - renderWithWorkspaceSettingsLayout(, { - route: `/@${MockUserOwner.username}/${MockWorkspace.name}/schedule`, - path: "/:username/:workspace/schedule", - }); - const user = userEvent.setup(); - const autostopToggle = await screen.findByLabelText("Enable Autostop"); - await user.click(autostopToggle); - const submitButton = await screen.findByRole("button", { - name: /save/i, - }); - await user.click(submitButton); - - const notification = await screen.findByText( - `Schedule for workspace "test-workspace" updated successfully.`, - ); - expect(notification).toBeInTheDocument(); - - const dialog = await screen.findByText("Restart workspace?"); - expect(dialog).toBeInTheDocument(); - }); - - it("doesn't show if workspace is stopped", async () => { - server.use( - http.get("/api/v2/users/:userId/workspace/:workspaceName", () => { - return HttpResponse.json({ - ...MockWorkspace, - latest_build: { ...MockWorkspaceBuild, status: "stopped" }, - }); - }), - ); - renderWithWorkspaceSettingsLayout(, { - route: `/@${MockUserOwner.username}/${MockWorkspace.name}/schedule`, - path: "/:username/:workspace/schedule", - extraRoutes: [ - { path: "/:username/:workspace", element:
Workspace
}, - ], - }); - const user = userEvent.setup(); - const autostopToggle = await screen.findByLabelText("Enable Autostop"); - await user.click(autostopToggle); - const submitButton = await screen.findByRole("button", { - name: /save/i, - }); - await user.click(submitButton); - - const notification = await screen.findByText( - `Schedule for workspace "test-workspace" updated successfully.`, - ); - expect(notification).toBeInTheDocument(); - - const dialog = screen.queryByText("Restart workspace?"); - expect(dialog).not.toBeInTheDocument(); - }); - - it("doesn't show if autostop is not changed", async () => { - renderWithWorkspaceSettingsLayout(, { - route: `/@${MockUserOwner.username}/${MockWorkspace.name}/schedule`, - path: "/:username/:workspace/schedule", - extraRoutes: [ - { path: "/:username/:workspace", element:
Workspace
}, - ], - }); - const user = userEvent.setup(); - const autostartToggle = await screen.findByLabelText("Enable Autostart"); - await user.click(autostartToggle); - const submitButton = await screen.findByRole("button", { - name: /save/i, - }); - await user.click(submitButton); - - const notification = await screen.findByText( - `Schedule for workspace "test-workspace" updated successfully.`, - ); - expect(notification).toBeInTheDocument(); - - const dialog = screen.queryByText("Restart workspace?"); - expect(dialog).not.toBeInTheDocument(); - }); - }); });