refactor(browser): remove unused CDP DOM helpers

This commit is contained in:
Vincent Koc
2026-06-19 00:44:26 +08:00
parent 1eaace70e3
commit 9546b19a59
2 changed files with 0 additions and 506 deletions

View File

@@ -6,18 +6,12 @@ import "../test-support/browser-security.mock.js";
import {
type AriaSnapshotNode,
captureScreenshot,
captureScreenshotPng,
createTargetViaCdp,
type DomSnapshotNode,
evaluateJavaScript,
formatAriaSnapshot,
getDomText,
normalizeCdpWsUrl,
type QueryMatch,
querySelector,
type RawAXNode,
snapshotAria,
snapshotDom,
snapshotRoleViaCdp,
} from "./cdp.js";
@@ -164,27 +158,6 @@ describe("cdp internal", () => {
expect(buf.toString("utf8")).toBe("PNGDATA");
});
it("captureScreenshotPng forwards to the png captureScreenshot flow", async () => {
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Page.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Page.captureScreenshot") {
expect(msg.params?.format).toBe("png");
socket.send(
JSON.stringify({
id: msg.id,
result: { data: Buffer.from("WRAPPED").toString("base64") },
}),
);
}
});
wss = server.wss;
const buf = await captureScreenshotPng({ wsUrl: server.wsUrl });
expect(buf.toString("utf8")).toBe("WRAPPED");
});
it("clamps out-of-range JPEG quality values into [0, 100]", async () => {
const { observed } = await captureScreenshotAndObserveParams({
format: "jpeg",
@@ -730,257 +703,6 @@ describe("cdp internal", () => {
});
});
describe("snapshotDom", () => {
it("returns the nodes array from the evaluated expression", async () => {
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
const fake: DomSnapshotNode[] = [{ ref: "n1", parentRef: null, depth: 0, tag: "html" }];
socket.send(
JSON.stringify({
id: msg.id,
result: { result: { value: { nodes: fake } } },
}),
);
}
});
wss = server.wss;
const snap = await snapshotDom({ wsUrl: server.wsUrl, limit: 10, maxTextChars: 200 });
expect(snap.nodes[0]?.tag).toBe("html");
});
it("returns an empty nodes array when the value is not an object", async () => {
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
socket.send(
JSON.stringify({
id: msg.id,
result: { result: { value: null } },
}),
);
}
});
wss = server.wss;
const snap = await snapshotDom({ wsUrl: server.wsUrl });
expect(snap.nodes).toStrictEqual([]);
});
it("returns an empty nodes array when nodes is not an array", async () => {
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
socket.send(
JSON.stringify({
id: msg.id,
result: { result: { value: { nodes: "not-an-array" } } },
}),
);
}
});
wss = server.wss;
const snap = await snapshotDom({ wsUrl: server.wsUrl });
expect(snap.nodes).toStrictEqual([]);
});
it("uses default DOM snapshot budgets for non-finite options", async () => {
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
const expression =
typeof msg.params?.expression === "string" ? msg.params.expression : "";
expect(expression).toContain("const maxNodes = 800;");
expect(expression).toContain("const maxText = 220;");
socket.send(JSON.stringify({ id: msg.id, result: { result: { value: { nodes: [] } } } }));
}
});
wss = server.wss;
const snap = await snapshotDom({
wsUrl: server.wsUrl,
limit: Number.NaN,
maxTextChars: Number.NaN,
});
expect(snap.nodes).toStrictEqual([]);
});
});
describe("getDomText", () => {
it("returns the evaluated string for text format", async () => {
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
socket.send(
JSON.stringify({
id: msg.id,
result: { result: { value: "plain body text" } },
}),
);
}
});
wss = server.wss;
const res = await getDomText({ wsUrl: server.wsUrl, format: "text", maxChars: 100 });
expect(res.text).toBe("plain body text");
});
it("returns the html outerHTML for html format with a selector", async () => {
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
socket.send(
JSON.stringify({
id: msg.id,
result: { result: { value: "<div>html</div>" } },
}),
);
}
});
wss = server.wss;
const res = await getDomText({
wsUrl: server.wsUrl,
format: "html",
selector: "#foo",
});
expect(res.text).toBe("<div>html</div>");
});
it("coerces numeric/boolean values to strings and falls back to empty for objects", async () => {
const responses: unknown[] = [42, true, { shape: "object" }];
let i = 0;
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
socket.send(
JSON.stringify({
id: msg.id,
result: { result: { value: responses[i++] } },
}),
);
}
});
wss = server.wss;
const num = await getDomText({ wsUrl: server.wsUrl, format: "text" });
expect(num.text).toBe("42");
const bool = await getDomText({ wsUrl: server.wsUrl, format: "text" });
expect(bool.text).toBe("true");
const obj = await getDomText({ wsUrl: server.wsUrl, format: "text" });
expect(obj.text).toBe("");
});
it("uses the default text budget for non-finite maxChars", async () => {
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
const expression =
typeof msg.params?.expression === "string" ? msg.params.expression : "";
expect(expression).toContain("const max = 200000;");
socket.send(JSON.stringify({ id: msg.id, result: { result: { value: "ok" } } }));
}
});
wss = server.wss;
const res = await getDomText({
wsUrl: server.wsUrl,
format: "text",
maxChars: Number.NaN,
});
expect(res.text).toBe("ok");
});
});
describe("querySelector", () => {
it("returns the matches array from the evaluated expression", async () => {
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
const matches: QueryMatch[] = [{ index: 1, tag: "button", text: "OK" }];
socket.send(JSON.stringify({ id: msg.id, result: { result: { value: matches } } }));
}
});
wss = server.wss;
const out = await querySelector({
wsUrl: server.wsUrl,
selector: "button",
limit: 5,
maxTextChars: 100,
maxHtmlChars: 500,
});
expect(out.matches[0]?.tag).toBe("button");
});
it("returns an empty array when the value is not an array", async () => {
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
socket.send(JSON.stringify({ id: msg.id, result: { result: { value: "not-array" } } }));
}
});
wss = server.wss;
const out = await querySelector({ wsUrl: server.wsUrl, selector: "button" });
expect(out.matches).toStrictEqual([]);
});
it("uses default query budgets for non-finite options", async () => {
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
const expression =
typeof msg.params?.expression === "string" ? msg.params.expression : "";
expect(expression).toContain("const lim = 20;");
expect(expression).toContain("const maxText = 500;");
expect(expression).toContain("const maxHtml = 1500;");
socket.send(JSON.stringify({ id: msg.id, result: { result: { value: [] } } }));
}
});
wss = server.wss;
const out = await querySelector({
wsUrl: server.wsUrl,
selector: "button",
limit: Number.NaN,
maxTextChars: Number.NaN,
maxHtmlChars: Number.NaN,
});
expect(out.matches).toStrictEqual([]);
});
});
describe("normalizeCdpWsUrl fill-in", () => {
it("respects an already-non-loopback ws hostname (no-rewrite branch)", () => {
// Covers the else side of the loopback/wildcard-guard in normalizeCdpWsUrl.
@@ -1287,21 +1009,4 @@ describe("cdp internal", () => {
});
});
describe("getDomText branch coverage", () => {
it("coerces a missing evaluated value to an empty string", async () => {
// Covers the right-hand side of `evaluated.result?.value ?? ""`.
const server = await startMockWsServer((msg, socket) => {
if (msg.method === "Runtime.enable") {
socket.send(JSON.stringify({ id: msg.id, result: {} }));
return;
}
if (msg.method === "Runtime.evaluate") {
socket.send(JSON.stringify({ id: msg.id, result: { result: {} } }));
}
});
wss = server.wss;
const res = await getDomText({ wsUrl: server.wsUrl, format: "text" });
expect(res.text).toBe("");
});
});
});

