Files
kandev/apps/web/e2e/tests/settings/workspace-delete.spec.ts
T
2026-09-04 12:44:45 +00:00

191 lines
8.5 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { OfficeApiClient } from "../../helpers/office-api-client";
import { KanbanPage } from "../../pages/kanban-page";
import { WorkflowSettingsPage } from "../../pages/workflow-settings-page";
test.describe("Workspace settings", () => {
test("creates a Kanban workflow with template steps through workspace settings", async ({
testPage,
apiClient,
prCapture,
}) => {
const workspaceName = `Settings Kanban ${Date.now().toString(36)}`;
const taskTitle = `Kanban Bootstrap Task ${Date.now().toString(36)}`;
await testPage.goto("/settings/workspaces");
await testPage.getByRole("button", { name: "Add Workspace" }).click();
await testPage.getByLabel("Workspace Name").fill(workspaceName);
await testPage.locator("form").getByRole("button", { name: "Add Workspace" }).click();
// Follow the newly rendered workspace link rather than seeding or creating
// it through the API: this covers the standard Settings creation flow. The
// card's link is an empty overlay named by `aria-label` — the workspace
// name is a sibling `h4`, not the link's child.
const workspaceLink = testPage
.getByTestId("settings-scroll-container")
.getByRole("link", { name: workspaceName, exact: true });
await expect(workspaceLink).toBeVisible();
const workspaceHref = await workspaceLink.getAttribute("href");
const workspaceId = new URL(workspaceHref ?? "", "http://kandev.test").pathname.split("/")[3];
expect(workspaceId).toBeTruthy();
let createdTaskId: string | undefined;
try {
// The card's section tiles sit above the overlay link (z-10) and cover
// its centre, so click near a corner — the card padding, where the
// overlay is what a user's pointer actually reaches.
await workspaceLink.click({ position: { x: 8, y: 8 } });
// The workspace name heads the page through the switcher trigger, not a
// heading: the shell doubles it as the workspace-to-workspace navigator.
await expect(testPage.getByTestId("workspace-settings-switcher")).toHaveText(workspaceName);
await testPage
.getByTestId("settings-scroll-container")
.getByRole("link", { name: "Workflows", exact: true })
.click();
const workflows = new WorkflowSettingsPage(testPage);
const kanbanCard = await workflows.findWorkflowCard("Kanban");
await expect(kanbanCard).toBeVisible();
for (const stepName of ["Backlog", "In Progress", "Review", "Done"]) {
await expect(workflows.stepNodeByName(kanbanCard, stepName)).toBeVisible();
}
await prCapture.screenshot("desktop-kanban-workspace-bootstrap", {
caption:
"A workspace created from Settings includes the Kanban workflow and its four ready-to-use steps.",
});
// Select the new workspace from the user-facing picker, then verify the
// bootstrap immediately makes its workflow usable by the standard New
// Task flow. Scratch mode avoids introducing a repository as test setup.
const kanban = new KanbanPage(testPage);
await kanban.goto();
await testPage.getByTestId("sidebar-workspace-trigger").click();
await testPage.getByTestId(`sidebar-workspace-item-${workspaceId}`).click();
await expect(testPage).toHaveURL(
(url) => url.pathname === "/" && url.searchParams.get("workspaceId") === workspaceId,
);
await expect(kanban.board).toBeVisible();
await kanban.createTaskButton.first().click();
const dialog = testPage.getByTestId("create-task-dialog");
await expect(dialog).toBeVisible();
await dialog.getByTestId("source-mode-scratch").click();
await dialog.getByTestId("task-title-input").fill(taskTitle);
await dialog.getByTestId("task-description-input").fill("Created without starting an agent");
await expect(dialog.getByTestId("submit-start-agent-chevron")).toBeEnabled();
const created = testPage.waitForResponse(
(response) =>
response.url().endsWith("/api/v1/tasks") && response.request().method() === "POST",
);
await dialog.getByTestId("submit-start-agent-chevron").click();
await testPage.getByTestId("submit-create-without-agent").click();
const createdTask = (await created).json() as Promise<{ id?: string }>;
createdTaskId = (await createdTask).id;
await expect(dialog).not.toBeVisible();
await expect(testPage).toHaveURL(/\/t\//);
await expect(testPage.getByTestId("task-topbar-title")).toHaveText(taskTitle);
} finally {
if (createdTaskId) await apiClient.deleteTask(createdTaskId);
if (workspaceId) await apiClient.deleteWorkspace(workspaceId, workspaceName);
}
});
test("deletes a workspace from the settings edit page", async ({ testPage, apiClient }) => {
const suffix = Date.now().toString(36);
const workspaceName = `Settings Delete ${suffix}`;
const workspace = await apiClient.createWorkspace(workspaceName);
await testPage.goto(`/settings/workspaces/${workspace.id}`);
await expect(testPage.getByTestId("workspace-settings-switcher")).toHaveText(workspaceName, {
timeout: 15_000,
});
await testPage.getByTestId("workspace-settings-delete-button").click();
const confirmInput = testPage.getByTestId("workspace-settings-delete-confirm-input");
const confirmButton = testPage.getByTestId("workspace-settings-delete-confirm-button");
await expect(confirmInput).toBeVisible();
// The wrong confirmation string ("delete") must not enable deletion — the
// backend requires confirm_name to equal the workspace name.
await confirmInput.fill("delete");
await expect(confirmButton).toBeDisabled();
await confirmInput.fill(workspaceName);
await expect(confirmButton).toBeEnabled();
// Cancel-then-reopen must reset the confirmation field; otherwise the
// re-type requirement would be silently bypassed on the second open.
await testPage.getByRole("button", { name: "Cancel" }).click();
await testPage.getByTestId("workspace-settings-delete-button").click();
await expect(confirmInput).toHaveValue("");
await expect(confirmButton).toBeDisabled();
await confirmInput.fill(workspaceName);
await expect(confirmButton).toBeEnabled();
// Deletion runs through an action wrapper, so assert the user-visible
// outcome: redirect to the workspace list and the workspace gone from the
// backend.
await confirmButton.click();
await expect(testPage).toHaveURL(/\/settings\/workspaces$/, { timeout: 10_000 });
const { workspaces } = await apiClient.listWorkspaces();
expect(workspaces.some((item) => item.id === workspace.id)).toBe(false);
});
test("deletes an office workspace from the settings edit page", async ({
testPage,
apiClient,
backend,
seedData,
}) => {
const officeApi = new OfficeApiClient(backend.baseUrl);
const suffix = Date.now().toString(36);
const workspaceName = `Settings Office Delete ${suffix}`;
const onboarded = await officeApi.completeOnboarding({
workspaceName,
taskPrefix: `SOD${suffix.toUpperCase().slice(0, 3)}`,
agentName: "Settings Delete CEO",
agentProfileId: seedData.agentProfileId,
executorPreference: "local_pc",
});
await officeApi.createSkill(onboarded.workspaceId, {
name: `Settings Cleanup Skill ${suffix}`,
slug: `settings-cleanup-skill-${suffix}`,
content: "# Settings cleanup skill\n",
});
await testPage.goto(`/settings/workspaces/${onboarded.workspaceId}`);
await expect(testPage.getByTestId("workspace-settings-switcher")).toHaveText(workspaceName, {
timeout: 15_000,
});
await testPage.getByTestId("workspace-settings-delete-button").click();
const confirmInput = testPage.getByTestId("workspace-settings-delete-confirm-input");
const confirmButton = testPage.getByTestId("workspace-settings-delete-confirm-button");
await expect(confirmInput).toBeVisible();
await confirmInput.fill(workspaceName);
await expect(confirmButton).toBeEnabled();
await confirmButton.click();
await expect(testPage).toHaveURL(/\/settings\/workspaces$/, { timeout: 10_000 });
const { workspaces } = await apiClient.listWorkspaces();
expect(workspaces.some((item) => item.id === onboarded.workspaceId)).toBe(false);
await apiClient.saveUserSettings({
workspace_id: seedData.workspaceId,
workflow_filter_id: seedData.workflowId,
keyboard_shortcuts: {},
enable_preview_on_click: false,
sidebar_views: [],
});
});
});