132 lines · 3.9 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import {  PRELOAD_ERROR_EVENT,  installPreloadErrorHandler,  respondToPreloadError,  type PreloadErrorEnvironment,} from "./preload-error"; interface Recorder {  readonly env: PreloadErrorEnvironment;  readonly calls: {    probes: number;    disconnected: number;    reloads: number;    reported: unknown[];    flagWrites: number;  };} const recorder = (options: {  readonly serverUp: readonly boolean[];  readonly reloadFlag?: boolean;  readonly target?: EventTarget;}): Recorder => {  const calls = {    probes: 0,    disconnected: 0,    reloads: 0,    reported: [] as unknown[],    flagWrites: 0,  };  let flag = options.reloadFlag ?? false;  return {    calls,    env: {      target: options.target ?? new EventTarget(),      probeServer: async () => {        const answer = options.serverUp[calls.probes] ?? options.serverUp.at(-1) ?? false;        calls.probes += 1;        return answer;      },      showDisconnected: () => {        calls.disconnected += 1;      },      reload: () => {        calls.reloads += 1;      },      report: (error) => {        calls.reported.push(error);      },      readReloadFlag: () => flag,      writeReloadFlag: () => {        flag = true;        calls.flagWrites += 1;      },      delay: async () => {},    },  };}; // oxlint-disable-next-line executor/no-error-constructor -- test boundary: reproduces the exact TypeError the browser hands to vite:preloadErrorconst chunkError = new TypeError(  "Failed to fetch dynamically imported module: http://127.0.0.1:4789/assets/route-abc123.js",); describe("respondToPreloadError", () => {  it("shows the reconnect surface and never reports when the server is unreachable", async () => {    // The sidecar died under a live window: the chunk fetch failing is a    // symptom of that one event, already covered by the sidecar-exit path.    const { env, calls } = recorder({ serverUp: [false, true] });     const outcome = await respondToPreloadError(env, chunkError);     expect(outcome).toBe("reconnected");    expect(calls.disconnected).toBe(1);    expect(calls.reported).toEqual([]);    expect(calls.reloads).toBe(1);    expect(calls.flagWrites).toBe(0);  });   it("reloads exactly once when the server answers and the chunk is genuinely stale", async () => {    const { env, calls } = recorder({ serverUp: [true] });     const outcome = await respondToPreloadError(env, chunkError);     expect(outcome).toBe("reloaded");    expect(calls.reloads).toBe(1);    expect(calls.flagWrites).toBe(1);    expect(calls.disconnected).toBe(0);    expect(calls.reported).toEqual([]);  });   it("reports instead of looping when a reload already happened this session", async () => {    const { env, calls } = recorder({ serverUp: [true], reloadFlag: true });     const outcome = await respondToPreloadError(env, chunkError);     expect(outcome).toBe("reported");    expect(calls.reloads).toBe(0);    expect(calls.reported).toEqual([chunkError]);  });}); describe("installPreloadErrorHandler", () => {  it("prevents the default rejection so a dead chunk is not an unhandled TypeError", () => {    const target = new EventTarget();    const { env, calls } = recorder({ serverUp: [false, true], target });    installPreloadErrorHandler(env);     const event = new CustomEvent(PRELOAD_ERROR_EVENT, {      cancelable: true,      detail: { payload: chunkError },    });    target.dispatchEvent(event);     expect(event.defaultPrevented).toBe(true);    expect(calls.probes).toBe(1);  });   it("stops responding after dispose", () => {    const target = new EventTarget();    const { env, calls } = recorder({ serverUp: [false, true], target });    const dispose = installPreloadErrorHandler(env);    dispose();     target.dispatchEvent(      new CustomEvent(PRELOAD_ERROR_EVENT, { cancelable: true, detail: { payload: chunkError } }),    );     expect(calls.probes).toBe(0);  });});