summaryrefslogtreecommitdiff
path: root/test/web_driver_test.mjs
blob: 5484d7634850d73f0a5c5d21bb39c49f8f0a9446 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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);
});