/** * The recovery policy is tested against a fake environment in * preload-error.test.ts; this covers the environment the browser actually gets, * because a policy fed the wrong inputs recovers from nothing. The failure this * guards against is silent: a probe that answers backwards, or a reload guard * that never latches, turns "reload once" into a reload loop with every * behavioural test still green. */ import { afterEach, describe, expect, it } from "@effect/vitest"; import { browserEnvironment } from "./server-disconnected"; const BROWSER_GLOBALS = ["window", "document", "fetch", "reportError"] as const; const originalGlobals = new Map( BROWSER_GLOBALS.map((key) => [key, (globalThis as Record)[key]]), ); afterEach(() => { // The fakes below are installed on the real global object; leaving them there // would silently hand the next test a browser that does not exist. for (const [key, value] of originalGlobals) { if (value === undefined) delete (globalThis as Record)[key]; else (globalThis as Record)[key] = value; } }); class FakeElement { id = ""; textContent = ""; readonly style = { cssText: "" }; readonly children: FakeElement[] = []; append(...nodes: FakeElement[]): void { this.children.push(...nodes); } byId(id: string): FakeElement | null { if (this.id === id) return this; for (const child of this.children) { const found = child.byId(id); if (found) return found; } return null; } get text(): string { return [this.textContent, ...this.children.map((child) => child.text)].join(" ").trim(); } } interface FetchCall { readonly input: unknown; readonly init: RequestInit | undefined; } const environment = (options: { readonly respond?: () => Promise<{ ok: boolean; body: string }>; readonly storage?: "working" | "blocked"; }) => { const body = new FakeElement(); const fetches: FetchCall[] = []; const reloads = { count: 0 }; const reported: unknown[] = []; const store = new Map(); const sessionStorage = { getItem: (key: string) => store.get(key) ?? null, setItem: (key: string, value: string) => store.set(key, value), }; const fakeWindow = { addEventListener: () => {}, removeEventListener: () => {}, location: { reload: () => { reloads.count += 1; }, }, }; if (options.storage === "blocked") { Object.defineProperty(fakeWindow, "sessionStorage", { get: () => { // oxlint-disable-next-line executor/no-error-constructor, executor/no-try-catch-or-throw -- boundary: reproduces the SecurityError a browser raises when storage is blocked throw new Error("SecurityError: storage is disabled"); }, }); } else { Object.defineProperty(fakeWindow, "sessionStorage", { get: () => sessionStorage }); } const globals = globalThis as Record; globals.window = fakeWindow; globals.document = { body, createElement: () => new FakeElement(), getElementById: (id: string) => body.byId(id), }; globals.fetch = async (input: unknown, init?: RequestInit) => { fetches.push({ input, init }); const answer = await (options.respond?.() ?? Promise.resolve({ ok: true, body: "ok" })); return { ok: answer.ok, text: async () => answer.body }; }; globals.reportError = (error: unknown) => reported.push(error); return { env: browserEnvironment(), body, fetches, reloads, reported, store }; }; describe("browserEnvironment", () => { it("probes the unauthenticated health endpoint and believes only a healthy answer", async () => { const healthy = environment({ respond: async () => ({ ok: true, body: "ok\n" }) }); expect(await healthy.env.probeServer()).toBe(true); expect(healthy.fetches).toHaveLength(1); expect(healthy.fetches[0]?.input).toBe("/api/health"); // A cached 200 from before the server died would report it as alive. expect(healthy.fetches[0]?.init?.cache).toBe("no-store"); const wrongBody = environment({ respond: async () => ({ ok: true, body: "" }) }); expect(await wrongBody.env.probeServer()).toBe(false); const failing = environment({ respond: async () => ({ ok: false, body: "" }) }); expect(await failing.env.probeServer()).toBe(false); }); it("reads a refused connection as a down server rather than propagating", async () => { const down = environment({ respond: async () => { // oxlint-disable-next-line executor/no-error-constructor, executor/no-try-catch-or-throw -- boundary: fetch rejects when the origin is gone, which is the signal under test throw new TypeError("Failed to fetch"); }, }); expect(await down.env.probeServer()).toBe(false); }); it("shows one reconnect overlay however many times it is asked", () => { const { env, body } = environment({}); env.showDisconnected(); env.showDisconnected(); const overlays = body.children.filter((child) => child.id === "executor-server-disconnected"); expect(overlays).toHaveLength(1); expect(overlays[0]?.text).toContain("Lost connection to the Executor server"); expect(overlays[0]?.text).toContain("RECONNECTING"); }); it("latches the one-reload-per-session guard in storage", () => { const { env, store } = environment({}); expect(env.readReloadFlag()).toBe(false); env.writeReloadFlag(); expect(env.readReloadFlag()).toBe(true); expect([...store.keys()]).toEqual(["executor:preload-reloaded"]); }); it("reports rather than reloads when storage is unavailable", () => { // Without a place to record the reload, an auto-reload could repeat // forever; reading the flag as already spent degrades to reporting once. const { env } = environment({ storage: "blocked" }); expect(env.readReloadFlag()).toBe(true); }); it("reloads the page and reports through the shared handled-error reporter", () => { const { env, reloads, reported } = environment({}); // oxlint-disable-next-line executor/no-error-constructor -- test boundary: stands in for the chunk TypeError const failure = new TypeError("Failed to fetch dynamically imported module"); env.reload(); env.report(failure); expect(reloads.count).toBe(1); // Reporting the raw TypeError would file this as an unhandled crash with no // surface attached; it must arrive as the normalized handled-error envelope. expect(reported).toHaveLength(1); const report = reported[0] as { readonly context?: unknown; readonly cause?: unknown }; expect(report.context).toMatchObject({ surface: "renderer", action: "load-route-chunk", }); expect(report.cause).toBe(failure); }); });