Skip to content
222 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { StatusIncident, StatusMaintenance } from "@g1t/contracts";
5
6import { components } from "./components.ts";
7import { type DayRow, buildPage } from "./model.ts";
8import {
9 THEME_KEY,
10 THEME_SCRIPT,
11 byMonth,
12 dayDetail,
13 escape,
14 prose,
15 renderBadge,
16 renderHistory,
17 renderIncident,
18 renderMaintenance,
19 renderMessage,
20 renderPage,
21 renderSubscribe,
22} from "./render.ts";
23
24const NOW = new Date("2026-10-05T12:00:00Z");
25const OPTIONS = { siteUrl: "https://g1t.sh", supportUrl: "https://g1t.sh/support", ogImage: "", selfUrl: "https://status.g1t.sh/", now: NOW, email: true };
26const parts = components(
27 {
28 SITE_URL: "https://g1t.sh",
29 API_URL: "https://api.g1t.sh",
30 DOCS_URL: "https://docs.g1t.sh",
31 MODELS_URL: "https://models.g1t.sh",
32 PROBE_REPO: "flagon-io/g1t",
33 },
34 false,
35);
36
37function page(states: Record<string, "up" | "degraded" | "down">, incidents: StatusIncident[] = [], maintenance: StatusMaintenance[] = []) {
38 const current = new Map(
39 Object.entries(states).map(([k, state]) => [k, { state, detail: state === "down" ? "Failed: HTTP 502" : "Answered in 80 ms", latency_ms: 80 }]),
40 );
41 const days: DayRow[] = [
42 { component: "api", day: "2026-10-05", checks: 720, up: 719, degraded: 0, down: 1, latency_total: 71900, latency_count: 719 },
43 ];
44 return buildPage({ parts, current, checkedAt: "2026-10-05T11:59:40Z", days, incidents, maintenance, now: NOW });
45}
46
47const INCIDENT: StatusIncident = {
48 id: "abc",
49 title: "Sign-in <failing>",
50 impact: "down",
51 status: "identified",
52 components: ["site"],
53 component_impacts: [{ key: "site", impact: "major_outage" }],
54 started_at: "2026-10-05T11:40:00Z",
55 resolved_at: null,
56 url: "https://status.g1t.sh/incidents/abc",
57 postmortem_published_at: null,
58 updates: [{ id: "u1", at: "2026-10-05T11:45:00Z", status: "identified", text: "A bad deploy.\n\nRolling back." }],
59};
60
61const MAINT: StatusMaintenance = {
62 id: "m1",
63 title: "Database upgrade",
64 message: "Pushes pause for up to five minutes.",
65 components: ["git"],
66 starts_at: "2026-10-06T02:00:00Z",
67 ends_at: "2026-10-06T03:00:00Z",
68 state: "scheduled",
69 url: "https://status.g1t.sh/maintenance/m1",
70 updates: [{ id: "mu1", at: "2026-10-05T10:00:00Z", text: "Pushes pause for up to five minutes." }],
71};
72
73test("escape covers HTML's specials", () => {
74 assert.equal(escape(`<a href="x">'&'</a>`), "&lt;a href=&quot;x&quot;&gt;&#39;&amp;&#39;&lt;/a&gt;");
75});
76
77test("the page: banner, every part with its bar, and no incidents", () => {
78 const html = renderPage(page({ site: "up", api: "up", git: "up", docs: "up", agents: "up" }), OPTIONS);
79 assert.match(html, /<h1>All systems normal<\/h1>/);
80 for (const name of ["Website and sign-in", "API", "Git and repositories", "Documentation", "Sandboxes"]) assert.ok(html.includes(name), name);
81 assert.equal((html.match(/<ol class="bar"/g) ?? []).length, 5, "Sandboxes has no bar");
82 assert.ok(html.includes("No incidents in the last 14 days"));
83 assert.ok(html.includes('action="/subscribe"'), "a subscribe form");
84 assert.ok(html.includes('href="/feed.xml"'));
85 assert.match(html, /99\.86% uptime/);
86 assert.match(html, /aria-label="g1t"/);
87 assert.doesNotMatch(html, /Happening now/);
88});
89
90test("an outage and an open incident are said at the top, escaped", () => {
91 const html = renderPage(
92 page({ site: "down", api: "up", git: "up", docs: "up", agents: "up" }, [
93 INCIDENT,
94 ]),
95 OPTIONS,
96 );
97 assert.match(html, /<h1>Major outage<\/h1>/);
98 assert.match(html, /Happening now/);
99 assert.ok(html.includes("Sign-in &lt;failing&gt;"));
100 assert.ok(!html.includes("<failing>"));
101 assert.match(html, /<p>A bad deploy\.<\/p><p>Rolling back\.<\/p>/);
102 assert.match(html, /Identified · Major outage/);
103 assert.ok(html.includes('href="/incidents/abc"'), "the title links to its page");
104});
105
106test("every page follows the system's light or dark setting, with a switch kept in this browser", () => {
107 for (const html of [
108 renderPage(page({ site: "up" }), OPTIONS),
109 renderHistory([], [], OPTIONS),
110 renderMessage(OPTIONS, { title: "Not found", text: "Nothing here." }),
111 ]) {
112 const head = html.slice(0, html.indexOf("</head>"));
113 // The choice is applied by a script that blocks in the head, before anything is drawn.
114 assert.match(head, /<script src="\/theme\.js"><\/script>/);
115 assert.match(head, /<meta name="color-scheme" content="dark light">/);
116 assert.match(head, /theme-color" content="#fbfbfa" media="\(prefers-color-scheme: light\)"/);
117 // Dark is the base; light on the system's say-so unless Dark was chosen, or when Light was.
118 assert.match(head, /:root\{--bg:#0f0f11;/);
119 assert.match(head, /@media \(prefers-color-scheme:light\)\{:root:not\(\[data-theme=dark\]\)\{--bg:#fbfbfa;/);
120 assert.match(head, /:root\[data-theme=light\]\{--bg:#fbfbfa;/);
121 for (const choice of ["auto", "light", "dark"]) assert.ok(html.includes(`data-theme-choice="${choice}"`), choice);
122 }
123 assert.ok(THEME_SCRIPT.includes(JSON.stringify(THEME_KEY)));
124 assert.ok(!/<script>/.test(renderPage(page({ site: "up" }), OPTIONS)), "no inline script: the page's policy allows only its own files");
125});
126
127test("the theme script picks the stored choice, else auto", () => {
128 for (const [stored, expected] of [["light", "light"], ["dark", "dark"], [null, "auto"], ["purple", "auto"]] as const) {
129 const root = { dataset: {} as Record<string, string> };
130 const run = new Function("localStorage", "document", THEME_SCRIPT);
131 run({ getItem: (key: string) => (key === THEME_KEY ? stored : null) }, { documentElement: root });
132 assert.equal(root.dataset.theme, expected, String(stored));
133 }
134 // A browser that refuses storage still gets a page.
135 const root = { dataset: {} as Record<string, string> };
136 new Function("localStorage", "document", THEME_SCRIPT)({ getItem: () => { throw new Error("denied"); } }, { documentElement: root });
137 assert.equal(root.dataset.theme, "auto");
138});
139
140test("a stale page says the checks stopped", () => {
141 const model = page({ api: "up" });
142 const html = renderPage({ ...model, stale: true }, OPTIONS);
143 assert.match(html, /have not run since/);
144});
145
146test("a day's detail names what happened", () => {
147 assert.equal(
148 dayDetail({ day: "2026-10-05", state: "up", uptime: 99.86, checks: 720, failed: 1, slow: 0, avg_ms: 100, incidents: ["Pushes failing"] }),
149 "5 Oct 2026 · 99.86% answered · 1 failed check of 720 · average 100 ms · Incident: Pushes failing",
150 );
151 assert.equal(
152 dayDetail({ day: "2026-10-01", state: "none", uptime: null, checks: 0, failed: 0, slow: 0, avg_ms: null, incidents: [] }),
153 "1 Oct 2026 · No data",
154 );
155});
156
157test("the badge is an SVG coloured by state", () => {
158 const svg = renderBadge("degraded", "Partial outage");
159 assert.match(svg, /^<svg/);
160 assert.match(svg, /#ffbd8c/);
161 assert.match(svg, /partial outage/);
162});
163
164test("maintenance: upcoming, then in progress with its parts under maintenance", () => {
165 const ahead = renderPage(page({ site: "up", api: "up", git: "up" }, [], [MAINT]), OPTIONS);
166 assert.ok(ahead.includes("Upcoming maintenance") && ahead.includes("Database upgrade") && !ahead.includes("Maintenance in progress"));
167 const during = { ...OPTIONS, now: new Date("2026-10-06T02:30:00Z") };
168 const model = buildPage({
169 parts,
170 current: new Map([["git", { state: "down" as const, detail: "Failed: HTTP 503", latency_ms: null }], ["site", { state: "up" as const, detail: "ok", latency_ms: 1 }]]),
171 checkedAt: "2026-10-06T02:29:40Z",
172 days: [],
173 incidents: [],
174 maintenance: [MAINT],
175 now: during.now,
176 });
177 const html = renderPage(model, during);
178 assert.ok(html.includes("Maintenance in progress"));
179 assert.ok(html.includes("<h1>Under maintenance</h1>"));
180 assert.ok(html.includes('class="state s-maintenance"'));
181});
182
183test("an incident's page: every update, its parts' impact, and the postmortem once published", () => {
184 const names = new Map([["site", "Website and sign-in"]]);
185 const resolved = { ...INCIDENT, status: "resolved" as const, resolved_at: "2026-10-05T12:40:00Z", postmortem_published_at: "2026-10-06T09:00:00Z" };
186 const pm = { summary: "A deploy broke sign-in.", impact: "", timeline: "- 11:40 declared\n- 12:40 resolved", root_cause: "A <bad> cookie.", went_well: "", went_badly: "", action_items: "- Canary deploys", published_at: "2026-10-06T09:00:00Z" };
187 const html = renderIncident(resolved, pm, names, { ...OPTIONS, selfUrl: resolved.url });
188 assert.ok(html.includes('id="update-u1"'));
189 assert.ok(html.includes("Website and sign-in"));
190 assert.ok(html.includes("Major outage"));
191 assert.ok(html.includes('id="postmortem"'));
192 assert.ok(html.includes("<h3>Root cause</h3>") && html.includes("A &lt;bad&gt; cookie."));
193 assert.ok(!html.includes("<h3>Impact</h3>"), "empty sections are left out");
194 assert.ok(html.includes("<ul><li>Canary deploys</li></ul>"));
195 assert.ok(html.includes("lasted 1h 00m"));
196 const draft = renderIncident(resolved, null, names, OPTIONS);
197 assert.ok(!draft.includes('id="postmortem"') && draft.includes("If a postmortem is written"));
198});
199
200test("prose: paragraphs and lists, escaped", () => {
201 assert.equal(prose("One\ntwo\n\n- a\n- <b>"),"<p>One<br>two</p><ul><li>a</li><li>&lt;b&gt;</li></ul>");
202});
203
204test("history: months newest first, empty ones said so", () => {
205 const old = { ...INCIDENT, id: "old", started_at: "2026-08-10T00:00:00Z", resolved_at: "2026-08-10T01:00:00Z" };
206 const groups = byMonth([INCIDENT, old], [MAINT]);
207 assert.deepEqual(groups.map((g) => g.month), ["2026-10", "2026-08"]);
208 const html = renderHistory([INCIDENT, old], [MAINT], OPTIONS, 3);
209 assert.ok(html.includes("October 2026") && html.includes("September 2026") && html.includes("August 2026"));
210 assert.ok(html.includes("No incidents or maintenance."));
211 assert.ok(html.indexOf("October 2026") < html.indexOf("August 2026"));
212 assert.ok(html.includes('href="/maintenance/m1"'));
213});
214
215test("subscribe pages: the form, its parts, and a confirm button rather than a link that acts", () => {
216 const html = renderSubscribe(OPTIONS, [{ key: "git", name: "Git" }]);
217 assert.ok(html.includes('name="components" value="git"') && html.includes('name="website"'));
218 assert.ok(renderSubscribe({ ...OPTIONS, email: false }, []).includes("not available"));
219 const confirm = renderMessage(OPTIONS, { title: "Confirm", text: "One more step", form: { action: "/subscribe/confirm", fields: { token: 'a"b' }, button: "Confirm subscription" } });
220 assert.ok(confirm.includes('method="post" action="/subscribe/confirm"') && confirm.includes('value="a&quot;b"'));
221 assert.ok(renderMaintenance(MAINT, new Map([["git", "Git"]]), OPTIONS).includes("Affects Git"));
222});