Files
kandev/apps/web/e2e/tests/task/subtask-detachment.spec.ts
T
2026-08-23 04:22:38 +00:00

149 lines
5.7 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { KanbanPage } from "../../pages/kanban-page";
import { SessionPage } from "../../pages/session-page";
const DETACH_LABEL = "Detach from parent";
test.describe("Subtask detachment", () => {
test("promotes an inherited-workspace subtree live from the sidebar", async ({
testPage,
apiClient,
seedData,
}) => {
const placement = {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
};
const parent = await apiClient.createTask(
seedData.workspaceId,
"Detach sidebar parent",
placement,
);
const child = await apiClient.createTask(seedData.workspaceId, "Detach sidebar child", {
...placement,
parent_id: parent.id,
workspace_mode: "inherit_parent",
});
const descendant = await apiClient.seedTask(seedData.workspaceId, "Detach sidebar descendant", {
...placement,
parent_id: child.id,
});
await testPage.goto(`/t/${parent.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
const taskBlock = (taskId: string) =>
session.sidebar.locator(`[data-testid="sortable-task-block"][data-task-id="${taskId}"]`);
await expect(taskBlock(child.id)).toHaveAttribute("data-depth", "1");
await expect(taskBlock(descendant.task_id)).toHaveAttribute("data-depth", "2");
const parentRow = session.sidebar
.getByTestId("sidebar-task-item")
.filter({ hasText: "Detach sidebar parent" });
await parentRow.click({ button: "right" });
await expect(testPage.getByRole("menuitem", { name: DETACH_LABEL })).toHaveCount(0);
await testPage.keyboard.press("Escape");
const childRow = session.sidebar
.getByTestId("sidebar-task-item")
.filter({ hasText: "Detach sidebar child" });
await childRow.click({ button: "right" });
await testPage.getByRole("menuitem", { name: DETACH_LABEL }).click();
const dialog = testPage.getByRole("alertdialog", { name: "Detach task from parent?" });
await expect(dialog).toContainText("shares its parent's workspace");
await expect(dialog).toContainText("Current and future sessions");
await dialog.getByTestId("detach-task-confirm").click();
await expect(taskBlock(child.id)).toHaveAttribute("data-depth", "0", { timeout: 10_000 });
await expect(taskBlock(descendant.task_id)).toHaveAttribute("data-depth", "1");
await expect(taskBlock(child.id).locator(`[data-task-id="${descendant.task_id}"]`)).toHaveCount(
1,
);
await expect
.poll(async () => {
const detached = await apiClient.getTask(child.id);
const workspace = detached.metadata?.workspace as { mode?: string } | undefined;
return { parentId: detached.parent_id ?? "", workspaceMode: workspace?.mode };
})
.toEqual({
parentId: "",
workspaceMode: "shared_group",
});
await expect
.poll(async () => (await apiClient.getTask(descendant.task_id)).parent_id)
.toBe(child.id);
await childRow.click({ button: "right" });
await expect(testPage.getByRole("menuitem", { name: DETACH_LABEL })).toHaveCount(0);
});
test("keeps card menus aligned and preserves kanban placement", async ({
testPage,
apiClient,
seedData,
}) => {
const placement = {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
};
const parent = await apiClient.createTask(
seedData.workspaceId,
"Detach kanban parent",
placement,
);
const child = await apiClient.createTask(seedData.workspaceId, "Detach kanban child", {
...placement,
parent_id: parent.id,
workspace_mode: "new_workspace",
});
const kanban = new KanbanPage(testPage);
await kanban.goto();
await kanban.openTaskActionsMenu(parent.id);
await expect(testPage.getByRole("menuitem", { name: DETACH_LABEL })).toHaveCount(0);
await testPage.keyboard.press("Escape");
await kanban.openTaskContextMenu(child.id);
await expect(testPage.getByRole("menuitem", { name: DETACH_LABEL })).toBeVisible();
await testPage.getByRole("menuitem", { name: DETACH_LABEL }).click();
const contextConfirmation = testPage.getByRole("dialog", { name: "Detach task from parent?" });
await expect(contextConfirmation).toBeVisible();
await contextConfirmation.getByRole("button", { name: "Cancel" }).click();
await expect(contextConfirmation).toHaveCount(0);
await expect(
kanban.taskCard(child.id).getByRole("button", { name: "More options" }),
).toBeFocused();
await expect.poll(async () => (await apiClient.getTask(child.id)).parent_id).toBe(parent.id);
await kanban.openTaskActionsMenu(child.id);
await testPage.getByRole("menuitem", { name: DETACH_LABEL }).click();
const confirmation = testPage.getByRole("dialog", { name: "Detach task from parent?" });
await expect(confirmation).toBeVisible();
await expect(testPage.getByRole("alertdialog")).toHaveCount(0);
await expect(confirmation).toContainText("workflow, subtasks, and state will not change");
await confirmation.getByTestId("detach-task-confirm").click();
await expect(kanban.taskCardInColumn("Detach kanban child", seedData.startStepId)).toBeVisible({
timeout: 10_000,
});
await expect
.poll(async () => {
const detached = await apiClient.getTask(child.id);
const workspace = detached.metadata?.workspace as { mode?: string } | undefined;
return {
parentId: detached.parent_id ?? "",
stepId: detached.workflow_step_id,
workspaceMode: workspace?.mode,
};
})
.toEqual({
parentId: "",
stepId: seedData.startStepId,
workspaceMode: "new_workspace",
});
});
});