mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 18:42:25 +00:00
refactor(browser): remove unused CDP DOM helpers
This commit is contained in:
@@ -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("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user