import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { once } from "node:events"; import net from "node:net"; import test from "node:test"; import { Cdp, stopChrome } from "./web_snapshot.mjs"; class Socket extends EventTarget { readyState = WebSocket.OPEN; sent = []; closes = 0; send(data) { this.sent.push(JSON.parse(data)); } reply(data) { this.dispatchEvent(new MessageEvent("message", { data: JSON.stringify(data) })); } close() { this.readyState = WebSocket.CLOSED; this.closes += 1; this.dispatchEvent(new Event("close")); } } test("CDP replies clear deadlines and preserve protocol errors", { timeout: 2_000 }, async () => { const socket = new Socket(); const client = new Cdp(socket, [], 20); try { const reply = client.send("Runtime.enable"); socket.reply({ id: socket.sent[0].id, result: { enabled: true } }); assert.deepEqual(await reply, { enabled: true }); const error = assert.rejects(client.send("Runtime.bad"), /Runtime.bad: unknown method/); socket.reply({ id: socket.sent[1].id, error: { message: "unknown method" } }); await error; await new Promise((resolve) => setTimeout(resolve, 40)); assert.equal(client.pending.size, 0); assert.equal(client.error, null); assert.equal(socket.closes, 0); } finally { client.close(); } }); test("CDP timeout rejects all pending and future commands", { timeout: 2_000 }, async () => { const socket = new Socket(); const client = new Cdp(socket, [], 20); await Promise.all([ assert.rejects(client.send("Runtime.evaluate"), /Runtime.evaluate timed out/), assert.rejects(client.send("Page.enable"), /Runtime.evaluate timed out/), ]); await assert.rejects(client.send("Log.enable"), /Runtime.evaluate timed out/); assert.equal(client.pending.size, 0); assert.equal(socket.closes, 1); assert.equal(socket.sent.length, 2); }); for (const event of ["close", "error"]) { test(`CDP ${event} immediately rejects pending commands`, { timeout: 2_000 }, async () => { const socket = new Socket(); const client = new Cdp(socket, [], 10_000); const pending = Promise.all([ assert.rejects(client.send("Runtime.evaluate"), /DevTools connection/), assert.rejects(client.send("Page.enable"), /DevTools connection/), ]); socket.dispatchEvent(new Event(event)); await pending; assert.equal(client.pending.size, 0); assert.equal(socket.closes, 1); }); } test("CDP rejects malformed replies and synchronous send failures", { timeout: 2_000 }, async () => { const socket = new Socket(); const client = new Cdp(socket, []); const pending = assert.rejects(client.send("Runtime.evaluate"), /invalid DevTools message/); socket.dispatchEvent(new MessageEvent("message", { data: "not json" })); await pending; assert.equal(client.pending.size, 0); assert.equal(socket.closes, 1); const broken = new Socket(); broken.send = () => { throw new Error("broken transport"); }; const failed = new Cdp(broken, []); await assert.rejects(failed.send("Page.enable"), /send DevTools command Page.enable/); assert.equal(failed.pending.size, 0); assert.equal(broken.closes, 1); }); test("closing CDP cancels commands once", { timeout: 2_000 }, async () => { const socket = new Socket(); const client = new Cdp(socket, []); const pending = assert.rejects(client.send("Runtime.evaluate"), /DevTools client closed/); client.close(); client.close(); await pending; assert.equal(client.pending.size, 0); assert.equal(socket.closes, 1); }); test("CDP closes a stalled connection after its deadline", { timeout: 2_000 }, async (t) => { const sockets = new Set(); const server = net.createServer((socket) => { sockets.add(socket); socket.resume(); socket.on("close", () => sockets.delete(socket)); }); t.after(async () => { const closed = new Promise((resolve) => server.close(resolve)); for (const socket of sockets) socket.destroy(); await closed; }); server.listen(0, "127.0.0.1"); await once(server, "listening"); await assert.rejects(Cdp.connect(`ws://127.0.0.1:${server.address().port}`, [], 40), /connection timed out/); const deadline = Date.now() + 1_000; while (sockets.size && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 5)); assert.equal(sockets.size, 0, "timed-out connection must close its TCP socket"); }); test("Chrome cleanup escalates and reaps a process ignoring SIGTERM", { timeout: 2_000 }, async (t) => { const child = spawn(process.execPath, ["--input-type=module", "-e", ` process.on("SIGTERM", () => {}); setInterval(() => {}, 1000); console.log("ready"); `], { stdio: ["ignore", "pipe", "ignore"] }); t.after(() => stopChrome(child, 10)); await once(child.stdout, "data"); await stopChrome(child, 20); assert.equal(child.signalCode, "SIGKILL"); await stopChrome(child, 20); });