Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184import { describe, expect, it } from "vitest";import { PassThrough } from "node:stream";import { CdpPipe } from "../../runner/cdp";
/** The fake Chrome: reads what the client wrote (NUL separated JSON) and can answer however a test likes. */function setup(options: { timeoutMs?: number } = {}) { const toChrome = new PassThrough(); const fromChrome = new PassThrough(); const cdp = new CdpPipe(toChrome, fromChrome, options); const received: any[] = []; let buf = ""; toChrome.on("data", (d) => { buf += d.toString(); let i: number; while ((i = buf.indexOf("\0")) >= 0) { received.push(JSON.parse(buf.slice(0, i))); buf = buf.slice(i + 1); } }); const reply = (m: unknown) => fromChrome.write(JSON.stringify(m) + "\0"); const tick = () => new Promise((r) => setTimeout(r, 5)); return { cdp, toChrome, fromChrome, received, reply, tick };}
describe("CdpPipe", () => { it("writes each command as NUL-terminated JSON with a fresh id, and resolves with the result", async () => { const t = setup(); const p = t.cdp.send("Target.getTargets"); await t.tick(); expect(t.received).toEqual([{ id: 1, method: "Target.getTargets", params: {} }]); t.reply({ id: 1, result: { targetInfos: [] } }); expect(await p).toEqual({ targetInfos: [] }); }); it("sends params and the session id when given", async () => { const t = setup(); const p = t.cdp.send("Page.captureScreenshot", { format: "png" }, "S1"); await t.tick(); expect(t.received[0]).toEqual({ id: 1, method: "Page.captureScreenshot", params: { format: "png" }, sessionId: "S1" }); t.reply({ id: 1, result: {} }); await p; }); it("matches answers to commands by id, whatever order they come back in", async () => { const t = setup(); const a = t.cdp.send("A"); const b = t.cdp.send("B"); await t.tick(); t.reply({ id: 2, result: "for B" }); t.reply({ id: 1, result: "for A" }); expect([await a, await b]).toEqual(["for A", "for B"]); }); it("rejects with Chrome's own message when Chrome answers with an error", async () => { const t = setup(); const p = t.cdp.send("Page.captureScreenshot"); await t.tick(); t.reply({ id: 1, error: { code: -32000, message: "Cannot take screenshot" } }); await expect(p).rejects.toThrowError("Page.captureScreenshot: Cannot take screenshot"); }); it("reads a message that arrives in pieces, and several that arrive together", async () => { const t = setup(); const a = t.cdp.send("A"); const b = t.cdp.send("B"); await t.tick(); const text = JSON.stringify({ id: 1, result: "x".repeat(100) }) + "\0" + JSON.stringify({ id: 2, result: "y" }) + "\0"; t.fromChrome.write(text.slice(0, 30)); await t.tick(); t.fromChrome.write(text.slice(30)); expect(await a).toBe("x".repeat(100)); expect(await b).toBe("y"); }); it("reads a multi-byte character that is split between two chunks", async () => { const t = setup(); const p = t.cdp.send("A"); await t.tick(); const bytes = Buffer.from(JSON.stringify({ id: 1, result: "café 中文" }) + "\0"); const cut = bytes.indexOf(0xc3) + 1; // between the two bytes of the e-acute t.fromChrome.write(bytes.subarray(0, cut)); await t.tick(); t.fromChrome.write(bytes.subarray(cut)); expect(await p).toBe("café 中文"); }); it("carries a multi-megabyte answer (a screenshot)", async () => { const t = setup(); const p = t.cdp.send("Page.captureScreenshot"); await t.tick(); const big = "A".repeat(6 * 1024 * 1024); t.reply({ id: 1, result: { data: big } }); expect((await p).data.length).toBe(big.length); }); it("ignores events, answers nobody asked for, and frames that are not JSON", async () => { const t = setup(); const p = t.cdp.send("A"); await t.tick(); t.reply({ method: "Page.loadEventFired", params: {} }); t.reply({ id: 99, result: "nobody asked" }); t.fromChrome.write("{ nope\0"); t.fromChrome.write("\0"); t.reply({ id: 1, result: "fine" }); expect(await p).toBe("fine"); }); it("gives up on a command Chrome never answers, and ignores the answer if it comes later", async () => { const t = setup({ timeoutMs: 30 }); const p = t.cdp.send("Page.captureScreenshot"); await expect(p).rejects.toThrowError("Page.captureScreenshot timed out after 30 ms"); t.reply({ id: 1, result: "too late" }); await t.tick(); const q = t.cdp.send("B"); await t.tick(); t.reply({ id: 2, result: "ok" }); expect(await q).toBe("ok"); }); it("fails every waiting command when Chrome closes the pipe", async () => { const t = setup(); const a = t.cdp.send("A"); const b = t.cdp.send("B"); await t.tick(); t.fromChrome.end(); await expect(a).rejects.toThrowError("Chrome closed the debugging pipe"); await expect(b).rejects.toThrowError("Chrome closed the debugging pipe"); }); it("refuses new commands after the pipe is closed, by Chrome or by us", async () => { const t = setup(); t.fromChrome.end(); await t.tick(); await expect(t.cdp.send("A")).rejects.toThrowError("the debugging pipe is closed"); const u = setup(); u.cdp.close(); await expect(u.cdp.send("A")).rejects.toThrowError("the debugging pipe is closed"); }); it("close() fails what is waiting, and may be called twice", async () => { const t = setup(); const p = t.cdp.send("A"); t.cdp.close(); t.cdp.close(); await expect(p).rejects.toThrowError("the debugging pipe is closed"); }); it("a write error fails that command instead of crashing", async () => { const t = setup(); t.toChrome.destroy(new Error("EPIPE")); await t.tick(); await expect(t.cdp.send("A")).rejects.toThrowError(/EPIPE|closed/); });});
describe("CdpPipe events", () => { it("tells a listener about events (messages with a method and no id), with their params and session", async () => { const t = setup(); const seen: unknown[] = []; t.cdp.onEvent((e) => seen.push(e)); t.reply({ method: "Network.requestWillBeSent", params: { requestId: "1" }, sessionId: "S1" }); t.reply({ method: "Page.loadEventFired", params: {} }); await t.tick(); expect(seen).toEqual([ { method: "Network.requestWillBeSent", params: { requestId: "1" }, sessionId: "S1" }, { method: "Page.loadEventFired", params: {} }, ]); }); it("stops telling a listener once it unsubscribes, and one throwing listener does not stop the others", async () => { const t = setup(); const a: string[] = []; const b: string[] = []; const off = t.cdp.onEvent((e) => a.push(e.method)); t.cdp.onEvent(() => { throw new Error("boom"); }); t.cdp.onEvent((e) => b.push(e.method)); t.reply({ method: "One", params: {} }); await t.tick(); off(); t.reply({ method: "Two", params: {} }); await t.tick(); expect(a).toEqual(["One"]); expect(b).toEqual(["One", "Two"]); }); it("answers to commands still resolve while events arrive between them", async () => { const t = setup(); const seen: string[] = []; t.cdp.onEvent((e) => seen.push(e.method)); const p = t.cdp.send("X.y"); await t.tick(); t.reply({ method: "Ev", params: {} }); t.reply({ id: 1, result: { ok: 1 } }); expect(await p).toEqual({ ok: 1 }); expect(seen).toEqual(["Ev"]); });});