import assert from "node:assert/strict"; import { EventEmitter } from "node:events"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; import { __test } from "./cdp.mjs"; const PNG_1X1 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="; function fakeCdp(handler) { const events = new Map(); return { send(method, params = {}, sid) { return handler(method, params, sid, events); }, waitForEvent(method) { let cancel = false; return { promise: new Promise((resolve, reject) => { events.set(method, { resolve, reject, isCanceled: () => cancel }); }), cancel() { cancel = true; }, }; }, }; } test("envInt accepts finite values above min and rejects bad input", () => { process.env.PI_TMP_INT = "42"; assert.equal(__test.envInt("PI_TMP_INT", 7), 42); process.env.PI_TMP_INT = "0"; assert.equal(__test.envInt("PI_TMP_INT", 7), 7); assert.equal(__test.envInt("PI_TMP_INT", 7, { min: 0 }), 0); process.env.PI_TMP_INT = "nope"; assert.equal(__test.envInt("PI_TMP_INT", 7), 7); delete process.env.PI_TMP_INT; assert.equal(__test.envInt("PI_TMP_INT", 7), 7); }); test("resolvePrefix reports no match, ambiguity, and exact match", () => { assert.equal(__test.resolvePrefix("abc", ["ABC123"], "target"), "ABC123"); assert.throws( () => __test.resolvePrefix("zzz", ["ABC123"], "target", "Run list."), /No target matching prefix "zzz"\. Run list\./, ); assert.throws( () => __test.resolvePrefix("abc", ["ABC123", "ABCD99"], "target"), /Ambiguous prefix/, ); }); test("getDisplayPrefixLength finds shortest unique uppercase-insensitive prefix", () => { assert.equal(__test.getDisplayPrefixLength([]), 8); assert.equal(__test.getDisplayPrefixLength(["ABCDEF120", "abcdef12F"]), 9); assert.equal(__test.getDisplayPrefixLength(["SAME", "same"]), 4); }); test("middle limiters preserve small input and mark truncated large input", () => { assert.equal(__test.limitTextMiddle("short", 10, "text"), "short"); assert.match( __test.limitTextMiddle("abcdefghijklmnopqrstuvwxyz", 20, "text"), /text truncated/, ); assert.deepEqual(__test.limitLinesMiddle(["a", "b"], 3, "lines"), ["a", "b"]); assert.deepEqual(__test.limitLinesMiddle(["a", "b", "c", "d", "e"], 3, "lines"), [ "a", "b", "… (lines truncated: 5 lines > 3; set limit env to 0 to disable)", ]); }); test("evalStr renders objects, nulls, and exception descriptions", async () => { assert.equal( await __test.evalStr(fakeCdp((method) => method === "Runtime.evaluate" ? { result: { value: { ok: true } } } : {}), "sid", "obj"), '{\n "ok": true\n}', ); assert.equal( await __test.evalStr(fakeCdp((method) => method === "Runtime.evaluate" ? { result: { value: null } } : {}), "sid", "nil"), "", ); await assert.rejects( () => __test.evalStr(fakeCdp((method) => method === "Runtime.evaluate" ? { exceptionDetails: { exception: { description: "Error: boom" } } } : {}), "sid", "bad"), /Error: boom/, ); }); test("snapshotStr orders explicit and parent-linked children without duplicates", async () => { const out = await __test.snapshotStr(fakeCdp(() => ({ nodes: [ { nodeId: "1", role: { value: "RootWebArea" }, name: { value: "Root" }, childIds: ["2"] }, { nodeId: "2", parentId: "1", role: { value: "button" }, name: { value: "Go" } }, { nodeId: "3", parentId: "1", role: { value: "checkbox" }, name: { value: "Remember" }, value: { value: true } }, { nodeId: "4", role: { value: "none" }, name: { value: "" } }, ], })), "sid", false); assert.match(out, /\[RootWebArea\] Root/); assert.match(out, / \[button\] Go/); assert.match(out, / \[checkbox\] Remember = true/); assert.doesNotMatch(out, /\[none\]/); }); test("shotStr falls back to layout metrics when devicePixelRatio eval fails", async () => { const dir = await mkdtemp(join(tmpdir(), "cdp-shot-unit-")); const out = join(dir, "shot.png"); try { const result = await __test.shotStr(fakeCdp((method) => { if (method === "Runtime.evaluate") throw new Error("no dpr"); if (method === "Page.getLayoutMetrics") return { visualViewport: { clientWidth: 1600 }, cssVisualViewport: { clientWidth: 800 } }; if (method === "Page.captureScreenshot") return { data: PNG_1X1 }; return {}; }), "sid", out); assert.match(result, /DPR\): 2/); assert.equal(await readFile(out, "base64"), PNG_1X1); } finally { await rm(dir, { recursive: true, force: true }); } }); test("navStr cancels immediately when no loaderId is returned", async () => { let canceled = false; const cdp = fakeCdp((method, _params, _sid, events) => { if (method === "Page.navigate") return { frameId: "same-document" }; if (method === "Runtime.evaluate") return { result: { value: "complete" } }; return {}; }); const originalWait = cdp.waitForEvent; cdp.waitForEvent = (method) => { const event = originalWait(method); const originalCancel = event.cancel; event.cancel = () => { canceled = true; originalCancel(); }; return event; }; assert.equal(await __test.navStr(cdp, "sid", "https://example.test/hash"), "Navigated to https://example.test/hash"); assert.equal(canceled, true); }); test("sendCommand handles early end, early close, and socket errors", async () => { class FakeConn extends EventEmitter { write() {} end() {} } const ended = new FakeConn(); const endPromise = __test.sendCommand(ended, { cmd: "x" }); ended.emit("end"); await assert.rejects(() => endPromise, /Connection closed before response/); const closed = new FakeConn(); const closePromise = __test.sendCommand(closed, { cmd: "x" }); closed.emit("close"); await assert.rejects(() => closePromise, /Connection closed before response/); const errored = new FakeConn(); const errorPromise = __test.sendCommand(errored, { cmd: "x" }); errored.emit("error", new Error("boom")); await assert.rejects(() => errorPromise, /boom/); }); test("waitForDocumentReady reports timeout states and errors", async () => { await assert.rejects( () => __test.waitForDocumentReady(fakeCdp(() => ({ result: { value: "loading" } })), "sid", 1), /last readyState: loading/, ); await assert.rejects( () => __test.waitForDocumentReady(fakeCdp(() => { throw new Error("runtime gone"); }), "sid", 1), /runtime gone/, ); await assert.rejects( () => __test.waitForDocumentReady(fakeCdp(() => ({ result: { value: "" } })), "sid", 1), /Timed out waiting for navigation to finish$/, ); }); test("netStr formats missing sizes", async () => { const out = await __test.netStr(fakeCdp((method) => method === "Runtime.evaluate" ? { result: { value: JSON.stringify([{ name: "https://example.test/x", duration: 1 }]) } } : {}), "sid"); assert.match(out, /\?B\s+\?\s+https:\/\/example\.test\/x/); });