import { describe, expect, it } from "vitest"; import { captureAccessibility, captureDom, captureFrames, captureResources, } from "./CdpCaptureDocuments.js"; import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js"; import { inspectWebPageInputSchema } from "../domain/browserObservation.js"; const origin = "https://app.example.test"; describe("CDP DOM capture", () => { it("preserves node names and parents when aligned arrays are valid", () => { const generator = captureDom( { strings: [origin + "/app", "#document", "DIV", "SPAN", ""], documents: [ { documentURL: 0, baseURL: 0, nodes: { nodeType: [9, 1, 1], nodeName: [1, 2, 3], nodeValue: [4, 4, 4], parentIndex: [-1, 0, 1], attributes: [[], [], []], }, }, ], }, new Set([origin]), inspectWebPageInputSchema.parse({ cdp_endpoint: "http://127.0.0.1:9222", allowed_origins: [origin], target_id: "page-1", }), ); let captured = generator.next([]); while (!captured.done) captured = generator.next([]); expect(captured.value.nodes.map((node) => node.node_name)).toEqual([ "#document", "DIV", "SPAN", ]); expect(captured.value.nodes.map((node) => node.parent_index)).toEqual([ -1, 0, 1, ]); expect(captured.value.nodes.map((node) => node.node_value_length)).toEqual([ 0, 0, 0, ]); }); it.each(["nodeName", "parentIndex", "nodeValue", "attributes"] as const)( "preserves DOM fact positions and reports malformed %s entries", (field) => { const strings = [ origin + "/app", "#document", "DIV", "SPAN", "href", "title", "/page", ]; const nodeArrays: Record = { nodeType: [9, 1, 1], nodeName: [1, 2, 3], nodeValue: [0, 0, 0], parentIndex: [-1, 0, 1], attributes: [[], [], []], }; if (field === "nodeName") nodeArrays.nodeName = [1, "malformed", 3]; if (field === "parentIndex") nodeArrays.parentIndex = [-1, "malformed", 1]; if (field === "nodeValue") nodeArrays.nodeValue = [0, 0, "malformed"]; if (field === "attributes") nodeArrays.attributes = [[], [4, "malformed", 5, 6], []]; const completeness = new CdpCaptureCompleteness(); const generator = captureDom( { strings, documents: [ { documentURL: 0, baseURL: 0, nodes: nodeArrays, }, ], }, new Set([origin]), inspectWebPageInputSchema.parse({ cdp_endpoint: "http://127.0.0.1:9222", allowed_origins: [origin], target_id: "page-1", }), completeness, ); let captured = generator.next([]); while (!captured.done) captured = generator.next([]); expect(captured.value.total).toBe(3); expect(captured.value.nodes.map((node) => node.node_name)).toEqual( field === "nodeValue" ? ["#document", "DIV"] : ["#document", "SPAN"], ); expect(captured.value.nodes.map((node) => node.index)).toEqual( field === "nodeValue" ? [0, 1] : [0, 2], ); expect(captured.value.nodes.map((node) => node.parent_index)).toEqual( field === "nodeValue" ? [-1, 0] : [-1, 1], ); if (field === "attributes") { expect( captured.value.nodes.map((node) => node.attribute_names), ).toEqual([[], []]); expect(captured.value.urls).toEqual([]); } expect(completeness.snapshot()).toMatchObject({ unavailable_sections: expect.arrayContaining(["dom"]), excluded: expect.arrayContaining([ { section: "dom", reason: "invalid_protocol_value", count: 1 }, ]), }); }, ); }); describe("CDP DOM string and namespace validity", () => { it.each([ { field: "nodeName", index: -1 }, { field: "nodeName", index: 20 }, { field: "nodeName", index: 2, malformedString: true }, { field: "nodeValue", index: -1 }, { field: "nodeValue", index: 20 }, { field: "nodeValue", index: 5, malformedString: true }, { field: "attributeName", index: -1 }, { field: "attributeName", index: 20 }, { field: "attributeValue", index: -1 }, { field: "attributeValue", index: 20 }, ] as const)( "omits a node when its $field string index is invalid ($index)", ({ field, index, malformedString, }: { readonly field: | "nodeName" | "nodeValue" | "attributeName" | "attributeValue"; readonly index: number; readonly malformedString?: boolean; }) => { const strings: unknown[] = [ origin + "/app", "#document", "DIV", "SPAN", "href", "/good", ]; const nodeArrays: Record = { nodeType: [9, 1, 1], nodeName: [1, 2, 3], nodeValue: [0, 0, 0], parentIndex: [-1, 0, 0], attributes: [[], [], []], }; if (field === "nodeName") nodeArrays.nodeName = [1, index, 3]; if (field === "nodeValue") nodeArrays.nodeValue = [0, index, 0]; if (field === "attributeName") nodeArrays.attributes = [[], [index, 5], []]; if (field === "attributeValue") nodeArrays.attributes = [[], [4, index], []]; if (malformedString === true) strings[index === 2 ? 2 : 5] = 42; const completeness = new CdpCaptureCompleteness(); const generator = captureDom( { strings, documents: [ { documentURL: 0, baseURL: 0, nodes: nodeArrays, }, ], }, new Set([origin]), inspectWebPageInputSchema.parse({ cdp_endpoint: "http://127.0.0.1:9222", allowed_origins: [origin], target_id: "page-1", }), completeness, ); let captured = generator.next([]); while (!captured.done) captured = generator.next([]); expect(captured.value.total).toBe(3); expect(captured.value.nodes.map((node) => node.index)).toEqual([0, 2]); expect(captured.value.nodes.map((node) => node.node_name)).toEqual([ "#document", "SPAN", ]); expect(completeness.snapshot().excluded).toContainEqual({ section: "dom", reason: "invalid_protocol_value", count: 1, }); }, ); it("does not infer SVG ancestry through an element with malformed namespace data", () => { const strings: unknown[] = [ origin + "/app", "#document", "svg", "a", "xmlns:xlink", "http://www.w3.org/1999/xlink", "xlink:href", "/svg-link", ]; const completeness = new CdpCaptureCompleteness(); const generator = captureDom( { strings, documents: [ { documentURL: 0, baseURL: 0, nodes: { nodeType: [9, 1, 1], nodeName: [1, 2, 3], nodeValue: [0, 0, 0], parentIndex: [-1, 0, 1], attributes: [[], [4, 99], [6, 7]], }, }, ], }, new Set([origin]), inspectWebPageInputSchema.parse({ cdp_endpoint: "http://127.0.0.1:9222", allowed_origins: [origin], target_id: "page-1", }), completeness, ); let captured = generator.next([]); while (!captured.done) captured = generator.next([]); expect(captured.value.nodes.map((node) => node.index)).toEqual([0, 2]); expect(captured.value.nodes[1]?.attribute_names).toEqual(["xlink:href"]); expect(captured.value.urls).toEqual([]); expect( completeness .snapshot() .excluded.some( ({ section, reason }) => section === "metadata" && reason === "invalid_protocol_value", ), ).toBe(true); }); }); describe("CDP frame capture", () => { it("walks deeply nested frame trees and returns every frame and resource", () => { let frameTree: Record = { frame: { id: "frame-0", url: `${origin}/0` }, resources: [{ url: `${origin}/0.js`, type: "Script" }], }; for (let index = 1; index < 2_000; index += 1) { frameTree = { frame: { id: `frame-${String(index)}`, url: `${origin}/${String(index)}`, }, resources: [{ url: `${origin}/${String(index)}.js`, type: "Script" }], childFrames: [frameTree], }; } const result = { frameTree }; const frames = captureFrames(result, new Set([origin])); const resources = captureResources(result, new Set([origin])); expect(frames.items).toHaveLength(2_000); expect(resources.items).toHaveLength(2_000); }); }); describe("CDP accessibility capture", () => { it("retains full approved accessibility text", () => { const capture = captureAccessibility( [ { nodes: [ { nodeId: "node-1", role: { value: "button" }, name: { value: "😀a" }, description: { value: "éé" }, childIds: ["missing-child"], properties: [ { name: "disabled", value: { value: true } }, { name: "valuetext", value: { value: "private value" } }, ], }, ], }, ], { includeText: true, }, ); expect(capture.nodes[0]).toMatchObject({ name: "😀a", description: "éé", states: [{ name: "disabled", value: true }], }); expect(capture.treeIncomplete).toBe(true); expect(capture.textCapture).toEqual({ status: "included", retained_bytes: 9, excluded_fields: 0, }); }); it("counts accessibility fields omitted without text approval", () => { const capture = captureAccessibility( [ { nodes: [ { nodeId: "node-1", name: { value: "private name" }, description: { value: "private description" }, }, ], }, ], { includeText: false, }, ); expect(capture.nodes[0]).toMatchObject({ name: null, description: null }); expect(capture.textCapture).toEqual({ status: "not_approved", retained_bytes: 0, excluded_fields: 2, }); }); });