Public: anyone can read this; the crew can change it.
A test pins the home's KPI words: spend, backups, speed and visitors read right at zero, at their edges and with no data #6923
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6906-a-test-pins-the-home-s-kpi-words-spend-b → main 1 commit fork
1 file
changed +142
−0
test/kpi-words.test.ts added +142
| @@ -0,0 +1,142 @@ | ||
| 1 | import { describe, expect, test } from "bun:test"; | |
| 2 | import { backupAge, backupOverdue, buildersValue, reactNote, speedWords, spendFull, spendValue, trendText, usersValue, visitorsNote, visitorsValue } from "../src/flow/kpi_words.ts"; | |
| 3 | import { NO_REACH, type Reach } from "../src/flow/reach.ts"; | |
| 4 | ||
| 5 | const NOW = new Date("2026-10-27T12:00:00.000Z"); | |
| 6 | const ago = (ms: number): string => new Date(NOW.getTime() - ms).toISOString(); | |
| 7 | const H = 3_600_000; | |
| 8 | const reach = (o: Partial<Reach>): Reach => ({ ...NO_REACH, ...o }); | |
| 9 | const clean = (s: string): void => { | |
| 10 | for (const bad of ["NaN", "undefined", "Infinity", "-0"]) expect(s).not.toContain(bad); | |
| 11 | }; | |
| 12 | ||
| 13 | describe("trendText", () => { | |
| 14 | test("empty and all-zero series draw the lowest block, one each", () => { | |
| 15 | expect(trendText([])).toBe(""); | |
| 16 | expect(trendText([0, 0, 0])).toBe("▁▁▁"); | |
| 17 | }); | |
| 18 | test("up, down and flat read in the blocks' height", () => { | |
| 19 | const up = trendText([0, 2, 4, 8]); | |
| 20 | const down = trendText([8, 4, 2, 0]); | |
| 21 | const flat = trendText([3, 3, 3]); | |
| 22 | expect(up).toBe("▁▃▅█"); | |
| 23 | expect(down).toBe("█▅▃▁"); | |
| 24 | expect(flat).toBe("███"); | |
| 25 | expect(up).not.toBe(down); | |
| 26 | }); | |
| 27 | test("a single count of one is a full block", () => { | |
| 28 | expect(trendText([1])).toBe("█"); | |
| 29 | }); | |
| 30 | }); | |
| 31 | ||
| 32 | describe("spend", () => { | |
| 33 | test("zero is two decimals, with and without a ceiling", () => { | |
| 34 | expect(spendValue({ todayUsd: 0, ceilingUsd: null })).toBe("$0.00"); | |
| 35 | expect(spendValue({ todayUsd: 0, ceilingUsd: 40 })).toBe("$0.00 of $40"); | |
| 36 | expect(spendFull({ todayUsd: 0, ceilingUsd: null })).toBe("$0.00"); | |
| 37 | expect(spendFull({ todayUsd: 0, ceilingUsd: 40 })).toBe("$0.00 of $40.00"); | |
| 38 | }); | |
| 39 | test("the ten-dollar edge: cents below, whole dollars from ten", () => { | |
| 40 | expect(spendValue({ todayUsd: 9.99, ceilingUsd: null })).toBe("$9.99"); | |
| 41 | expect(spendValue({ todayUsd: 10, ceilingUsd: null })).toBe("$10"); | |
| 42 | expect(spendValue({ todayUsd: 12.4, ceilingUsd: 40 })).toBe("$12 of $40"); | |
| 43 | expect(spendValue({ todayUsd: 1, ceilingUsd: 5 })).toBe("$1.00 of $5.00"); | |
| 44 | }); | |
| 45 | test("spendFull is always to the cent", () => { | |
| 46 | expect(spendFull({ todayUsd: 12.4, ceilingUsd: 40 })).toBe("$12.40 of $40.00"); | |
| 47 | expect(spendFull({ todayUsd: 0.005, ceilingUsd: null })).toMatch(/^\$0\.0[01]$/); | |
| 48 | }); | |
| 49 | test("no answer is NaN, undefined, Infinity or -0", () => { | |
| 50 | for (const s of [{ todayUsd: 0, ceilingUsd: null }, { todayUsd: 0, ceilingUsd: 0 }, { todayUsd: 1, ceilingUsd: 1 }]) { | |
| 51 | clean(spendValue(s)); | |
| 52 | clean(spendFull(s)); | |
| 53 | } | |
| 54 | }); | |
| 55 | }); | |
| 56 | ||
| 57 | describe("backups", () => { | |
| 58 | test("none at all is overdue and never", () => { | |
| 59 | expect(backupOverdue(null, NOW)).toBe(true); | |
| 60 | expect(backupAge(null, NOW)).toBe("never"); | |
| 61 | }); | |
| 62 | test("an unreadable time is overdue and never", () => { | |
| 63 | expect(backupOverdue("garbage", NOW)).toBe(true); | |
| 64 | expect(backupAge("garbage", NOW)).toBe("never"); | |
| 65 | }); | |
| 66 | test("just under a day is not overdue, just over is", () => { | |
| 67 | expect(backupOverdue(ago(24 * H - 1), NOW)).toBe(false); | |
| 68 | expect(backupOverdue(ago(24 * H), NOW)).toBe(false); | |
| 69 | expect(backupOverdue(ago(24 * H + 1), NOW)).toBe(true); | |
| 70 | expect(backupOverdue(ago(0), NOW)).toBe(false); | |
| 71 | }); | |
| 72 | test("age words at zero, one, and each unit edge", () => { | |
| 73 | expect(backupAge(ago(0), NOW)).toBe("just now"); | |
| 74 | expect(backupAge(ago(59_999), NOW)).toBe("just now"); | |
| 75 | expect(backupAge(ago(60_000), NOW)).toBe("1 min ago"); | |
| 76 | expect(backupAge(ago(59 * 60_000), NOW)).toBe("59 min ago"); | |
| 77 | expect(backupAge(ago(H), NOW)).toBe("1 h ago"); | |
| 78 | expect(backupAge(ago(47 * H + 59 * 60_000), NOW)).toBe("47 h ago"); | |
| 79 | expect(backupAge(ago(48 * H), NOW)).toBe("2 d ago"); | |
| 80 | }); | |
| 81 | test("a backup dated in the future reads just now, never negative", () => { | |
| 82 | const future = new Date(NOW.getTime() + 5 * H).toISOString(); | |
| 83 | expect(backupAge(future, NOW)).toBe("just now"); | |
| 84 | clean(backupAge(future, NOW)); | |
| 85 | }); | |
| 86 | }); | |
| 87 | ||
| 88 | describe("speedWords", () => { | |
| 89 | test("none is an em dash", () => { | |
| 90 | expect(speedWords(null)).toBe("—"); | |
| 91 | }); | |
| 92 | test("zero, one, and the second edge", () => { | |
| 93 | expect(speedWords(0)).toBe("0 ms"); | |
| 94 | expect(speedWords(1)).toBe("1 ms"); | |
| 95 | expect(speedWords(850)).toBe("850 ms"); | |
| 96 | expect(speedWords(999)).toBe("999 ms"); | |
| 97 | expect(speedWords(1000)).toBe("1.0 s"); | |
| 98 | expect(speedWords(1234)).toBe("1.2 s"); | |
| 99 | }); | |
| 100 | test("a tiny negative rounds to a plain 0, not -0", () => { | |
| 101 | clean(speedWords(-0.4)); | |
| 102 | expect(speedWords(-0.4)).toBe("0 ms"); | |
| 103 | }); | |
| 104 | }); | |
| 105 | ||
| 106 | describe("reach words", () => { | |
| 107 | test("zero and no data", () => { | |
| 108 | expect(usersValue(NO_REACH)).toBe("0 of 0"); | |
| 109 | expect(visitorsValue(NO_REACH)).toBe("0 today"); | |
| 110 | expect(visitorsNote(NO_REACH)).toBe("0 in 7 days"); | |
| 111 | expect(reactNote(NO_REACH)).toBe("no taps measured yet"); | |
| 112 | }); | |
| 113 | test("one", () => { | |
| 114 | const r = reach({ usersToday: 1, usersTotal: 1, visitorsToday: 1, visitors7d: 1, inpP75: 1 }); | |
| 115 | expect(usersValue(r)).toBe("1 of 1"); | |
| 116 | expect(visitorsValue(r)).toBe("1 today"); | |
| 117 | expect(visitorsNote(r)).toBe("1 in 7 days"); | |
| 118 | expect(reactNote(r)).toBe("reacts in 1 ms"); | |
| 119 | }); | |
| 120 | test("many, and react speed crossing a second", () => { | |
| 121 | const r = reach({ usersToday: 3, usersTotal: 12, visitorsToday: 5, visitors7d: 31, inpP75: 180 }); | |
| 122 | expect(usersValue(r)).toBe("3 of 12"); | |
| 123 | expect(visitorsValue(r)).toBe("5 today"); | |
| 124 | expect(visitorsNote(r)).toBe("31 in 7 days"); | |
| 125 | expect(reactNote(r)).toBe("reacts in 180 ms"); | |
| 126 | expect(reactNote(reach({ inpP75: 1500 }))).toBe("reacts in 1.5 s"); | |
| 127 | }); | |
| 128 | test("none of them is dirty", () => { | |
| 129 | for (const r of [NO_REACH, reach({ inpP75: 0 })]) { | |
| 130 | for (const s of [usersValue(r), visitorsValue(r), visitorsNote(r), reactNote(r)]) clean(s); | |
| 131 | } | |
| 132 | }); | |
| 133 | }); | |
| 134 | ||
| 135 | describe("buildersValue", () => { | |
| 136 | test("no slots is an em dash, otherwise running of slots", () => { | |
| 137 | expect(buildersValue({ running: 0, slots: 0 })).toBe("—"); | |
| 138 | expect(buildersValue({ running: 0, slots: 1 })).toBe("0 of 1"); | |
| 139 | expect(buildersValue({ running: 1, slots: 1 })).toBe("1 of 1"); | |
| 140 | expect(buildersValue({ running: 2, slots: 4 })).toBe("2 of 4"); | |
| 141 | }); | |
| 142 | }); |
new above
offline