Files
fusion/packages/dashboard/app/components/__tests__/Board.canDropTask.test.tsx
gsxdsm e57db86a5c FN-6029: restore board drop-check regression coverage
Restore Board drag-drop pre-check coverage by extracting the decision logic into a reusable helper.

- extract Board canDropTask rejection logic into a shared helper used by the component
- replace Lane-mocking coverage with direct helper tests for workflow, column, and capacity branches
- add regression cases for missing workflows, default-workflow fallback, same-column WIP drops, and cross-workflow occupancy handling

Files changed:
 packages/dashboard/app/components/Board.tsx        |  37 +--
 .../__tests__/Board.canDropTask.test.tsx           | 261 ++++++++++++---------
 .../dashboard/app/components/boardCanDropTask.ts   |  56 +++++
 3 files changed, 218 insertions(+), 136 deletions(-)

Fusion-Task-Id: FN-6029

Fusion-Task-Lineage: cd71bbc5-0f97-49c3-9eef-0dd8221341c0
2026-06-08 13:32:58 -07:00

213 lines
6.4 KiB
TypeScript

// FN-1416/FN-6029: Board-level coverage of the canDropTask drag pre-check (R17).
//
// Board now passes one-argument per-column wrappers directly to <Column> in the
// selected-workflow rendering path, while <Lane> still adapts the canonical
// three-argument decision for multi-lane rendering. These tests exercise the
// pure Board decision seam directly so unrendered-column branches (especially
// unknownColumn) remain covered without stale Lane mocking.
import { describe, it, expect } from "vitest";
import type { Task } from "@fusion/core";
import type { BoardWorkflowsPayload } from "../../api";
import { getBoardCanDropTaskRejection } from "../boardCanDropTask";
const DEFAULT_WORKFLOW = "builtin:coding";
const CUSTOM_WORKFLOW = "WF-001";
// builtin:coding columns (in-progress counts toward wip; todo does not).
const defaultColumns = [
{ id: "triage", name: "Triage", flags: {} },
{ id: "todo", name: "Todo", flags: {} },
{ id: "in-progress", name: "In Progress", flags: { countsTowardWip: true } },
{ id: "in-review", name: "In Review", flags: {} },
{ id: "done", name: "Done", flags: { complete: true } },
];
const customColumns = [
{ id: "c-intake", name: "Intake", flags: { intake: true } },
{ id: "c-run", name: "Run", flags: { countsTowardWip: true } },
{ id: "c-done", name: "Done", flags: { complete: true } },
];
function makeTask(id: string, column: string): Task {
const now = new Date().toISOString();
return {
id,
description: id,
column,
dependencies: [],
createdAt: now,
updatedAt: now,
size: "M",
subtasks: [],
log: [],
tags: [],
blockedBy: [],
source: { sourceType: "api" },
} as unknown as Task;
}
function boardWorkflows(taskWorkflowIds: Record<string, string> = {}): BoardWorkflowsPayload {
return {
flagEnabled: true,
defaultWorkflowId: DEFAULT_WORKFLOW,
workflows: [
{ id: DEFAULT_WORKFLOW, name: "Coding", columns: defaultColumns },
{ id: CUSTOM_WORKFLOW, name: "Custom", columns: customColumns },
],
taskWorkflowIds,
};
}
function canDrop({
workflows = boardWorkflows(),
tasks,
maxConcurrent = 2,
taskId = "FN-1",
targetColumnId,
laneWorkflowId = DEFAULT_WORKFLOW,
}: {
workflows?: BoardWorkflowsPayload | null | undefined;
tasks: Task[];
maxConcurrent?: number;
taskId?: string;
targetColumnId: string;
laneWorkflowId?: string;
}) {
return getBoardCanDropTaskRejection({
boardWorkflows: workflows,
tasks,
maxConcurrent,
taskId,
targetColumnId,
laneWorkflowId,
});
}
describe("Board canDropTask pre-check (FN-1416/FN-6029)", () => {
it("cross-workflow drag returns workflowMismatch", () => {
const tasks = [makeTask("FN-1", "todo")];
expect(canDrop({
workflows: boardWorkflows({ "FN-1": DEFAULT_WORKFLOW }),
tasks,
targetColumnId: "c-run",
laneWorkflowId: CUSTOM_WORKFLOW,
})).toBe("board.rejection.workflowMismatch");
});
it("unknown target column in the source workflow returns unknownColumn", () => {
const tasks = [makeTask("FN-1", "todo")];
expect(canDrop({
workflows: boardWorkflows({ "FN-1": DEFAULT_WORKFLOW }),
tasks,
targetColumnId: "does-not-exist",
})).toBe("board.rejection.unknownColumn");
});
it("full wip column returns capacityExhausted", () => {
// maxConcurrent: 2; two cards already occupy in-progress in the default
// workflow. Dragging a third from todo into in-progress must reject.
const tasks = [
makeTask("FN-1", "todo"),
makeTask("FN-2", "in-progress"),
makeTask("FN-3", "in-progress"),
];
expect(canDrop({
workflows: boardWorkflows({
"FN-1": DEFAULT_WORKFLOW,
"FN-2": DEFAULT_WORKFLOW,
"FN-3": DEFAULT_WORKFLOW,
}),
tasks,
targetColumnId: "in-progress",
})).toBe("board.rejection.capacityExhausted");
});
it("valid same-workflow drops under capacity return null", () => {
const tasks = [makeTask("FN-1", "todo"), makeTask("FN-2", "in-progress")];
const workflows = boardWorkflows({ "FN-1": DEFAULT_WORKFLOW, "FN-2": DEFAULT_WORKFLOW });
expect(canDrop({ workflows, tasks, targetColumnId: "in-progress" })).toBeNull();
expect(canDrop({ workflows, tasks, targetColumnId: "in-review" })).toBeNull();
});
it("returns null when boardWorkflows is undefined", () => {
expect(canDrop({
workflows: undefined,
tasks: [makeTask("FN-1", "todo")],
targetColumnId: "in-progress",
})).toBeNull();
});
it("returns null when the source task is missing", () => {
expect(canDrop({
workflows: boardWorkflows({ "FN-1": DEFAULT_WORKFLOW }),
tasks: [makeTask("FN-2", "todo")],
taskId: "FN-1",
targetColumnId: "in-progress",
})).toBeNull();
});
it("returns null when the source workflow is missing from boardWorkflows", () => {
expect(canDrop({
workflows: {
flagEnabled: true,
defaultWorkflowId: DEFAULT_WORKFLOW,
workflows: [{ id: CUSTOM_WORKFLOW, name: "Custom", columns: customColumns }],
taskWorkflowIds: { "FN-1": DEFAULT_WORKFLOW },
},
tasks: [makeTask("FN-1", "todo")],
targetColumnId: "in-progress",
})).toBeNull();
});
it("falls back to the default workflow when a task has no workflow id", () => {
expect(canDrop({
workflows: boardWorkflows({}),
tasks: [makeTask("FN-1", "todo")],
targetColumnId: "does-not-exist",
laneWorkflowId: DEFAULT_WORKFLOW,
})).toBe("board.rejection.unknownColumn");
});
it("allows same-column wip drops even when the column is at capacity", () => {
const tasks = [
makeTask("FN-1", "in-progress"),
makeTask("FN-2", "in-progress"),
makeTask("FN-3", "in-progress"),
];
expect(canDrop({
workflows: boardWorkflows({
"FN-1": DEFAULT_WORKFLOW,
"FN-2": DEFAULT_WORKFLOW,
"FN-3": DEFAULT_WORKFLOW,
}),
tasks,
targetColumnId: "in-progress",
})).toBeNull();
});
it("does not count occupants from other workflows toward this workflow's capacity", () => {
const tasks = [
makeTask("FN-1", "todo"),
makeTask("FN-2", "in-progress"),
makeTask("FN-3", "in-progress"),
makeTask("FN-4", "in-progress"),
];
expect(canDrop({
workflows: boardWorkflows({
"FN-1": DEFAULT_WORKFLOW,
"FN-2": DEFAULT_WORKFLOW,
"FN-3": CUSTOM_WORKFLOW,
"FN-4": CUSTOM_WORKFLOW,
}),
tasks,
targetColumnId: "in-progress",
})).toBeNull();
});
});