// @vitest-environment happy-dom import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createSpawnForm, type SpawnFormHandle } from "./spawnForm.js"; const BATCH_A = ["allegro-cello", "presto-fugue", "adagio-lyre"]; const BATCH_B = ["forte-oboe", "largo-tuba", "vivace-harp"]; /** Stub the three fetches the form's initial load makes. Records every * /api/names URL so tests can assert how many batches were fetched. */ function stubFetch(batches: string[][]): string[] { let batch = 0; const nameUrls: string[] = []; vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url.includes("/api/names")) { nameUrls.push(url); const names = batches[Math.min(batch, batches.length - 1)]; batch += 1; return new Response(JSON.stringify({ names }), { status: 200 }); } const body = url.includes("/api/info") ? { hostname: "h", version: "0", home_dir: "/home/user", presets: [] } : []; return new Response(JSON.stringify(body), { status: 200 }); }), ); return nameUrls; } describe("spawnForm name batch (#53)", () => { let handle: SpawnFormHandle | null = null; let nameUrls: string[]; beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { handle?.destroy(); handle = null; vi.unstubAllGlobals(); vi.useRealTimers(); }); async function mount(seed?: Parameters[0]["seed"]) { nameUrls = stubFetch([BATCH_A, BATCH_B]); handle = createSpawnForm({ seed, onSuccess() {} }); document.body.appendChild(handle.el); for (let i = 0; i < 40; i++) await Promise.resolve(); return handle.el; } const nameOf = (form: HTMLFormElement) => form.querySelector("input[name=name]")!.value; function reroll(form: HTMLFormElement): void { form.querySelector(".reroll")!.click(); } async function drain(): Promise { for (let i = 0; i < 40; i++) await Promise.resolve(); } it("seeds its name from a fetched batch when no seed provides one", async () => { const form = await mount(); expect(BATCH_A).toContain(nameOf(form)); expect(nameUrls).toHaveLength(1); expect(nameUrls[0]).toContain("/api/names"); expect(nameUrls[0]).toContain("count="); }); it("keeps a seeded name and defers fetching until the first reroll", async () => { const form = await mount({ name: "my-session" }); expect(nameOf(form)).toBe("my-session"); expect(nameUrls ?? []).toHaveLength(0); reroll(form); await drain(); expect(nameOf(form)).not.toBe("my-session"); expect(nameUrls).toHaveLength(1); }); it("rerolls locally from the batch with no extra round trips", async () => { const form = await mount(); expect(nameUrls).toHaveLength(1); const seen = new Set([nameOf(form)]); for (let i = 0; i < BATCH_A.length; i++) { reroll(form); seen.add(nameOf(form)); } await drain(); // Every value came from the one fetched batch. expect(seen.size).toBeGreaterThanOrEqual(2); for (const value of seen) { expect([...BATCH_A, ...BATCH_B]).toContain(value); } expect(nameUrls).toHaveLength(2); // refetch fired when the batch ran out expect(nameUrls[1]).toContain("/api/names"); }); it("refetches only once even if rerolls outpace the network", async () => { const form = await mount(); // Drain the batch without awaiting between rerolls. for (let i = 0; i < BATCH_A.length + 1; i++) reroll(form); await drain(); // One refill in flight covers the whole run — no fetch storm. expect(nameUrls.length).toBeLessThanOrEqual(2); }); it("does not clobber an operator-typed name when the batch arrives late", async () => { nameUrls = stubFetch([BATCH_A]); handle = createSpawnForm({ onSuccess() {} }); const form = handle.el; document.body.appendChild(form); // The operator types before the fetch resolves. const input = form.querySelector("input[name=name]")!; input.value = "hand-typed"; input.dispatchEvent(new Event("input")); for (let i = 0; i < 40; i++) await Promise.resolve(); expect(nameOf(form)).toBe("hand-typed"); }); });