| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { WorkItem, WorkProject } from "@g1t/contracts"; |
| 5 | |
| 6 | import { |
| 7 | datesText, |
| 8 | dayText, |
| 9 | healthTone, |
| 10 | hillCounts, |
| 11 | hillDots, |
| 12 | hillY, |
| 13 | isLive, |
| 14 | percentDone, |
| 15 | projectChangeOf, |
| 16 | projectGroups, |
| 17 | projectPath, |
| 18 | projectsPath, |
| 19 | sidebarProjects, |
| 20 | timeElapsed, |
| 21 | timeLeft, |
| 22 | } from "./projects.ts"; |
| 23 | |
| 24 | const project = (name: string, state: WorkProject["state"], archivedAt: string | null = null) => ({ name, state, archivedAt }); |
| 25 | const item = (number: number, horizon: WorkItem["horizon"] = "now") => ({ number, horizon }) as WorkItem; |
| 26 | const form = (fields: Record<string, string>) => { |
| 27 | const data = new FormData(); |
| 28 | for (const [key, value] of Object.entries(fields)) data.set(key, value); |
| 29 | return data; |
| 30 | }; |
| 31 | const today = new Date("2026-10-21T15:00:00Z"); |
| 32 | |
| 33 | test("a project's pages are under Work", () => { |
| 34 | assert.equal(projectsPath("Acme"), "/acme/-/work/projects"); |
| 35 | assert.equal(projectsPath("acme", "new=1"), "/acme/-/work/projects?new=1"); |
| 36 | assert.equal(projectPath("acme", "work-overhaul"), "/acme/-/work/projects/work-overhaul"); |
| 37 | assert.equal(projectPath("acme", "work-overhaul", "items"), "/acme/-/work/projects/work-overhaul/items"); |
| 38 | }); |
| 39 | |
| 40 | test("the sidebar lists live projects, active first, each by name", () => { |
| 41 | const listed = sidebarProjects([ |
| 42 | project("Zeta", "active"), |
| 43 | project("Beta", "paused"), |
| 44 | project("Alpha", "active"), |
| 45 | project("Gamma", "shaping"), |
| 46 | project("Done", "done"), |
| 47 | project("Old", "active", "2026-10-01T00:00:00Z"), |
| 48 | ]); |
| 49 | assert.deepEqual( |
| 50 | listed.map((p) => p.name), |
| 51 | ["Alpha", "Zeta", "Gamma", "Beta"], |
| 52 | ); |
| 53 | assert.ok(isLive(project("A", "paused"))); |
| 54 | assert.ok(!isLive(project("A", "dropped"))); |
| 55 | const { live, finished } = projectGroups([project("B", "done"), project("A", "active"), project("C", "dropped")]); |
| 56 | assert.deepEqual( |
| 57 | live.map((p) => p.name), |
| 58 | ["A"], |
| 59 | ); |
| 60 | assert.deepEqual( |
| 61 | finished.map((p) => p.name), |
| 62 | ["B", "C"], |
| 63 | ); |
| 64 | }); |
| 65 | |
| 66 | test("dates read as people read them", () => { |
| 67 | assert.equal(dayText("2026-12-12", 2026), "Dec 12"); |
| 68 | assert.equal(dayText("2027-01-04", 2026), "Jan 4, 2027"); |
| 69 | assert.equal(datesText({ startOn: "2026-10-20", targetOn: "2026-12-12" }, today), "Oct 20 → Dec 12"); |
| 70 | assert.equal(datesText({ startOn: null, targetOn: "2026-12-12" }, today), "Due Dec 12"); |
| 71 | assert.equal(datesText({ startOn: null, targetOn: null }, today), null); |
| 72 | assert.deepEqual(timeLeft({ targetOn: "2026-10-31", state: "active" }, today), { text: "10 days left", late: false }); |
| 73 | assert.deepEqual(timeLeft({ targetOn: "2026-12-12", state: "active" }, today), { text: "7 weeks left", late: false }); |
| 74 | assert.deepEqual(timeLeft({ targetOn: "2026-10-21", state: "active" }, today), { text: "Due today", late: false }); |
| 75 | assert.deepEqual(timeLeft({ targetOn: "2026-10-18", state: "paused" }, today), { text: "3 days late", late: true }); |
| 76 | // A finished project has no time left to count. |
| 77 | assert.equal(timeLeft({ targetOn: "2026-10-18", state: "done" }, today), null); |
| 78 | assert.equal(timeElapsed({ startOn: "2026-10-11", targetOn: "2026-10-31" }, new Date("2026-10-21T00:00:00Z")), 50); |
| 79 | assert.equal(timeElapsed({ startOn: null, targetOn: "2026-10-31" }, today), null); |
| 80 | }); |
| 81 | |
| 82 | test("progress and health read from the record", () => { |
| 83 | assert.equal(percentDone({ openItems: 13, closedItems: 11 }), 45); |
| 84 | assert.equal(percentDone({ openItems: 0, closedItems: 0 }), 0); |
| 85 | assert.equal(healthTone("at_risk"), "warn"); |
| 86 | assert.equal(healthTone(null), "neutral"); |
| 87 | }); |
| 88 | |
| 89 | test("each open work item sits in its stage's quarter of the hill, none on another", () => { |
| 90 | const open = [item(1), item(2), item(3), item(4, "next")]; |
| 91 | const dots = hillDots(open, [ |
| 92 | { number: 1, hill: "building" }, |
| 93 | { number: 2, hill: "building" }, |
| 94 | { number: 3, hill: "landing" }, |
| 95 | ]); |
| 96 | // #4 has no progress: it is still being figured out. |
| 97 | assert.deepEqual( |
| 98 | dots.map((dot) => [dot.item.number, dot.hill]), |
| 99 | [ |
| 100 | [4, "figuring_out"], |
| 101 | [1, "building"], |
| 102 | [2, "building"], |
| 103 | [3, "landing"], |
| 104 | ], |
| 105 | ); |
| 106 | const building = dots.filter((dot) => dot.hill === "building").map((dot) => dot.x); |
| 107 | assert.ok( |
| 108 | building.every((x) => x > 160 && x < 234), |
| 109 | "building is the third quarter, past the top", |
| 110 | ); |
| 111 | assert.notEqual(building[0], building[1]); |
| 112 | for (const dot of dots) assert.equal(dot.y, Math.round(hillY(dot.x) * 10) / 10); |
| 113 | assert.equal(hillY(160), 16); |
| 114 | assert.deepEqual(hillCounts(dots), { figuring_out: 1, decided: 0, building: 2, landing: 1 }); |
| 115 | }); |
| 116 | |
| 117 | test("a project's form posts only the fields it has", () => { |
| 118 | assert.deepEqual(projectChangeOf(form({ health: "at_risk" })), { health: "at_risk" }); |
| 119 | assert.deepEqual(projectChangeOf(form({ health: "" })), { health: null }); |
| 120 | assert.deepEqual(projectChangeOf(form({ name: " Work overhaul ", targetOn: "2026-12-12", appetite: "large" })), { |
| 121 | name: "Work overhaul", |
| 122 | targetOn: "2026-12-12", |
| 123 | appetite: "large", |
| 124 | }); |
| 125 | assert.equal(projectChangeOf(form({ name: " " })), "A project needs a name."); |
| 126 | assert.equal(projectChangeOf(form({ health: "fine" })), "Choose a health: on track, at risk or off track."); |
| 127 | assert.equal(projectChangeOf(form({ startOn: "2026-12-12", targetOn: "2026-10-01" })), "The target date can't be before the start date."); |
| 128 | assert.deepEqual(projectChangeOf(form({ brief: "## Problem\n" })), { brief: "## Problem\n" }); |
| 129 | }); |