150 lines
6.1 KiB
TypeScript
150 lines
6.1 KiB
TypeScript
/** Execute the real helper entry with fake Electron; no display or user profile. */
|
|
import assert from "node:assert/strict";
|
|
import { EventEmitter } from "node:events";
|
|
import { readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { dirname } from "node:path";
|
|
import { Readable } from "node:stream";
|
|
import { describe, it } from "node:test";
|
|
import { setImmediate as turn } from "node:timers/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { runInNewContext } from "node:vm";
|
|
import { stopBrowserService } from "../scripts/smoke-demo-runtime.mjs";
|
|
|
|
const mainFile = fileURLToPath(new URL("../electron/main.cjs", import.meta.url));
|
|
const mainSource = readFileSync(mainFile, "utf8");
|
|
const requireMain = createRequire(mainFile);
|
|
|
|
function helperFixture(connected = true, channel = true) {
|
|
let allowAppReady!: () => void;
|
|
const appReady = new Promise<void>((resolve) => { allowAppReady = resolve; });
|
|
let allowReadiness!: () => void;
|
|
const readiness = new Promise<void>((resolve) => { allowReadiness = resolve; });
|
|
const state = { quits: 0, exits: [] as number[], windows: 0, readinessCalls: 0, stdinReads: 0, signal: undefined as AbortSignal | undefined };
|
|
const logs: string[] = [];
|
|
const errors: string[] = [];
|
|
// Match Electron 44's Windows override, not a native pipe.
|
|
const stdin = new Readable({ read() {} });
|
|
stdin.push(null);
|
|
const process = Object.assign(new EventEmitter(), {
|
|
platform: "win32", argv: ["electron", "--browser-service"], env: {},
|
|
connected, channel: channel ? {} : undefined,
|
|
});
|
|
Object.defineProperty(process, "stdin", { get() { state.stdinReads++; return stdin; } });
|
|
const app = {
|
|
setName() {}, commandLine: { appendSwitch() {} }, whenReady: () => appReady,
|
|
quit() { state.quits++; }, exit(code: number) { state.exits.push(code); },
|
|
};
|
|
class FakeWindow {
|
|
constructor() { state.windows++; }
|
|
async loadURL(url: string) { assert.equal(url, "about:blank"); }
|
|
}
|
|
const net = {
|
|
createServer: () => Object.assign(new EventEmitter(), {
|
|
listen(_port: number, _host: string, callback: () => void) { callback(); },
|
|
address: () => ({ port: 54321 }), close(callback: () => void) { callback(); },
|
|
}),
|
|
};
|
|
const waitForBrowserService = (_port: number, { signal }: { signal: AbortSignal }) => {
|
|
state.readinessCalls++;
|
|
state.signal = signal;
|
|
return new Promise<void>((resolve, reject) => {
|
|
if (signal.aborted) return reject(signal.reason);
|
|
signal.addEventListener("abort", () => reject(signal.reason), { once: true });
|
|
readiness.then(resolve);
|
|
});
|
|
};
|
|
runInNewContext(mainSource, {
|
|
require: (id: string) => id === "electron" ? { app, BrowserWindow: FakeWindow }
|
|
: id === "node:net" ? net
|
|
: id === "./browser-service-readiness.cjs" ? { waitForBrowserService }
|
|
: requireMain(id),
|
|
process, __dirname: dirname(mainFile), AbortController,
|
|
console: { log: (line: string) => logs.push(line), error: (...parts: unknown[]) => errors.push(parts.join(" ")) },
|
|
}, { filename: mainFile });
|
|
return {
|
|
state, logs, errors, stdin, process, allowAppReady, allowReadiness,
|
|
disconnect() { process.connected = false; process.emit("disconnect"); },
|
|
};
|
|
}
|
|
|
|
describe("browser-service IPC-owned lifetime", () => {
|
|
it("ignores synthetic Windows stdin EOF while IPC remains connected", async () => {
|
|
const helper = helperFixture();
|
|
helper.stdin.resume();
|
|
await turn();
|
|
assert.equal(helper.stdin.readableEnded, true);
|
|
assert.equal(helper.state.stdinReads, 0);
|
|
assert.equal(helper.state.quits, 0);
|
|
helper.allowAppReady();
|
|
await turn();
|
|
assert.equal(helper.state.readinessCalls, 1);
|
|
assert.equal(helper.state.signal?.aborted, false);
|
|
helper.allowReadiness();
|
|
await turn();
|
|
assert.deepEqual(helper.logs, ["BROWSER_SERVICE_CDP 54321"]);
|
|
assert.deepEqual(helper.state.exits, []);
|
|
helper.disconnect();
|
|
assert.equal(helper.state.quits, 1);
|
|
});
|
|
|
|
it("disconnect during readiness aborts it, quits once, and never announces", async () => {
|
|
const helper = helperFixture();
|
|
helper.allowAppReady();
|
|
await turn();
|
|
assert.equal(helper.state.readinessCalls, 1);
|
|
helper.disconnect();
|
|
helper.disconnect();
|
|
await turn();
|
|
assert.equal(helper.state.signal?.aborted, true);
|
|
assert.equal(helper.state.quits, 1);
|
|
assert.deepEqual(helper.logs, []);
|
|
assert.deepEqual(helper.errors, []);
|
|
assert.deepEqual(helper.state.exits, []);
|
|
assert.equal(helper.state.stdinReads, 0);
|
|
});
|
|
|
|
it("owns disconnect before app.whenReady resolves", async () => {
|
|
const helper = helperFixture();
|
|
helper.disconnect();
|
|
await turn();
|
|
assert.equal(helper.state.quits, 1);
|
|
assert.equal(helper.state.windows, 0);
|
|
assert.deepEqual(helper.logs, []);
|
|
});
|
|
|
|
it("fails closed without both a connected IPC channel and connection state", async () => {
|
|
for (const [connected, channel] of [[false, false], [true, false], [false, true]]) {
|
|
const helper = helperFixture(connected, channel);
|
|
await turn();
|
|
assert.deepEqual(helper.state.exits, [1]);
|
|
assert.match(helper.errors.join("\n"), /requires connected parent IPC/);
|
|
assert.equal(helper.state.windows, 0);
|
|
assert.equal(helper.state.readinessCalls, 0);
|
|
assert.equal(helper.state.stdinReads, 0);
|
|
assert.equal(helper.process.listenerCount("disconnect"), 0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("browser-service probe cleanup", () => {
|
|
it("requests IPC disconnect and accepts actual exit without a close event", async () => {
|
|
const helper = Object.assign(new EventEmitter(), {
|
|
pid: 123, connected: true, exitCode: null as number | null, signalCode: null,
|
|
disconnect() {
|
|
helper.connected = false;
|
|
queueMicrotask(() => { helper.exitCode = 0; helper.emit("exit", 0, null); });
|
|
},
|
|
});
|
|
assert.deepEqual(await stopBrowserService(helper), { code: 0, signal: null, requested: true });
|
|
assert.equal(helper.listenerCount("exit"), 0);
|
|
assert.equal(helper.listenerCount("error"), 0);
|
|
});
|
|
|
|
it("does not label an already-exited helper as parent-requested shutdown", async () => {
|
|
const helper = { pid: 123, connected: false, exitCode: 0, signalCode: null,
|
|
disconnect() { assert.fail("must not request shutdown after early exit"); } };
|
|
assert.deepEqual(await stopBrowserService(helper), { code: 0, signal: null, requested: false });
|
|
});
|
|
});
|