Files
xboct-flow/src/kanban/buildKanbanBoard.test.ts
T

148 lines
3.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { Task } from "@/model/task";
import {
TASK_STATUS_META,
TASK_STATUS_ORDER,
TaskStatus,
} from "@/model/taskStatus";
import { buildKanbanBoard } from "./buildKanbanBoard";
function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task {
return {
description: "",
status: TaskStatus.TODO,
priority: "medium",
tags: [],
assignee: "",
created: "2026-01-01T00:00:00.000Z",
updated: "2026-01-01T00:00:00.000Z",
...partial,
};
}
const visibleStatuses = TASK_STATUS_ORDER.filter(
(status) => !TASK_STATUS_META[status].defaultHidden,
);
describe("buildKanbanBoard", () => {
const tasks: Task[] = [
task({
id: "a",
title: "Done one",
status: TaskStatus.DONE,
updated: "2026-01-01T00:00:02.000Z",
}),
task({
id: "b",
title: "Todo older",
status: TaskStatus.TODO,
updated: "2026-01-01T00:00:01.000Z",
}),
task({
id: "c",
title: "Todo newer",
status: TaskStatus.TODO,
updated: "2026-01-01T00:00:03.000Z",
}),
task({
id: "d",
title: "In progress",
status: TaskStatus.IN_PROGRESS,
updated: "2026-01-01T00:00:04.000Z",
}),
task({
id: "e",
title: "Backlog item",
status: TaskStatus.BACKLOG,
updated: "2026-01-01T00:00:00.000Z",
}),
task({
id: "f",
title: "Cancelled",
status: TaskStatus.CANCELLED,
updated: "2026-01-01T00:00:05.000Z",
}),
];
it("hides defaultHidden statuses by default", () => {
const board = buildKanbanBoard(tasks);
const statuses = board.columns.map((col) => col.status);
expect(statuses).toEqual(visibleStatuses);
for (const col of board.columns) {
expect(TASK_STATUS_META[col.status].defaultHidden).toBe(false);
expect(col.label).toBe(TASK_STATUS_META[col.status].label);
}
});
it("includes all statuses when showHiddenStatuses is true", () => {
const board = buildKanbanBoard(tasks, { showHiddenStatuses: true });
expect(board.columns.map((col) => col.status)).toEqual(TASK_STATUS_ORDER);
for (const col of board.columns) {
expect(col.label).toBe(TASK_STATUS_META[col.status].label);
}
});
it("places each task into the column matching its status", () => {
const board = buildKanbanBoard(tasks, { showHiddenStatuses: true });
for (const col of board.columns) {
for (const t of col.tasks) {
expect(t.status).toBe(col.status);
}
}
const byStatus = Object.fromEntries(
board.columns.map((col) => [col.status, col.tasks.map((t) => t.id)]),
);
expect(byStatus[TaskStatus.TODO]).toEqual(["c", "b"]);
expect(byStatus[TaskStatus.IN_PROGRESS]).toEqual(["d"]);
expect(byStatus[TaskStatus.DONE]).toEqual(["a"]);
expect(byStatus[TaskStatus.BACKLOG]).toEqual(["e"]);
expect(byStatus[TaskStatus.CANCELLED]).toEqual(["f"]);
});
it("sorts tasks within a column by updated desc", () => {
const board = buildKanbanBoard(tasks);
const todo = board.columns.find((col) => col.status === TaskStatus.TODO);
expect(todo?.tasks.map((t) => t.id)).toEqual(["c", "b"]);
});
it("returns empty visible columns when there are no tasks", () => {
const board = buildKanbanBoard([]);
expect(board.columns.map((col) => col.status)).toEqual(visibleStatuses);
for (const col of board.columns) {
expect(col.tasks).toEqual([]);
}
});
it("preserves input order for tasks with same updated timestamp (stable sort)", () => {
const sameUpdated = "2026-01-01T00:00:00.000Z";
const tasks: Task[] = [
task({
id: "a",
title: "A",
status: TaskStatus.TODO,
updated: sameUpdated,
}),
task({
id: "b",
title: "B",
status: TaskStatus.TODO,
updated: sameUpdated,
}),
task({
id: "c",
title: "C",
status: TaskStatus.TODO,
updated: sameUpdated,
}),
];
const board = buildKanbanBoard(tasks);
const todo = board.columns.find((col) => col.status === TaskStatus.TODO);
expect(todo?.tasks.map((t) => t.id)).toEqual(["a", "b", "c"]);
});
});