import { afterEach, beforeEach, test } from "node:test"; import assert from "node:assert/strict"; import { __resetRegistryForTests, getBuiltinsLoaded, getRegisteredSites, loadBuiltins, subscribeRegistry, } from "./siteRegistry"; // ─── the built-ins' "loaded" flag ─── // // The hub's /ask counts as ready only once `/hub-sites.json` has been ANSWERED: // before that, the list may hold only the archives this browser added, and a // question would ground in half the hub. So the flag must turn true for every // answer — a list, an empty list, a 404, a network error, bad JSON — and never // before the built-ins are in the list (release 11, O1). const A = { siteId: "a", siteTitle: "Archive A", siteUrl: "https://a.example.com", pwa: true, contract: 1, }; type Answer = { status: number; body: string } | "throw"; let restore: (() => void) | null = null; // A client-side module: it only loads with a `window` to load into. function install(answer: Answer): { release: () => void; asked: string[] } { const asked: string[] = []; const realFetch = globalThis.fetch; const hadWindow = "window" in globalThis; (globalThis as { window?: unknown }).window = globalThis; let release!: () => void; const gate = new Promise((r) => (release = r)); globalThis.fetch = (async (input: RequestInfo | URL) => { asked.push(String(input)); await gate; if (answer === "throw") throw new TypeError("Failed to fetch"); return new Response(answer.body, { status: answer.status }); }) as typeof fetch; restore = () => { globalThis.fetch = realFetch; if (!hadWindow) delete (globalThis as { window?: unknown }).window; }; return { release, asked }; } beforeEach(() => __resetRegistryForTests()); afterEach(() => { restore?.(); restore = null; __resetRegistryForTests(); }); test("a listed built-in: loaded turns true only once it is in the list, in one update", async () => { const { release, asked } = install({ status: 200, body: JSON.stringify([A]) }); const seen: { loaded: boolean; origins: string[] }[] = []; const unsubscribe = subscribeRegistry(() => seen.push({ loaded: getBuiltinsLoaded(), origins: getRegisteredSites().map((s) => s.origin), }), ); const pending = loadBuiltins(); assert.equal(getBuiltinsLoaded(), false, "not loaded while the file is in flight"); release(); await pending; unsubscribe(); assert.deepEqual(asked, ["/hub-sites.json"]); assert.equal(getBuiltinsLoaded(), true); assert.deepEqual( getRegisteredSites().map((s) => [s.origin, s.kind]), [["https://a.example.com", "builtin"]], ); // One notification, and it already carries the built-in: no reader sees // "loaded" over a list that lacks it. assert.deepEqual(seen, [{ loaded: true, origins: ["https://a.example.com"] }]); }); for (const [name, answer] of [ ["an empty list ([])", { status: 200, body: "[]" }], ["a 404 (no hub-sites.json)", { status: 404, body: "" }], ["a 500", { status: 500, body: "" }], ["a network error", "throw"], ["unreadable JSON", { status: 200, body: "{not json" }], ["JSON that is not a list", { status: 200, body: '{"sites":[]}' }], ] as [string, Answer][]) { test(`${name}: loaded turns true with no built-ins`, async () => { const { release } = install(answer); let notified = 0; const unsubscribe = subscribeRegistry(() => notified++); const pending = loadBuiltins(); assert.equal(getBuiltinsLoaded(), false); release(); await pending; unsubscribe(); assert.equal(getBuiltinsLoaded(), true); assert.deepEqual(getRegisteredSites(), []); // Subscribers hear it even though the list did not change. assert.equal(notified, 1); }); } test("it is asked for once: a second load neither refetches nor re-notifies", async () => { const { release, asked } = install({ status: 200, body: JSON.stringify([A]) }); release(); await loadBuiltins(); let notified = 0; const unsubscribe = subscribeRegistry(() => notified++); await loadBuiltins(); unsubscribe(); assert.equal(asked.length, 1); assert.equal(notified, 0); assert.equal(getBuiltinsLoaded(), true); });