View File

@@ -65,20 +65,6 @@ export function normalizeCdpWsUrl(wsUrl: string, cdpUrl: string): string {
return ws.toString();
}
/** Capture a PNG screenshot through CDP. */
export async function captureScreenshotPng(opts: {
wsUrl: string;
fullPage?: boolean;
timeoutMs?: number;
}): Promise<Buffer> {
return await captureScreenshot({
wsUrl: opts.wsUrl,
fullPage: opts.fullPage,
format: "png",
timeoutMs: opts.timeoutMs,
});
}
/** Capture a PNG or JPEG screenshot through CDP, optionally full-page. */
export async function captureScreenshot(opts: {
wsUrl: string;
@@ -979,200 +965,3 @@ export async function snapshotRoleViaCdp(opts: {
{ commandTimeoutMs: opts.timeoutMs ?? 5000 },
);
}
/** Capture a raw DOM snapshot through CDP. */
export async function snapshotDom(opts: {
wsUrl: string;
limit?: number;
maxTextChars?: number;
}): Promise<{
nodes: DomSnapshotNode[];
}> {
const limit = resolveIntegerOption(opts.limit, 800, { min: 1, max: 5000 });
const maxTextChars = resolveIntegerOption(opts.maxTextChars, 220, { min: 0, max: 5000 });
const expression = `(() => {
const maxNodes = ${JSON.stringify(limit)};
const maxText = ${JSON.stringify(maxTextChars)};
const lower = (value) => String(value || "").toLocaleLowerCase();
const nodes = [];
const root = document.documentElement;
if (!root) return { nodes };
const stack = [{ el: root, depth: 0, parentRef: null }];
while (stack.length && nodes.length < maxNodes) {
const cur = stack.pop();
const el = cur.el;
if (!el || el.nodeType !== 1) continue;
const ref = "n" + String(nodes.length + 1);
const tag = lower(el.tagName);
const id = el.id ? String(el.id) : undefined;
const className = el.className ? String(el.className).slice(0, 300) : undefined;
const role = el.getAttribute && el.getAttribute("role") ? String(el.getAttribute("role")) : undefined;
const name = el.getAttribute && el.getAttribute("aria-label") ? String(el.getAttribute("aria-label")) : undefined;
let text = "";
try { text = String(el.innerText || "").trim(); } catch {}
if (maxText && text.length > maxText) text = text.slice(0, maxText) + "…";
const href = (el.href !== undefined && el.href !== null) ? String(el.href) : undefined;
const type = (el.type !== undefined && el.type !== null) ? String(el.type) : undefined;
const value = (el.value !== undefined && el.value !== null) ? String(el.value).slice(0, 500) : undefined;
nodes.push({
ref,
parentRef: cur.parentRef,
depth: cur.depth,
tag,
...(id ? { id } : {}),
...(className ? { className } : {}),
...(role ? { role } : {}),
...(name ? { name } : {}),
...(text ? { text } : {}),
...(href ? { href } : {}),
...(type ? { type } : {}),
...(value ? { value } : {}),
});
const children = el.children ? Array.from(el.children) : [];
for (let i = children.length - 1; i >= 0; i--) {
stack.push({ el: children[i], depth: cur.depth + 1, parentRef: ref });
}
}
return { nodes };
})()`;
const evaluated = await evaluateJavaScript({
wsUrl: opts.wsUrl,
expression,
awaitPromise: true,
returnByValue: true,
});
const value = evaluated.result?.value;
if (!value || typeof value !== "object") {
return { nodes: [] };
}
const nodes = (value as { nodes?: unknown }).nodes;
return { nodes: Array.isArray(nodes) ? (nodes as DomSnapshotNode[]) : [] };
}
/** Simplified DOM node returned by DOM snapshot helpers. */
export type DomSnapshotNode = {
ref: string;
parentRef: string | null;
depth: number;
tag: string;
id?: string;
className?: string;
role?: string;
name?: string;
text?: string;
href?: string;
type?: string;
value?: string;
};
/** Extract visible DOM text from a CDP target. */
export async function getDomText(opts: {
wsUrl: string;
format: "html" | "text";
maxChars?: number;
selector?: string;
}): Promise<{ text: string }> {
const maxChars = resolveIntegerOption(opts.maxChars, 200_000, { min: 0, max: 5_000_000 });
const selectorExpr = opts.selector ? JSON.stringify(opts.selector) : "null";
const expression = `(() => {
const fmt = ${JSON.stringify(opts.format)};
const max = ${JSON.stringify(maxChars)};
const sel = ${selectorExpr};
const pick = sel ? document.querySelector(sel) : null;
let out = "";
if (fmt === "text") {
const el = pick || document.body || document.documentElement;
try { out = String(el && el.innerText ? el.innerText : ""); } catch { out = ""; }
} else {
const el = pick || document.documentElement;
try { out = String(el && el.outerHTML ? el.outerHTML : ""); } catch { out = ""; }
}
if (max && out.length > max) out = out.slice(0, max) + "\\n<!-- …truncated… -->";
return out;
})()`;
const evaluated = await evaluateJavaScript({
wsUrl: opts.wsUrl,
expression,
awaitPromise: true,
returnByValue: true,
});
const textValue = (evaluated.result?.value ?? "") as unknown;
const text =
typeof textValue === "string"
? textValue
: typeof textValue === "number" || typeof textValue === "boolean"
? String(textValue)
: "";
return { text };
}
/** Query a selector in a CDP target and return matching node metadata. */
export async function querySelector(opts: {
wsUrl: string;
selector: string;
limit?: number;
maxTextChars?: number;
maxHtmlChars?: number;
}): Promise<{
matches: QueryMatch[];
}> {
const limit = resolveIntegerOption(opts.limit, 20, { min: 1, max: 200 });
const maxText = resolveIntegerOption(opts.maxTextChars, 500, { min: 0, max: 5000 });
const maxHtml = resolveIntegerOption(opts.maxHtmlChars, 1500, { min: 0, max: 20_000 });
const expression = `(() => {
const sel = ${JSON.stringify(opts.selector)};
const lim = ${JSON.stringify(limit)};
const maxText = ${JSON.stringify(maxText)};
const maxHtml = ${JSON.stringify(maxHtml)};
const lower = (value) => String(value || "").toLocaleLowerCase();
const els = Array.from(document.querySelectorAll(sel)).slice(0, lim);
return els.map((el, i) => {
const tag = lower(el.tagName);
const id = el.id ? String(el.id) : undefined;
const className = el.className ? String(el.className).slice(0, 300) : undefined;
let text = "";
try { text = String(el.innerText || "").trim(); } catch {}
if (maxText && text.length > maxText) text = text.slice(0, maxText) + "…";
const value = (el.value !== undefined && el.value !== null) ? String(el.value).slice(0, 500) : undefined;
const href = (el.href !== undefined && el.href !== null) ? String(el.href) : undefined;
let outerHTML = "";
try { outerHTML = String(el.outerHTML || ""); } catch {}
if (maxHtml && outerHTML.length > maxHtml) outerHTML = outerHTML.slice(0, maxHtml) + "…";
return {
index: i + 1,
tag,
...(id ? { id } : {}),
...(className ? { className } : {}),
...(text ? { text } : {}),
...(value ? { value } : {}),
...(href ? { href } : {}),
...(outerHTML ? { outerHTML } : {}),
};
});
})()`;
const evaluated = await evaluateJavaScript({
wsUrl: opts.wsUrl,
expression,
awaitPromise: true,
returnByValue: true,
});
const matches = evaluated.result?.value;
return { matches: Array.isArray(matches) ? (matches as QueryMatch[]) : [] };
}
/** Selector match metadata returned by querySelector. */
export type QueryMatch = {
index: number;
tag: string;
id?: string;
className?: string;
text?: string;
value?: string;
href?: string;
outerHTML?: string;
};