Files
kandev/apps/web/e2e/tests/task/sidebar-subtask-collapse.spec.ts
T
2026-04-23 18:40:58 +01:00

142 lines
5.5 KiB
TypeScript

/**
* E2E tests for collapsing subtasks in the left sidebar.
*
* Covers:
* - Chevron toggle appears on parent tasks that have subtasks
* - Clicking the chevron hides/shows the subtask rows
* - Clicking the chevron does NOT select the parent task
* - Collapsed state survives page refresh (sessionStorage)
* - Collapsed state survives navigating to another task
*/
import { test, expect } from "../../fixtures/test-base";
import { SessionPage } from "../../pages/session-page";
test.describe("Sidebar subtasks — collapse/expand", () => {
test("chevron hides and restores subtasks, and survives reload + navigation", async ({
testPage,
apiClient,
seedData,
}) => {
const parent = await apiClient.createTask(seedData.workspaceId, "Collapse Parent Task", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
await apiClient.createTask(seedData.workspaceId, "Collapse Child One", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
parent_id: parent.id,
});
await apiClient.createTask(seedData.workspaceId, "Collapse Child Two", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
parent_id: parent.id,
});
// A second root task for the navigation-survives check.
const other = await apiClient.createTask(seedData.workspaceId, "Collapse Unrelated Task", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
await testPage.goto(`/t/${parent.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await expect(session.sidebar).toBeVisible({ timeout: 10_000 });
// Subtasks are visible initially.
await expect(session.sidebar.getByText("Collapse Child One")).toBeVisible({ timeout: 10_000 });
await expect(session.sidebar.getByText("Collapse Child Two")).toBeVisible({ timeout: 10_000 });
// Chevron button appears on the parent row.
const chevron = session.sidebar.locator(
`[data-testid='sidebar-subtask-toggle'][data-task-id='${parent.id}']`,
);
await expect(chevron).toBeVisible({ timeout: 5_000 });
await expect(chevron).toHaveAttribute("aria-expanded", "true");
// Clicking the chevron collapses the subtasks without selecting the parent.
const urlBefore = testPage.url();
await chevron.click();
await expect(session.sidebar.getByText("Collapse Child One")).not.toBeVisible({
timeout: 5_000,
});
await expect(session.sidebar.getByText("Collapse Child Two")).not.toBeVisible({
timeout: 5_000,
});
await expect(chevron).toHaveAttribute("aria-expanded", "false");
// URL must be unchanged — chevron click must not navigate.
expect(testPage.url()).toBe(urlBefore);
// Parent itself is still visible.
await expect(session.sidebar.getByText("Collapse Parent Task")).toBeVisible({ timeout: 5_000 });
// Reload — sessionStorage keeps the collapsed state.
await testPage.reload();
await session.waitForLoad();
await expect(
session.sidebar.locator(
`[data-testid='sidebar-subtask-toggle'][data-task-id='${parent.id}']`,
),
).toHaveAttribute("aria-expanded", "false", { timeout: 10_000 });
await expect(session.sidebar.getByText("Collapse Child One")).not.toBeVisible({
timeout: 5_000,
});
// Navigate to another task — state is still collapsed.
await testPage.goto(`/t/${other.id}`);
await session.waitForLoad();
await expect(
session.sidebar.locator(
`[data-testid='sidebar-subtask-toggle'][data-task-id='${parent.id}']`,
),
).toHaveAttribute("aria-expanded", "false", { timeout: 10_000 });
await expect(session.sidebar.getByText("Collapse Child One")).not.toBeVisible({
timeout: 5_000,
});
// Expanding restores the subtasks — and at this point the active task is
// `other`, not `parent`, so a bubbled parent-row click (if any) would
// change the URL. Capture it and assert it doesn't change after the
// chevron click, which exercises the no-navigation invariant on an
// unselected parent row.
const chevronAgain = session.sidebar.locator(
`[data-testid='sidebar-subtask-toggle'][data-task-id='${parent.id}']`,
);
const urlBeforeExpand = testPage.url();
await chevronAgain.click();
expect(testPage.url()).toBe(urlBeforeExpand);
await expect(chevronAgain).toHaveAttribute("aria-expanded", "true");
await expect(session.sidebar.getByText("Collapse Child One")).toBeVisible({ timeout: 5_000 });
await expect(session.sidebar.getByText("Collapse Child Two")).toBeVisible({ timeout: 5_000 });
});
test("tasks without subtasks do not render a chevron toggle", async ({
testPage,
apiClient,
seedData,
}) => {
const task = await apiClient.createTask(seedData.workspaceId, "Lonely Task No Subtasks", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await expect(session.sidebar.getByText("Lonely Task No Subtasks")).toBeVisible({
timeout: 10_000,
});
// No toggle should exist for this task.
await expect(
session.sidebar.locator(`[data-testid='sidebar-subtask-toggle'][data-task-id='${task.id}']`),
).toHaveCount(0);
});
});