Files
kandev/apps/web/lib/tasks/wip-queue-view-model.test.ts
2026-09-18 10:33:17 +01:00

168 lines
4.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { KanbanState, WorkflowSnapshotData } from "@/lib/state/slices/kanban/types";
import { buildWipQueueViewModel } from "./wip-queue-view-model";
type Task = KanbanState["tasks"][number];
const WORKFLOW_ID = "workflow-1";
const WORKSPACE_ID = "workspace-1";
const FEEDER_STEP_ID = "backlog";
const STEP_ID = "review";
function task(overrides: Partial<Task> = {}): Task {
return {
id: "queued-task",
workspaceId: WORKSPACE_ID,
workflowId: WORKFLOW_ID,
workflowStepId: STEP_ID,
queuedForStepId: STEP_ID,
wipAdmitted: false,
title: "Queued task",
position: 1,
priority: "medium",
queuedAt: "2026-09-18T10:00:00Z",
createdAt: "2026-09-18T09:00:00Z",
...overrides,
};
}
function snapshot(tasks: Task[] = [task()]): WorkflowSnapshotData {
return {
workflowId: WORKFLOW_ID,
workflowName: "Development",
steps: [
{
id: FEEDER_STEP_ID,
title: "Backlog",
color: "gray",
position: 0,
wip_limit: 0,
},
{
id: STEP_ID,
title: "Review",
color: "blue",
position: 1,
wip_limit: 2,
},
],
tasks,
};
}
function input(overrides: Partial<Parameters<typeof buildWipQueueViewModel>[0]> = {}) {
return {
taskId: "queued-task",
snapshots: { [WORKFLOW_ID]: snapshot() },
activeWorkflowId: WORKFLOW_ID,
activeTasks: [],
activeSteps: [],
workflows: [{ id: WORKFLOW_ID, workspaceId: WORKSPACE_ID, name: "Development" }],
...overrides,
};
}
describe("buildWipQueueViewModel", () => {
it("uses the destination step and workspace identity", () => {
const view = buildWipQueueViewModel(input());
expect(view).toMatchObject({
workflowName: "Development",
destinationTitle: "Review",
position: 1,
total: 1,
admittedCount: 0,
wipLimit: 2,
settingsHref: `/settings/workspaces/${WORKSPACE_ID}/workflows#workflow-card-${WORKFLOW_ID}`,
});
});
it("keeps the WIP banner for a task without a session", () => {
const view = buildWipQueueViewModel(
input({ activeTasks: [task({ primarySessionId: null })], snapshots: {} }),
);
expect(view?.position).toBe(1);
});
it("prefers the active workflow projection over a stale snapshot", () => {
const view = buildWipQueueViewModel(
input({
snapshots: {
[WORKFLOW_ID]: snapshot([task()]),
},
activeTasks: [task({ queuedForStepId: undefined, wipAdmitted: true })],
activeSteps: snapshot().steps,
}),
);
expect(view).toBeNull();
});
it("keeps the WIP banner for a task queued from its feeder", () => {
const view = buildWipQueueViewModel(
input({
snapshots: {
[WORKFLOW_ID]: snapshot([
task({
workflowStepId: FEEDER_STEP_ID,
queuedForStepId: STEP_ID,
wipAdmitted: true,
}),
]),
},
}),
);
expect(view).toMatchObject({
workflowName: "Development",
destinationTitle: "Review",
position: null,
total: null,
admittedCount: 0,
wipLimit: 2,
settingsHref: `/settings/workspaces/${WORKSPACE_ID}/workflows#workflow-card-${WORKFLOW_ID}`,
});
});
it("does not invent a workflow link when identity is unavailable", () => {
const view = buildWipQueueViewModel(input({ workflows: [] }));
expect(view).toMatchObject({
workflowName: null,
destinationTitle: "Review",
settingsHref: null,
});
});
it("does not report a WIP queue for an admitted task", () => {
expect(
buildWipQueueViewModel(
input({
snapshots: {
[WORKFLOW_ID]: snapshot([task({ wipAdmitted: true, queuedForStepId: undefined })]),
},
}),
),
).toBeNull();
});
it("keeps position and counts unavailable for a placeholder snapshot", () => {
const placeholder = { ...snapshot(), isPlaceholder: true };
const view = buildWipQueueViewModel(input({ snapshots: { [WORKFLOW_ID]: placeholder } }));
expect(view).toMatchObject({ position: null, total: null, admittedCount: null });
});
it("uses the active workflow projection when the snapshot is a placeholder", () => {
const view = buildWipQueueViewModel(
input({
snapshots: { [WORKFLOW_ID]: { ...snapshot(), isPlaceholder: true } },
activeTasks: [task()],
activeSteps: snapshot().steps,
}),
);
expect(view).toMatchObject({ position: 1, total: 1, admittedCount: 0 });
});
});