desktop/preload.test.ts

import { test } from "node:test";
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";
import { transformSync } from "esbuild";

test("preload navigation bridge validates directions and removes its listener", () => {
  const ipcRenderer = new EventEmitter();
  let bridge!: NonNullable<Window["desktop"]>;
  const code = transformSync(readFileSync(new URL("./preload.ts", import.meta.url), "utf8"),
    { loader: "ts", format: "cjs" }).code;
  runInNewContext(code, { exports: {}, module: { exports: {} }, process: { platform: "linux" },
    require: () => ({ ipcRenderer, contextBridge: {
      exposeInMainWorld: (_name: string, value: typeof bridge) => { bridge = value; },
    } }),
  });
  const directions: string[] = [];
  const unsubscribe = bridge.onNavigation((direction) => directions.push(direction));
  for (const direction of ["back", "forward", "browser-backward", "invalid", null])
    ipcRenderer.emit("mail-navigation", {}, direction);
  assert.deepEqual(directions, ["back", "forward"]);
  unsubscribe();
  assert.equal(ipcRenderer.listenerCount("mail-navigation"), 0);
  ipcRenderer.emit("mail-navigation", {}, "back");
  assert.deepEqual(directions, ["back", "forward"]);
});

test("preload update bridge forwards only enum actions and correlates renderer drain without exposing IPC", async () => {
  const ipcRenderer = new EventEmitter() as EventEmitter & {
    invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
    send: (channel: string, ...args: unknown[]) => void;
  };
  const calls: unknown[][] = [];
  ipcRenderer.invoke = async (...args) => { calls.push(args); return { phase: "idle" }; };
  ipcRenderer.send = (...args) => { calls.push(args); };
  let bridge!: NonNullable<Window["desktop"]>;
  const code = transformSync(readFileSync(new URL("./preload.ts", import.meta.url), "utf8"), { loader: "ts", format: "cjs" }).code;
  runInNewContext(code, { exports: {}, module: { exports: {} }, process: { platform: "linux" },
    require: () => ({ ipcRenderer, contextBridge: { exposeInMainWorld: (_name: string, value: typeof bridge) => { bridge = value; } } }),
  });
  const updates = bridge.updates!;
  await updates.state(); await updates.command("download");
  assert.deepEqual(calls, [["update-state"], ["update-command", "download"]]);
  let drains = 0;
  const unsubscribe = updates.onDrain(async () => { drains++; });
  ipcRenderer.emit("update-drain", {}, "bad request");
  const request = "a".repeat(32);
  ipcRenderer.emit("update-drain", {}, request);
  await Promise.resolve();
  assert.equal(drains, 1); assert.deepEqual(calls.at(-1), ["update-drained", request]);
  let states = 0;
  const unsubscribeState = updates.onState(() => { states++; });
  ipcRenderer.emit("update-state", {}, { phase: "ready" });
  assert.equal(states, 1);
  unsubscribe(); unsubscribeState();
  assert.equal(ipcRenderer.listenerCount("update-drain"), 0);
  assert.equal(ipcRenderer.listenerCount("update-state"), 0);
});

test("isolated startup checks omit the updater bridge and perform no updater IPC", () => {
  let bridge!: NonNullable<Window["desktop"]>;
  const code = transformSync(readFileSync(new URL("./preload.ts", import.meta.url), "utf8"), { loader: "ts", format: "cjs" }).code;
  runInNewContext(code, { exports: {}, module: { exports: {} },
    process: { platform: "linux", argv: ["--emc2-disable-updates"] },
    require: () => ({ ipcRenderer: { invoke: () => { throw Error("No updater IPC allowed in startup checks"); } },
      contextBridge: { exposeInMainWorld: (_name: string, value: typeof bridge) => { bridge = value; } } }),
  });
  assert.equal(bridge.updates, undefined);
});

test("preload exposes credential fallback mode only for the exact main-process flag", () => {
  for (const [argv, expected] of [
    [["--emc2-credential-fallback=linux-local"], "linux-local"],
    [["--emc2-credential-fallback=other"], undefined],
    [[], undefined],
  ] as const) {
    let bridge!: NonNullable<Window["desktop"]>;
    const code = transformSync(readFileSync(new URL("./preload.ts", import.meta.url), "utf8"), { loader: "ts", format: "cjs" }).code;
    runInNewContext(code, { exports: {}, module: { exports: {} }, process: { platform: "linux", argv },
      require: () => ({ ipcRenderer: new EventEmitter(), contextBridge: { exposeInMainWorld: (_name: string, value: typeof bridge) => { bridge = value; } } }),
    });
    assert.equal(bridge.credentialFallback, expected);
  }
});