import { expect, test } from "../../fixtures/test-base"; import { KanbanPage } from "../../pages/kanban-page"; import { useRegularMode } from "../../helpers/regular-mode"; useRegularMode(); test("dragging into a feeder wakes an open pull target without reload", async ({ testPage, apiClient, seedData, }) => { await testPage.setViewportSize({ width: 1440, height: 900 }); const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Feeder Move Workflow"); const sourceStep = await apiClient.createWorkflowStep(workflow.id, "C", 0, { is_start_step: true, }); const feederStep = await apiClient.createWorkflowStep(workflow.id, "A", 1); const pullStep = await apiClient.createWorkflowStep(workflow.id, "B", 2); await apiClient.updateWorkflowStep(pullStep.id, { wip_limit: 1, pull_from_step_id: feederStep.id, }); await apiClient.saveUserSettings({ workspace_id: seedData.workspaceId, workflow_filter_id: workflow.id, }); const task = await apiClient.createTask(seedData.workspaceId, "Wake feeder task", { workflow_id: workflow.id, workflow_step_id: sourceStep.id, }); const kanban = new KanbanPage(testPage); await kanban.goto(); const pullColumn = kanban.columnByStepId(pullStep.id); await expect(pullColumn).toContainText("0/1"); const sourceCard = kanban.taskCard(task.id); const feederColumn = kanban.columnByStepId(feederStep.id); await sourceCard.waitFor({ state: "visible" }); const sourceBox = await sourceCard.boundingBox(); expect(sourceBox).not.toBeNull(); const sourceX = sourceBox!.x + sourceBox!.width / 2; const sourceY = sourceBox!.y + sourceBox!.height / 2; await testPage.mouse.move(sourceX, sourceY); await testPage.mouse.down(); await testPage.mouse.move(sourceX + 20, sourceY, { steps: 4 }); await expect(sourceCard).toHaveClass(/opacity-50/, { timeout: 5_000 }); await expect(testPage.getByTestId("desktop-kanban-drag-end-reserve")).toHaveCount(1); await testPage.evaluate( () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))), ); // Dragging can temporarily move destinations before the anchored source // column, so scroll them back into the viewport before dropping. let feederBox = await feederColumn.boundingBox(); const scrollWindowBox = await testPage.getByTestId("desktop-kanban-scroll-window").boundingBox(); expect(scrollWindowBox).not.toBeNull(); if (feederBox && feederBox.x + feederBox.width <= 0) { await testPage.mouse.move(scrollWindowBox!.x + 2, sourceY, { steps: 12 }); await expect .poll(async () => { const box = await feederColumn.boundingBox(); return box ? box.x + box.width / 2 : -1; }) .toBeGreaterThan(0); feederBox = await feederColumn.boundingBox(); } expect(feederBox).not.toBeNull(); const feederPoint = { x: feederBox!.x + feederBox!.width / 2, y: feederBox!.y + Math.min(160, feederBox!.height / 2), }; await testPage.mouse.move(feederPoint.x, feederPoint.y, { steps: 1 }); await expect(feederColumn).toHaveClass(/bg-primary\/5/); await testPage.mouse.up(); await expect(pullColumn).toContainText("1/1"); await expect(kanban.taskCardInColumn("Wake feeder task", pullStep.id)).toBeVisible(); await expect(kanban.taskCardInColumn("Wake feeder task", sourceStep.id)).not.toBeVisible(); }); test("shows same-step WIP overflow and promotes the next queued task", async ({ testPage, apiClient, seedData, prCapture, }) => { const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Visible Queue Workflow"); const reviewStep = await apiClient.createWorkflowStep(workflow.id, "Review", 0, { is_start_step: true, }); const doneStep = await apiClient.createWorkflowStep(workflow.id, "Done", 1); await apiClient.updateWorkflowStep(reviewStep.id, { wip_limit: 2 }); await apiClient.saveUserSettings({ workspace_id: seedData.workspaceId, workflow_filter_id: workflow.id, }); const tasks = []; for (let index = 1; index <= 7; index += 1) { tasks.push( await apiClient.createTask(seedData.workspaceId, `Queue Review ${index}`, { workflow_id: workflow.id, workflow_step_id: reviewStep.id, }), ); } const kanban = new KanbanPage(testPage); await kanban.goto(); const reviewColumn = kanban.columnByStepId(reviewStep.id); await expect(reviewColumn).toContainText("2/2"); await expect(reviewColumn.getByTestId("task-card-title")).toHaveCount(7); await expect(reviewColumn.getByTestId("kanban-queued-section")).toBeVisible(); await expect(reviewColumn).toContainText("Queued for Review"); await prCapture.screenshot("desktop-visible-queue", { caption: "Desktop Kanban showing seven visible tasks with two admitted cards and queued overflow.", }); await apiClient.moveTask(tasks[0].id, workflow.id, doneStep.id); await expect(reviewColumn).toContainText("2/2"); await expect(kanban.taskCardByTitle("Queue Review 3")).toBeVisible(); await expect(kanban.taskCardByTitle("Queue Review 3")).not.toContainText("Queued for Review"); }); test("moves into a full step, shows sidebar queue position, and promotes through the UI", async ({ testPage, apiClient, seedData, }) => { const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Move Queue Workflow"); const backlogStep = await apiClient.createWorkflowStep(workflow.id, "Backlog", 0, { is_start_step: true, }); const reviewStep = await apiClient.createWorkflowStep(workflow.id, "Review", 1); const doneStep = await apiClient.createWorkflowStep(workflow.id, "Done", 2); await apiClient.updateWorkflowStep(reviewStep.id, { wip_limit: 2 }); await apiClient.saveUserSettings({ workspace_id: seedData.workspaceId, workflow_filter_id: workflow.id, }); const admittedOne = await apiClient.createTask(seedData.workspaceId, "Admitted Review One", { workflow_id: workflow.id, workflow_step_id: reviewStep.id, }); await apiClient.createTask(seedData.workspaceId, "Admitted Review Two", { workflow_id: workflow.id, workflow_step_id: reviewStep.id, }); const source = await apiClient.createTask(seedData.workspaceId, "Moved Review Queue", { workflow_id: workflow.id, workflow_step_id: backlogStep.id, }); const kanban = new KanbanPage(testPage); await kanban.goto(); const reviewColumn = kanban.columnByStepId(reviewStep.id); await expect(reviewColumn).toContainText("2/2"); await kanban.moveTaskWithinWorkflow(source.id, reviewStep.id); const queuedCard = kanban.taskCardInColumn("Moved Review Queue", reviewStep.id); await expect(queuedCard).toBeVisible({ timeout: 10_000 }); await expect(queuedCard).toContainText("Queued for Review"); await expect(reviewColumn.getByTestId("kanban-queued-section")).toBeVisible(); const sidebarRow = testPage .getByTestId("sidebar-task-item") .filter({ hasText: "Moved Review Queue" }); await expect(sidebarRow).toBeVisible({ timeout: 10_000 }); const queueStatus = sidebarRow.getByTestId("sidebar-task-wip-queue"); await expect(queueStatus).toBeVisible(); await expect(queueStatus.locator("svg")).toBeVisible(); await expect(queueStatus).toHaveAttribute("aria-label", "Position 1 of 1 in Review queue"); await queueStatus.hover(); await expect(testPage.getByRole("tooltip")).toHaveText("Position 1 of 1 in Review queue"); await testPage.keyboard.press("Escape"); await kanban.moveTaskWithinWorkflow(admittedOne.id, doneStep.id); await expect(queuedCard).not.toContainText("Queued for Review", { timeout: 15_000 }); await expect(reviewColumn.getByTestId("kanban-queued-section")).toHaveCount(0); await expect(sidebarRow.getByTestId("sidebar-task-wip-queue")).toHaveCount(0); });