Files
kandev/apps/web/e2e/tests/workflow/workflow-steps.spec.ts
T
2026-08-29 07:47:46 +00:00

194 lines
6.4 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { KanbanPage } from "../../pages/kanban-page";
import { SessionPage } from "../../pages/session-page";
test.describe("Workflow steps", () => {
test("task appears in correct column after API move", async ({
testPage,
apiClient,
seedData,
}) => {
// Seed a task (lands in the start step by default)
const task = await apiClient.createTask(seedData.workspaceId, "Workflow Move Task", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
// Find a non-start step to move to
const targetStep = seedData.steps.find((s) => !s.is_start_step);
if (!targetStep) {
test.skip(true, "No non-start step available to test move");
return;
}
// Move the task via API
await apiClient.moveTask(task.id, seedData.workflowId, targetStep.id);
// Navigate to kanban and verify the card is visible
const kanban = new KanbanPage(testPage);
await kanban.goto();
const card = kanban.taskCardByTitle("Workflow Move Task");
await expect(card).toBeVisible();
});
test("multiple tasks can be seeded into different steps", async ({
testPage,
apiClient,
seedData,
}) => {
const startStep = seedData.steps.find((s) => s.is_start_step) ?? seedData.steps[0];
const otherStep = seedData.steps.find((s) => s.id !== startStep.id);
if (!otherStep) {
test.skip(true, "Need at least 2 workflow steps");
return;
}
// Create tasks and move one
await apiClient.createTask(seedData.workspaceId, "Step A Task", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
const taskB = await apiClient.createTask(seedData.workspaceId, "Step B Task", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
await apiClient.moveTask(taskB.id, seedData.workflowId, otherStep.id);
const kanban = new KanbanPage(testPage);
await kanban.goto();
await expect(kanban.taskCardByTitle("Step A Task")).toBeVisible();
await expect(kanban.taskCardByTitle("Step B Task")).toBeVisible();
});
test("explains a rejected move on the task page", async ({ testPage, apiClient, seedData }) => {
const targetStep = seedData.steps.find((s) => !s.is_start_step);
if (!targetStep) {
test.skip(true, "No non-start step available to test move feedback");
return;
}
const { task_id: taskId } = await apiClient.seedTask(
seedData.workspaceId,
"Workflow Move Feedback Task",
{
state: "IN_PROGRESS",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
},
);
await apiClient.seedTaskSession(taskId, {
state: "RUNNING",
agentProfileId: seedData.agentProfileId,
repositoryId: seedData.repositoryId,
});
await testPage.route(`**/api/v1/tasks/${taskId}/move`, async (route) => {
await route.fulfill({
status: 409,
contentType: "application/json",
body: JSON.stringify({
error: "task has an active session (RUNNING)",
code: "task_move_active_session",
}),
});
});
await testPage.goto(`/t/${taskId}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await expect(session.stepper).toBeVisible();
await session.moveToWorkflowStep(targetStep);
const moveError = testPage.getByTestId("task-move-error-banner");
await expect(moveError).toBeVisible();
await expect(moveError).toContainText("Stop the active session before moving the task.");
await expect(
session.stepperStep(seedData.steps.find((s) => s.is_start_step)?.name ?? ""),
).toBeVisible();
});
test("explains a rejected move from the task sidebar", async ({
testPage,
apiClient,
seedData,
}) => {
const targetStep = seedData.steps.find((s) => !s.is_start_step);
if (!targetStep) {
test.skip(true, "No non-start step available to test move feedback");
return;
}
const { task_id: taskId } = await apiClient.seedTask(
seedData.workspaceId,
"Workflow Sidebar Move Feedback Task",
{
state: "IN_PROGRESS",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
},
);
await apiClient.seedTaskSession(taskId, {
state: "RUNNING",
agentProfileId: seedData.agentProfileId,
repositoryId: seedData.repositoryId,
});
await testPage.route(`**/api/v1/tasks/${taskId}/move`, async (route) => {
await route.fulfill({
status: 409,
contentType: "application/json",
body: JSON.stringify({
error: "task has an active session (RUNNING)",
code: "task_move_active_session",
}),
});
});
await testPage.goto(`/t/${taskId}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.openSidebarTaskContextMenu("Workflow Sidebar Move Feedback Task");
await testPage.getByTestId("task-context-move-to").hover();
await testPage.getByTestId(`task-context-step-${targetStep.id}`).click();
const moveError = testPage.getByTestId("task-move-error-banner");
await expect(moveError).toBeVisible();
await expect(moveError).toContainText("Stop the active session before moving the task.");
});
test("an unstarted feeder task fills available WIP capacity", async ({
testPage,
apiClient,
seedData,
}) => {
const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Feeder pull workflow");
const waiting = await apiClient.createWorkflowStep(workflow.id, "Waiting", 0, {
is_start_step: true,
});
const review = await apiClient.createWorkflowStep(workflow.id, "Review", 1);
await apiClient.updateWorkflowStep(review.id, {
wip_limit: 2,
pull_from_step_id: waiting.id,
});
await apiClient.createTask(seedData.workspaceId, "Unstarted feeder task", {
workflow_id: workflow.id,
workflow_step_id: waiting.id,
});
await apiClient.saveUserSettings({
workspace_id: seedData.workspaceId,
workflow_filter_id: workflow.id,
repository_ids: [],
});
const kanban = new KanbanPage(testPage);
await kanban.goto();
await expect(kanban.taskCardInColumn("Unstarted feeder task", review.id)).toBeVisible();
await expect(kanban.taskCardInColumn("Unstarted feeder task", waiting.id)).toHaveCount(0);
});
});