diff options
Diffstat (limited to 'test/web_driver_test.mjs')
| -rw-r--r-- | test/web_driver_test.mjs | 134 |
1 files changed, 134 insertions, 0 deletions
diff --git a/test/web_driver_test.mjs b/test/web_driver_test.mjs new file mode 100644 index 00000000..5484d763 --- /dev/null +++ b/test/web_driver_test.mjs @@ -0,0 +1,134 @@ +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); +}); |
