/** * The panel that stands in for the sign-in form when the API is down must not * be a sign-in form. * * The whole reason the front page used to throw itself away during an outage * was the reasoning in router.ts: a form that cannot submit is worse than no * form, so rather than show one the router showed the error screen instead — * and took the headline, the pitch and the three facts down with it, none of * which ever asked the API for anything. * * Keeping the page therefore only works while the panel keeps the other half * of that bargain. A field added here later would collect a handle and fail * with it, silently and on the front page, so this asserts the absence: no * form, no input, and an alert a screen reader is actually told about. * * A real test rather than a source read, which outage-panel.ts is written to * allow: it imports ./dom and nothing else, so the fake below is enough. Its * neighbours in the hero cannot be tested this way — chrome.ts asks the API * for its version at import time, which is right in a browser and wrong here. * * The module under test is TypeScript with extensionless imports, so this * bundles it the way profile.test.mjs does. Run with `npm test`. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; // The same shape dom.test.mjs uses, plus the listener el() never touches and // this panel's button does. class FakeElement { constructor(tag) { this.tagName = tag.toUpperCase(); this.attributes = {}; this.children = []; this.listeners = {}; } setAttribute(name, value) { this.attributes[name] = value; } append(...nodes) { this.children.push(...nodes); } addEventListener(type, handler) { this.listeners[type] = handler; } /** Every element in the tree, this one included. */ *walk() { yield this; for (const child of this.children) if (child.walk) yield* child.walk(); } } globalThis.document = { createElement: (tag) => new FakeElement(tag) }; const run = promisify(execFile); const web = join(new URL(".", import.meta.url).pathname, ".."); const dir = await mkdtemp(join(tmpdir(), "outage-panel-")); const bundle = join(dir, "fixture.mjs"); await run( join(web, "node_modules/.bin/rolldown"), [ join(web, "scripts/outage-panel.fixture.ts"), "--format", "esm", "--platform", "node", "--file", bundle, ], { cwd: web }, ); const { outagePanel } = await import(bundle); /** The message api.ts writes when fetch itself could not reach the API. */ const MESSAGE = "Cannot reach the control plane."; test("the panel asks for nothing", () => { const panel = outagePanel(MESSAGE, () => {}); const tags = [...panel.walk()].map((node) => node.tagName); for (const asking of ["FORM", "INPUT", "SELECT", "TEXTAREA"]) { assert.ok( !tags.includes(asking), `the outage panel grew a <${asking.toLowerCase()}> — whatever stopped ` + `/api/session will stop the submit too`, ); } }); test("the message is announced, not just printed", () => { const panel = outagePanel(MESSAGE, () => {}); // A property and not an attribute, because `role` has no hyphen and el() // splits on that — which is a real assignment in a browser, where role is // reflected. See dom.test.mjs for why that split exists at all. const alert = [...panel.walk()].find((node) => node.role === "alert"); assert.ok( alert, "nothing on the panel is an alert — a player who pressed Sign in and got " + "this panel back is told nothing", ); assert.equal(alert.textContent, MESSAGE); }); test("try again runs the retry, once per press", () => { let asked = 0; const panel = outagePanel(MESSAGE, () => { asked += 1; }); const button = [...panel.walk()].find((node) => node.tagName === "BUTTON"); assert.ok(button, "the panel has no way out of the outage"); // type="button" and not a submit: there is no form, and a bare