Files
BrowserSkill/apps/extension/src/tools/vom/__tests__/facts.test.ts
T

157 lines
4.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { OVERLAY_HOST_MARKER_ATTR } from "@/lib/overlay-bridge";
import { buildDocumentIndex, type DecodedNode } from "../facts";
import { decodeDocument, REQUESTED_STYLES } from "../snapshot";
function node(id: number, parent: number | null): DecodedNode {
return {
backendNodeId: id,
parentBackendNodeId: parent,
tag: "div",
attrs: {},
paintOrder: 0,
position: "static",
pointerEvents: "auto",
};
}
describe("document facts", () => {
it.each([
1_000, 10_000, 100_000,
])("indexes deep and wide inputs of %i without copying ancestors", async (count) => {
for (const wide of [false, true]) {
let reads = 0;
const input = Array.from({ length: count }, (_, i) => {
const item = node(i, i ? (wide ? 0 : i - 1) : null);
Object.defineProperty(item, "parentBackendNodeId", {
get: () => {
reads++;
return i ? (wide ? 0 : i - 1) : null;
},
});
return item;
}).reverse(); // parent need not precede child
const index = await buildDocumentIndex(input);
expect(index.nodes.size).toBe(count);
expect(index.ancestry.get(count - 1)).toEqual({ complete: true, overlay: false });
expect(index.children.get(0)?.length).toBe(wide ? count - 1 : 1);
expect(reads).toBeLessThanOrEqual(count * 8);
expect(index.nodes.get(count - 1)).toBe(input[0]);
}
});
it("marks cycles and orphans incomplete and propagates overlay through shadow roots", async () => {
const host = { ...node(1, null), attrs: { [OVERLAY_HOST_MARKER_ATTR]: "" } };
const input = [
node(3, 2),
{ ...node(2, 1), tag: "#document-fragment" },
host,
node(4, 99),
node(5, 6),
node(6, 5),
node(7, 6),
];
const index = await buildDocumentIndex(input);
expect([...index.excludedBackendNodeIds].sort()).toEqual([1, 2, 3]);
for (const id of [4, 5, 6, 7]) expect(index.ancestry.get(id)?.complete).toBe(false);
});
it("does not propagate a descendant overlay backwards into a cycle", async () => {
const overlay = { ...node(3, 1), attrs: { [OVERLAY_HOST_MARKER_ATTR]: "" } };
const index = await buildDocumentIndex([overlay, node(1, 2), node(2, 1)]);
expect([...index.excludedBackendNodeIds]).toEqual([3]);
});
it("does not treat a missing snapshot parent as a complete root", async () => {
const { nodes } = await decodeDocument(
{ nodes: { backendNodeId: [1], nodeName: [0], parentIndex: [99] } },
["div"],
);
const index = await buildDocumentIndex(nodes);
expect(index.ancestry.get(1)?.complete).toBe(false);
});
it("retains raw box units and independent style/interaction evidence", async () => {
const strings = [
"html",
"canvas",
"aria-hidden",
"true",
"inert",
"",
"hidden",
"visible",
"0",
"auto",
"scroll",
"matrix(2,0,0,2,0,0)",
];
const styles = REQUESTED_STYLES.map((style) =>
style === "visibility"
? 7
: style === "opacity"
? 8
: style === "overflow-x"
? 10
: style === "transform"
? 11
: 9,
);
const { nodes } = await decodeDocument(
{
nodes: {
backendNodeId: [1, 2],
parentIndex: [-1, 0],
nodeName: [0, 1],
attributes: [[2, 3, 4, 5], []],
},
layout: {
nodeIndex: [0, 1],
bounds: [
[0, 0, 200, 100],
[10, 20, 120, 40],
],
clientRects: [
[5, 5, 90, 40],
[0, 0, 120, 40],
],
styles: [styles, styles],
},
},
strings,
);
expect(nodes[0].attrs).toEqual({ "aria-hidden": "true", inert: "" });
expect(nodes[0].layout).toMatchObject({
boundsSpace: "snapshot-document-css",
clientSpace: "unscaled-client-offset-css",
clientRect: [5, 5, 90, 40],
styles: {
visibility: "visible",
opacity: "0",
"overflow-x": "scroll",
transform: "matrix(2,0,0,2,0,0)",
},
});
expect(nodes[0]).not.toHaveProperty("rect");
expect(nodes[0]).not.toHaveProperty("excluded");
});
it("yields during large inputs so cancellation interrupts indexing", async () => {
const controller = new AbortController();
const input = Array.from({ length: 100_000 }, (_, i) => node(i, i ? i - 1 : null));
let reads = 0;
for (const item of input)
Object.defineProperty(item, "attrs", {
get: () => {
reads++;
if (reads === 100) controller.abort();
return {};
},
});
await expect(buildDocumentIndex(input, controller.signal)).rejects.toMatchObject({
name: "AbortError",
});
expect(reads).toBeLessThanOrEqual(356);
});
});