diff --git a/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx b/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx index ca8192dd894e3..e2976652ea872 100644 --- a/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx +++ b/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx @@ -15,6 +15,7 @@ import { page, userEvent } from "vite-plus/test/browser"; import "~/index.css"; import { act, render } from "../../../test/render"; import { ScheduledTasksSettings } from "./ScheduledTasksSettings"; +import { ConfirmDialogHost } from "../ConfirmDialogHost"; const environmentId = EnvironmentId.make("laptop"); const projectId = ProjectId.make("project"); @@ -47,13 +48,19 @@ const threadFamilyRead = { threads: [{ id: task.threadId, title: "Fix login bug" }], }; -const state = vi.hoisted(() => ({ connected: true, tasksLoaded: true, secondary: false })); +const state = vi.hoisted(() => ({ + connected: true, + tasksLoaded: true, + secondary: false, + remove: vi.fn(), +})); const secondaryId = EnvironmentId.make("secondary"); const permissions = new Map>(); function setup() { state.connected = true; state.tasksLoaded = true; + state.remove.mockReset().mockResolvedValue({ _tag: "Success", value: { id: task.id } }); state.secondary = false; permissions.clear(); permissions.set(environmentId, Atom.make(true)); @@ -65,6 +72,11 @@ function secondaryEnvironment() { return { ...environment(), environmentId: secondaryId, label: "Secondary" }; } +vi.mock(import("../../state/session"), async (original) => ({ + ...(await original()), + readEnvironmentScope: () => true, +})); + function environment() { return { environmentId, @@ -104,6 +116,10 @@ vi.mock(import("../../state/server"), async (original) => { ...actual, serverEnvironment: { ...actual.serverEnvironment, + deleteScheduledTask: { + ...actual.serverEnvironment.deleteScheduledTask, + run: state.remove, + }, upsertScheduledTask: { ...actual.serverEnvironment.upsertScheduledTask, permissionAtom: (id: string | null) => permissions.get(id ?? "")!, @@ -146,6 +162,34 @@ vi.mock(import("../../hooks/useSettings"), async (original) => ({ })); describe("ScheduledTasksSettings", () => { + it("only deletes a scheduled task after confirmation, including after cancelling", async () => { + setup(); + await using screen = await render( + <> + + + , + ); + await userEvent.click(screen.getByRole("button", { name: "Actions for Review issues" })); + await userEvent.click(page.getByRole("menuitem", { name: "Delete", exact: true })); + await expect.element(page.getByRole("alertdialog")).toBeVisible(); + expect(state.remove).not.toHaveBeenCalled(); + await userEvent.click(page.getByRole("button", { name: "Cancel", exact: true })); + expect(state.remove).not.toHaveBeenCalled(); + await expect + .element(screen.getByRole("button", { name: "Actions for Review issues" })) + .toBeVisible(); + await userEvent.click(screen.getByRole("button", { name: "Actions for Review issues" })); + await userEvent.click(page.getByRole("menuitem", { name: "Delete", exact: true })); + await userEvent.click(page.getByRole("button", { name: "Confirm", exact: true })); + await vi.waitFor(() => + expect(state.remove).toHaveBeenCalledExactlyOnceWith(expect.anything(), { + environmentId, + input: { id: task.id }, + }), + ); + }); + it("creates on a permitted secondary and reacts to permission changes", async () => { setup(); state.secondary = true; diff --git a/apps/web/src/components/settings/ScheduledTasksSettings.tsx b/apps/web/src/components/settings/ScheduledTasksSettings.tsx index fd4f7702d68ab..b5b01ddd58b51 100644 --- a/apps/web/src/components/settings/ScheduledTasksSettings.tsx +++ b/apps/web/src/components/settings/ScheduledTasksSettings.tsx @@ -753,6 +753,13 @@ function ScheduledTaskRow({ }); const act = async (action: "toggle" | "run" | "delete") => { if (busy || !readEnvironmentScope(environmentId, AuthOrchestrationOperateScope)) return; + if (action === "delete") { + const message = `Delete "${task.title}"?\nThis cannot be undone.`; + const confirmed = + (await requestConfirmDialog(message, { variant: "destructive" })) ?? + window.confirm(message); + if (!confirmed) return; + } setBusy(true); const result = action === "toggle"