// Render smoke test for the GitHub app — the regression that motivated it: // the auto-load fired mid-render and hit `paint`'s temporal dead zone, threw // before the busy-reset, and wedged the app on "loading…" forever (every // later Refresh silently swallowed). Fold tests can't see render, so this // drives it twice over a stub DOM and proves the fetch path both RUNS and // RELEASES. node test/github-render.test.mjs import assert from "node:assert/strict"; // A just-enough DOM: every element accepts children, classes, listeners. function fakeEl(tag = "div") { return { tagName: tag, children: [], textContent: "", className: "", hidden: false, disabled: false, value: "", placeholder: "", type: "", onclick: null, ondblclick: null, appendChild(c) { this.children.push(c); return c; }, append(...cs) { this.children.push(...cs); }, addEventListener() {}, querySelector() { return fakeEl(); }, classList: { add() {}, remove() {}, toggle() {} }, focus() {}, scrollIntoView() {}, }; } globalThis.document = { createElement: (tag) => fakeEl(tag), getElementById: () => null, // injectStyles' dedupe check head: fakeEl("head"), activeElement: null, body: null, }; globalThis.window = globalThis; const { app } = await import("../web/static/apps/github.js"); let failures = 0; async function test(name, fn) { try { await fn(); console.log(`PASS ${name}`); } catch (e) { failures++; console.log(`FAIL ${name} — ${e.message.split("\n")[0]}`); } } const tick = () => new Promise((r) => setTimeout(r, 0)); function ctxWith(requestOp) { const state = { repos: [], fetchedAt: 0, issues: {} }; return { threadId: "t1", host: { connected: { requestOp } }, getState: () => state, emit: () => {}, onCleanup: () => {}, id: () => "id", }; } await test("render survives the auto-load and RELEASES busy for the next fetch", async () => { let calls = 0; const ctx = ctxWith(async () => { calls++; return { ok: false, error: "stub" }; }); // First render auto-fetches (fetchedAt 0). A throw anywhere in that path — // the shipped TDZ bug — would reject unhandled and wedge the module `busy`. app.render(undefined, fakeEl(), ctx); await tick(); await tick(); assert.equal(calls, 1, "auto-load ran"); // Second render must be able to fetch again: a wedged busy flag (the bug's // signature) would swallow this silently and calls would stay at 1. app.render(undefined, fakeEl(), ctx); await tick(); await tick(); assert.equal(calls, 2, "busy was released — the app can fetch again"); }); await test("a requestOp that THROWS still lands in the error path and releases", async () => { let calls = 0; const ctx = ctxWith(async () => { calls++; throw new Error("boom"); }); app.render(undefined, fakeEl(), ctx); await tick(); await tick(); app.render(undefined, fakeEl(), ctx); await tick(); await tick(); assert.equal(calls, 2, "a thrown fetch must not wedge the app"); }); console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); process.exit(failures === 0 ? 0 : 1);