diff --git a/extensions/browser/src/browser/cdp.internal.test.ts b/extensions/browser/src/browser/cdp.internal.test.ts index 1a6d0725c679..eb6bdb80fa60 100644 --- a/extensions/browser/src/browser/cdp.internal.test.ts +++ b/extensions/browser/src/browser/cdp.internal.test.ts @@ -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: "
html
" } }, - }), - ); - } - }); - wss = server.wss; - const res = await getDomText({ - wsUrl: server.wsUrl, - format: "html", - selector: "#foo", - }); - expect(res.text).toBe("
html
"); - }); - - 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(""); - }); - }); }); diff --git a/extensions/browser/src/browser/cdp.ts b/extensions/browser/src/browser/cdp.ts index e3eb6cb23852..07072baeba3e 100644 --- a/extensions/browser/src/browser/cdp.ts +++ b/extensions/browser/src/browser/cdp.ts @@ -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 { - 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"; - 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; -};