184 lines · 6.7 KB · TypeScript
RawDownload
/** * 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<string, unknown>(  BROWSER_GLOBALS.map((key) => [key, (globalThis as Record<string, unknown>)[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<string, unknown>)[key];    else (globalThis as Record<string, unknown>)[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<string, string>();   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<string, unknown>;  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: "<!doctype html>" }) });    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);  });});