Files
kandev/apps/web/e2e/tests/task/task-loading-state.spec.ts
2026-08-13 11:08:24 +01:00

179 lines
6.5 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { SidebarTasksPage } from "../../pages/sidebar-tasks-page";
import { SessionPage } from "../../pages/session-page";
import { openBlockedTaskLoadingState } from "./task-loading-state-helpers";
test.describe("Task loading state", () => {
test("shows a spinner instead of a blank task detail pane while task data loads", async ({
testPage,
apiClient,
seedData,
}) => {
const unblockTaskDetailRequest = await openBlockedTaskLoadingState({
testPage,
apiClient,
seedData,
title: "Task Loading State Anchor",
unresolvedTaskId: "unresolved-task-detail-desktop",
});
try {
await expect(testPage.getByTestId("task-loading-state")).toBeVisible({ timeout: 10_000 });
await expect(testPage.getByText("Loading task...")).toBeVisible();
} finally {
await unblockTaskDetailRequest();
}
});
test("keeps sibling tasks available after a missing task route", async ({
testPage,
apiClient,
seedData,
}) => {
const sibling = await apiClient.createTask(seedData.workspaceId, "Missing route sibling", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
await testPage.goto(`/t/missing-task-route-desktop?workspaceId=${seedData.workspaceId}`);
await expect(testPage.getByTestId("task-load-error-state")).toBeVisible({ timeout: 10_000 });
await expect(testPage.getByTestId("task-unavailable-overview-link")).toBeVisible();
const sidebar = new SidebarTasksPage(testPage);
await expect(sidebar.row(sibling.id)).toBeVisible({ timeout: 10_000 });
await expect(testPage.getByText("No tasks yet.", { exact: true })).toHaveCount(0);
await sidebar.plainClick(sibling.id);
await expect(testPage).toHaveURL(new RegExp(`/t/${sibling.id}$`));
await expect(testPage.getByTestId("task-load-error-state")).toBeHidden({ timeout: 10_000 });
});
test("keeps a session-backed sibling active", async ({ testPage, apiClient, seedData }) => {
const sibling = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Missing route session sibling",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
if (!sibling.session_id) throw new Error("session-backed sibling has no session");
await testPage.goto(`/t/missing-task-route-session?workspaceId=${seedData.workspaceId}`);
await expect(testPage.getByTestId("task-load-error-state")).toBeVisible({ timeout: 10_000 });
const sidebar = new SidebarTasksPage(testPage);
await expect(sidebar.row(sibling.id)).toBeVisible({ timeout: 10_000 });
await sidebar.plainClick(sibling.id);
await expect(testPage).toHaveURL(new RegExp(`/t/${sibling.id}$`));
await expect(testPage.getByTestId("task-load-error-state")).toBeHidden({ timeout: 10_000 });
await expect(testPage.getByTestId("dockview-task-layout")).toBeVisible({ timeout: 10_000 });
await expect(sidebar.row(sibling.id)).toHaveAttribute("data-active", "true", {
timeout: 10_000,
});
});
test("restores the last selected session after an A to B to A switch", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
const taskA = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Session route sync task A",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
const taskB = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Session route sync task B",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
if (!taskA.session_id || !taskB.session_id) {
throw new Error("session-backed route sync tasks have no primary session");
}
await testPage.goto(`/t/${taskA.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.openNewSessionDialog();
await expect(session.newSessionDialog()).toBeVisible({ timeout: 5_000 });
await session.newSessionPromptInput().fill("/e2e:simple-message");
await session.newSessionStartButton().click();
await expect(session.newSessionDialog()).not.toBeVisible({ timeout: 10_000 });
let secondarySessionId: string | undefined;
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(taskA.id);
secondarySessionId = sessions.find((item) => item.id !== taskA.session_id)?.id;
return secondarySessionId ?? null;
},
{ timeout: 30_000, message: "Waiting for task A secondary session" },
)
.toBeTruthy();
await expect(session.sessionTabBySessionId(secondarySessionId!)).toBeVisible({
timeout: 15_000,
});
await session.sessionTabBySessionId(secondarySessionId!).click();
await expect
.poll(async () =>
testPage.evaluate(
() =>
(
window as unknown as {
__KANDEV_E2E_STORE__?: { getState: () => { tasks: { activeSessionId: string } } };
}
).__KANDEV_E2E_STORE__?.getState().tasks.activeSessionId,
),
)
.toBe(secondarySessionId);
await session.clickTaskInSidebar("Session route sync task B");
await expect(testPage).toHaveURL(new RegExp(`/t/${taskB.id}$`), { timeout: 15_000 });
await session.waitForLoad();
await expect(session.sessionTabBySessionId(taskB.session_id)).toBeVisible({
timeout: 15_000,
});
await session.clickTaskInSidebar("Session route sync task A");
await expect(testPage).toHaveURL(new RegExp(`/t/${taskA.id}$`), { timeout: 15_000 });
await session.waitForLoad();
await expect
.poll(async () =>
testPage.evaluate(
() =>
(
window as unknown as {
__KANDEV_E2E_STORE__?: { getState: () => { tasks: { activeSessionId: string } } };
}
).__KANDEV_E2E_STORE__?.getState().tasks.activeSessionId,
),
)
.toBe(secondarySessionId);
});
});