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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6906-a-test-pins-the-home-s-kpi-words-spend-b → main 1 commit fork

  • Merged. scout/6906-a-test-pins-the-home-s-kpi-words-spend-b was merged into main as bd360a4.

Review

Sign in to review this change.
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs