kanban-dev-webapp/src/lib/roadmap/load.test.ts
Dave 92c2501726 feat(plan): loadPlan loader with pure buildPlanSections helper
New loader returns the sectioned shape the Gantt view needs:

- buildPlanSections (pure): groups cards by project, sorts by
  start_date asc / title, splits subtasks into dated vs undated.
  Five unit tests cover the grouping and sorting rules.
- loadPlan (server-only): one PocketBase trip per collection,
  delegates to buildPlanSections.

471/471 unit tests pass (was 466, +5 new).
2026-09-21 16:13:47 +02:00

197 lines
5.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { buildPlanSections } from "@/lib/roadmap/load";
import type {
CardRecord,
ClientRecord,
ColumnRecord,
ProjectRecord,
SubtaskRecord,
} from "@/lib/pb/types";
function project(overrides: Partial<ProjectRecord> = {}): ProjectRecord {
return {
id: "p1",
created: "2026-01-01 00:00:00.000Z",
updated: "2026-01-01 00:00:00.000Z",
client: "c1",
name: "Client Board",
slug: "client-board",
description: "",
status: "active",
kind: "project",
color: "#0ea5e9",
repo_owner: "",
repo_name: "",
repo_url: "",
webhook_secret: "",
last_ingest_at: "",
session_gap_minutes: 15,
pre_session_minutes: 5,
post_session_minutes: 5,
background_md: "",
show_in_intake: false,
...overrides,
};
}
function card(overrides: Partial<CardRecord> = {}): CardRecord {
return {
id: "k1",
created: "2026-01-01 00:00:00.000Z",
updated: "2026-01-01 00:00:00.000Z",
project: "p1",
column: "col1",
title: "Refactor billing modal",
description: "",
position: 0,
priority: "medium",
target_date: "",
created_by: "",
created_by_role: "owner",
accepted: true,
start_date: "",
scope_rule: "",
archived: false,
number: 1,
narrative: "",
narrative_version: 0,
narrative_shas: "",
brief: null,
transcript: null,
size_band: "",
assignee: "",
...overrides,
};
}
function subtask(overrides: Partial<SubtaskRecord> = {}): SubtaskRecord {
return {
id: "s1",
created: "2026-01-01 00:00:00.000Z",
updated: "2026-01-01 00:00:00.000Z",
card: "k1",
title: "Write tests",
done: false,
position: 0,
start_date: "",
target_date: "",
...overrides,
};
}
function column(overrides: Partial<ColumnRecord> = {}): ColumnRecord {
return {
id: "col1",
created: "",
updated: "",
project: "p1",
name: "To Do",
position: 0,
is_inbox: false,
is_done: false,
...overrides,
};
}
function client(overrides: Partial<ClientRecord> = {}): ClientRecord {
return {
id: "c1",
created: "",
updated: "",
name: "Acme",
slug: "acme",
brand_color: "#0ea5e9",
active: true,
...overrides,
};
}
describe("buildPlanSections — groups cards by project, sorts by date", () => {
it("returns one section per active project, sorted client-then-project", () => {
const sections = buildPlanSections({
clients: [client({ id: "c2", name: "Bravo" }), client({ id: "c1", name: "Acme" })],
projects: [
project({ id: "p2", client: "c2", name: "Bravo Site", slug: "bravo-site" }),
project({ id: "p1", client: "c1", name: "Acme App", slug: "acme-app" }),
],
columns: [column()],
cards: [card({ project: "p2" }), card({ project: "p1" })],
subtasks: [],
});
expect(sections.map((s) => s.project.name)).toEqual(["Acme App", "Bravo Site"]);
});
it("excludes archived projects and Done column cards", () => {
const sections = buildPlanSections({
clients: [client()],
projects: [project({ status: "archived" })],
columns: [
column({ id: "col-done", name: "Done", is_done: true }),
column({ id: "col-todo", name: "To Do" }),
],
cards: [
card({ id: "k-archived-project", column: "col-todo" }),
card({ id: "k-done", column: "col-done" }),
],
subtasks: [],
});
expect(sections).toEqual([]);
});
it("sorts cards within a project: dated first by start_date asc, then undated by title", () => {
const sections = buildPlanSections({
clients: [client()],
projects: [project()],
columns: [column()],
cards: [
card({ id: "z-late", title: "Late", start_date: "2026-03-01", target_date: "2026-03-15" }),
card({ id: "a-early", title: "Early", start_date: "2026-02-01", target_date: "2026-02-10" }),
card({ id: "n-zeta", title: "Zeta" }),
card({ id: "n-alpha", title: "Alpha" }),
],
subtasks: [],
});
expect(sections[0].cards.map((c) => c.id)).toEqual([
"a-early",
"z-late",
"n-alpha",
"n-zeta",
]);
});
it("groups subtasks under their parent card and splits dated vs undated", () => {
const sections = buildPlanSections({
clients: [client()],
projects: [project()],
columns: [column()],
cards: [card({ id: "k1", start_date: "2026-02-01", target_date: "2026-02-10" })],
subtasks: [
subtask({ id: "s-dated", position: 1, start_date: "2026-02-01", target_date: "2026-02-05" }),
subtask({ id: "s-undated", position: 0 }),
],
});
const c = sections[0].cards[0];
expect(c.datedSubtasks.map((s) => s.id)).toEqual(["s-dated"]);
expect(c.undatedSubtaskCount).toBe(1);
});
it("sorts subtasks by position within a card", () => {
const sections = buildPlanSections({
clients: [client()],
projects: [project()],
columns: [column()],
cards: [card({ start_date: "2026-02-01", target_date: "2026-02-10" })],
subtasks: [
subtask({ id: "s2", position: 2, start_date: "2026-02-01", target_date: "2026-02-05" }),
subtask({ id: "s1", position: 1, start_date: "2026-02-01", target_date: "2026-02-03" }),
],
});
expect(sections[0].cards[0].datedSubtasks.map((s) => s.id)).toEqual(["s1", "s2"]);
});
});