summaryrefslogtreecommitdiff
path: root/test/web_driver_test.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'test/web_driver_test.mjs')
-rw-r--r--test/web_driver_test.mjs134
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);
+});