186 lines
7.6 KiB
TypeScript
186 lines
7.6 KiB
TypeScript
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 expect(sourceCard).toBeVisible({ timeout: 30_000 });
|
|
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<void>((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);
|
|
});
|