Blame · Line-by-line history
inbox.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 5d14510 | 1 | /** |
| 2 | * Tests for the pure helpers exported from src/routes/inbox.tsx — | |
| 3 | * `mergeAndCapInboxRows` and `filterInboxRows`. The route file is a | |
| 4 | * .tsx module; if the test sandbox can't resolve the jsx-dev-runtime | |
| 5 | * we defer (same defensive pattern as dashboard-coach.test.ts). | |
| 6 | */ | |
| 7 | ||
| 8 | import { describe, it, expect } from "bun:test"; | |
| 9 | ||
| 10 | async function tryLoad(): Promise< | |
| 11 | | { | |
| 12 | ok: true; | |
| 13 | mergeAndCapInboxRows: any; | |
| 14 | filterInboxRows: any; | |
| 15 | } | |
| 16 | | { ok: false; reason: "jsx-dev-runtime" | "other"; err: Error } | |
| 17 | > { | |
| 18 | try { | |
| 19 | const mod: any = await import("../routes/inbox"); | |
| 20 | return { | |
| 21 | ok: true, | |
| 22 | mergeAndCapInboxRows: mod.mergeAndCapInboxRows, | |
| 23 | filterInboxRows: mod.filterInboxRows, | |
| 24 | }; | |
| 25 | } catch (err) { | |
| 26 | const e = err instanceof Error ? err : new Error(String(err)); | |
| 27 | const reason = /jsx[-/]dev[-/]?runtime/i.test(e.message) | |
| 28 | ? "jsx-dev-runtime" | |
| 29 | : "other"; | |
| 30 | return { ok: false, reason, err: e }; | |
| 31 | } | |
| 32 | } | |
| 33 | ||
| 34 | const row = (id: string, kind: string, createdAt: Date) => | |
| 35 | ({ | |
| 36 | id, | |
| 37 | kind, | |
| 38 | title: `t-${id}`, | |
| 39 | sourceText: `s-${id}`, | |
| 40 | sourceUrl: `/u/${id}`, | |
| 41 | createdAt, | |
| 42 | }) as any; | |
| 43 | ||
| 44 | describe("mergeAndCapInboxRows", () => { | |
| 45 | it("merges multiple source arrays and sorts by timestamp desc", async () => { | |
| 46 | const loaded = await tryLoad(); | |
| 47 | if (!loaded.ok) { | |
| 48 | expect(loaded.reason).toBe("jsx-dev-runtime"); | |
| 49 | return; | |
| 50 | } | |
| 51 | const fn = loaded.mergeAndCapInboxRows; | |
| 52 | const a = [row("a", "mention", new Date("2026-05-25T10:00:00Z"))]; | |
| 53 | const b = [row("b", "review", new Date("2026-05-25T12:00:00Z"))]; | |
| 54 | const c = [row("c", "ci", new Date("2026-05-25T11:00:00Z"))]; | |
| 55 | const out = fn([a, b, c]); | |
| 56 | expect(out.map((r: any) => r.id)).toEqual(["b", "c", "a"]); | |
| 57 | }); | |
| 58 | ||
| 59 | it("tolerates null/undefined sources", async () => { | |
| 60 | const loaded = await tryLoad(); | |
| 61 | if (!loaded.ok) { | |
| 62 | expect(loaded.reason).toBe("jsx-dev-runtime"); | |
| 63 | return; | |
| 64 | } | |
| 65 | const fn = loaded.mergeAndCapInboxRows; | |
| 66 | const a = [row("a", "mention", new Date("2026-05-25T10:00:00Z"))]; | |
| 67 | const out = fn([a, null, undefined, []]); | |
| 68 | expect(out.map((r: any) => r.id)).toEqual(["a"]); | |
| 69 | }); | |
| 70 | ||
| 71 | it("caps to the specified limit", async () => { | |
| 72 | const loaded = await tryLoad(); | |
| 73 | if (!loaded.ok) { | |
| 74 | expect(loaded.reason).toBe("jsx-dev-runtime"); | |
| 75 | return; | |
| 76 | } | |
| 77 | const fn = loaded.mergeAndCapInboxRows; | |
| 78 | const many = Array.from({ length: 250 }, (_, i) => | |
| 79 | row(`r${i}`, "mention", new Date(2026, 4, 25, 0, 0, i)) | |
| 80 | ); | |
| 81 | const out = fn([many], 100); | |
| 82 | expect(out.length).toBe(100); | |
| 83 | // Sorted desc, so r249 should come first. | |
| 84 | expect(out[0].id).toBe("r249"); | |
| 85 | }); | |
| 86 | ||
| 87 | it("defaults the cap to 100", async () => { | |
| 88 | const loaded = await tryLoad(); | |
| 89 | if (!loaded.ok) { | |
| 90 | expect(loaded.reason).toBe("jsx-dev-runtime"); | |
| 91 | return; | |
| 92 | } | |
| 93 | const fn = loaded.mergeAndCapInboxRows; | |
| 94 | const many = Array.from({ length: 200 }, (_, i) => | |
| 95 | row(`r${i}`, "mention", new Date(2026, 4, 25, 0, 0, i)) | |
| 96 | ); | |
| 97 | const out = fn([many]); | |
| 98 | expect(out.length).toBe(100); | |
| 99 | }); | |
| 100 | }); | |
| 101 | ||
| 102 | describe("filterInboxRows", () => { | |
| 103 | const rows = () => [ | |
| 104 | row("m1", "mention", new Date("2026-05-25T01:00:00Z")), | |
| 105 | row("r1", "review", new Date("2026-05-25T02:00:00Z")), | |
| 106 | row("c1", "ci", new Date("2026-05-25T03:00:00Z")), | |
| 107 | row("af1", "ai-finding", new Date("2026-05-25T04:00:00Z")), | |
| 108 | row("am1", "ai-merge", new Date("2026-05-25T05:00:00Z")), | |
| 109 | ]; | |
| 110 | ||
| 111 | it("'all' returns everything", async () => { | |
| 112 | const loaded = await tryLoad(); | |
| 113 | if (!loaded.ok) { | |
| 114 | expect(loaded.reason).toBe("jsx-dev-runtime"); | |
| 115 | return; | |
| 116 | } | |
| 117 | const fn = loaded.filterInboxRows; | |
| 118 | expect(fn(rows(), "all").length).toBe(5); | |
| 119 | }); | |
| 120 | ||
| 121 | it("each single-kind tab returns only that kind", async () => { | |
| 122 | const loaded = await tryLoad(); | |
| 123 | if (!loaded.ok) { | |
| 124 | expect(loaded.reason).toBe("jsx-dev-runtime"); | |
| 125 | return; | |
| 126 | } | |
| 127 | const fn = loaded.filterInboxRows; | |
| 128 | expect(fn(rows(), "mentions").map((r: any) => r.id)).toEqual(["m1"]); | |
| 129 | expect(fn(rows(), "review").map((r: any) => r.id)).toEqual(["r1"]); | |
| 130 | expect(fn(rows(), "ci").map((r: any) => r.id)).toEqual(["c1"]); | |
| 131 | }); | |
| 132 | ||
| 133 | it("'ai' tab covers both ai-finding and ai-merge", async () => { | |
| 134 | const loaded = await tryLoad(); | |
| 135 | if (!loaded.ok) { | |
| 136 | expect(loaded.reason).toBe("jsx-dev-runtime"); | |
| 137 | return; | |
| 138 | } | |
| 139 | const fn = loaded.filterInboxRows; | |
| 140 | const ids = fn(rows(), "ai") | |
| 141 | .map((r: any) => r.id) | |
| 142 | .sort(); | |
| 143 | expect(ids).toEqual(["af1", "am1"]); | |
| 144 | }); | |
| 145 | }); |