Files
reverse-engineering-anything/src/browser/CdpCaptureDocuments.test.ts
T

356 lines
10 KiB
TypeScript

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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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,
});
});
});