/** * Unit tests for dumpDom. * * We use a lightweight fake DOM rather than jsdom to avoid ESM-dependency * transform issues. Each fake element is a plain JS object that duck-types * the DOM API used by the dumpDom evaluate payload. The fake `page` * captures the evaluate callback and runs it via new Function() with the * fake globals injected as named parameters. */ import { dumpDom, DomNode } from "../src/code-executor/dom-helpers"; // --------------------------------------------------------------------------- // Fake DOM builder // --------------------------------------------------------------------------- interface FakeText { nodeType: 3; textContent: string; } interface FakeEl { tagName: string; children: FakeEl[]; childNodes: FakeText[]; offsetParent: object | null; id: string; type: string; name: string; href: string; checked: boolean; disabled: boolean; innerText: string; textContent: string; getAttribute(name: string): string | null; _display: string; _visibility: string; } type ElAttrs = Partial<{ role: string; "data-testid": string; "data-qa": string; "data-action": string; "data-element-id": string; id: string; type: string; name: string; href: string; checked: boolean; disabled: boolean; style: string; }>; function el( tag: string, attrs: ElAttrs = {}, ...children: (FakeEl | string)[] ): FakeEl { const ownTextNodes: FakeText[] = children .filter((c): c is string => typeof c === "string") .map((t) => ({ nodeType: 3, textContent: t })); const childEls = children.filter((c): c is FakeEl => typeof c !== "string"); const deepText = children .map((c) => (typeof c === "string" ? c : c.innerText)) .join(""); const style = attrs.style ?? ""; const display = /display\s*:\s*none/.test(style) ? "none" : ""; const visibility = /visibility\s*:\s*hidden/.test(style) ? "hidden" : ""; const attrMap: Record = {}; for (const key of [ "role", "data-testid", "data-qa", "data-action", "data-element-id", ] as const) { if (attrs[key] != null) attrMap[key] = attrs[key] as string; } return { tagName: tag.toUpperCase(), children: childEls, childNodes: ownTextNodes, offsetParent: display || visibility ? null : {}, id: attrs.id ?? "", type: attrs.type ?? "", name: attrs.name ?? "", href: attrs.href ? attrs.href.startsWith("http") ? attrs.href : `https://example.com${attrs.href}` : "", checked: !!attrs.checked, disabled: !!attrs.disabled, innerText: deepText, textContent: deepText, getAttribute(name: string) { return attrMap[name] ?? null; }, _display: display, _visibility: visibility, }; } function body(...children: (FakeEl | string)[]): FakeEl { return el("body", {}, ...children); } // ── Fake page ────────────────────────────────────────────────────────────── function findByTag(root: FakeEl, tag: string): FakeEl | null { if (root.tagName.toLowerCase() === tag.toLowerCase()) return root; for (const child of root.children) { const found = findByTag(child, tag); if (found) return found; } return null; } function makePage(rootEl: FakeEl) { const fakeDocument = { querySelector(sel: string): FakeEl | null { if (sel.startsWith("#") || sel.startsWith("[") || sel.startsWith(".")) return null; return findByTag(rootEl, sel); }, }; const fakeWindow = { getComputedStyle: (e: FakeEl) => ({ display: e._display, visibility: e._visibility, }), location: { origin: "https://example.com" }, }; const fakeNode = { TEXT_NODE: 3 }; const evaluate = jest .fn() .mockImplementation( (fn: (...args: unknown[]) => unknown, args: unknown) => { const exec = new Function( "document", "window", "Node", "__args__", `return (${fn.toString()})(__args__)`, ); return Promise.resolve(exec(fakeDocument, fakeWindow, fakeNode, args)); }, ); return { evaluate } as unknown as import("playwright").Page; } const dump = (rootEl: FakeEl, sel?: string): Promise => dumpDom(makePage(rootEl), sel); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("dumpDom", () => { // ── Error handling ──────────────────────────────────────────────────────── it("returns an ERROR node when the root selector is not found", async () => { const result = await dump(body(el("p", {}, "hello")), "#does-not-exist"); expect(result.tag).toBe("ERROR"); expect(result.text).toContain("#does-not-exist"); }); // ── Scope & defaults ────────────────────────────────────────────────────── it("defaults to body scope", async () => { const result = await dump(body(el("button", {}, "Go"))); expect(result.tag).toBe("body"); }); it("scopes to an arbitrary sub-selector", async () => { const root = body( el("header", {}, el("a", { href: "/nav" }, "Nav")), el("main", {}, el("button", {}, "Action")), ); const result = await dump(root, "main"); expect(result.tag).toBe("main"); expect(result.children.find((c) => c.tag === "header")).toBeUndefined(); }); // ── Visibility filtering ────────────────────────────────────────────────── it("skips elements with display:none", async () => { const root = body( el("button", { style: "display:none" }, "Hidden"), el("button", {}, "Visible"), ); const result = await dump(root); const btns = result.children.filter((c) => c.tag === "button"); expect(btns).toHaveLength(1); expect(btns[0].text).toBe("Visible"); }); it("skips elements with visibility:hidden", async () => { const root = body( el("button", { style: "visibility:hidden" }, "Hidden"), el("button", {}, "Visible"), ); const result = await dump(root); const btns = result.children.filter((c) => c.tag === "button"); expect(btns).toHaveLength(1); expect(btns[0].text).toBe("Visible"); }); // ── Ignored tag types ───────────────────────────────────────────────────── it("skips SVG elements", async () => { const svgEl = el("svg", {}, el("path", {})); const result = await dump(body(el("button", {}, svgEl, "Click"))); const btn = result.children.find((c) => c.tag === "button"); expect(btn).toBeDefined(); expect(btn!.children.some((c) => c.tag === "svg")).toBe(false); }); it("skips SCRIPT elements", async () => { const result = await dump( body(el("script", {}, "alert(1)"), el("button", {}, "OK")), ); expect(result.children.some((c) => c.tag === "script")).toBe(false); }); // ── Semantic attributes ─────────────────────────────────────────────────── it("captures data-testid", async () => { const result = await dump( body(el("button", { "data-testid": "save-btn" }, "Save")), ); expect(result.children.find((c) => c.tag === "button")?.testid).toBe( "save-btn", ); }); it("captures data-qa", async () => { const result = await dump( body( el("div", { "data-qa": "process-name" }, el("input", { type: "text" })), ), ); expect(result.children.find((c) => c.qa === "process-name")).toBeDefined(); }); it("captures data-action", async () => { const result = await dump( body(el("div", { "data-action": "append.append-task" })), ); expect( result.children.find((c) => c.action === "append.append-task"), ).toBeDefined(); }); it("captures data-element-id", async () => { const result = await dump( body(el("div", { "data-element-id": "Activity_1abc" })), ); expect( result.children.find((c) => c.elementId === "Activity_1abc"), ).toBeDefined(); }); it("captures role attribute", async () => { const result = await dump( body(el("div", { role: "dialog" }, el("button", {}, "OK"))), ); const dialog = result.children.find((c) => c.role === "dialog"); expect(dialog).toBeDefined(); expect(dialog!.tag).toBe("div"); }); // ── Interactive element attributes ──────────────────────────────────────── it("captures input id, type, and name", async () => { const result = await dump( body(el("input", { id: "email", type: "email", name: "userEmail" })), ); const input = result.children.find((c) => c.tag === "input"); expect(input?.id).toBe("email"); expect(input?.type).toBe("email"); expect(input?.name).toBe("userEmail"); }); it("captures checked:true on a checked checkbox", async () => { const result = await dump( body(el("input", { type: "checkbox", checked: true })), ); expect(result.children.find((c) => c.tag === "input")?.checked).toBe(true); }); it("captures checked:false on an unchecked checkbox", async () => { const result = await dump(body(el("input", { type: "checkbox" }))); expect(result.children.find((c) => c.tag === "input")?.checked).toBe(false); }); it("captures disabled:true on a disabled button", async () => { const result = await dump(body(el("button", { disabled: true }, "Nope"))); expect(result.children.find((c) => c.tag === "button")?.disabled).toBe( true, ); }); it("does not set disabled for a non-disabled button", async () => { const result = await dump(body(el("button", {}, "OK"))); expect( result.children.find((c) => c.tag === "button")?.disabled, ).toBeUndefined(); }); it("does not capture type for button elements", async () => { const result = await dump(body(el("button", { type: "submit" }, "Go"))); expect( result.children.find((c) => c.tag === "button")?.type, ).toBeUndefined(); }); it("relativizes same-origin anchor href", async () => { const result = await dump(body(el("a", { href: "/workflow/123" }, "Link"))); expect(result.children.find((c) => c.tag === "a")?.href).toBe( "/workflow/123", ); }); it("keeps full href for cross-origin anchors", async () => { const result = await dump( body(el("a", { href: "https://other.com/page" }, "Ext")), ); expect(result.children.find((c) => c.tag === "a")?.href).toContain( "other.com", ); }); // ── Text content ────────────────────────────────────────────────────────── it("captures own text content of a button", async () => { const result = await dump(body(el("button", {}, "Save"))); expect(result.children.find((c) => c.tag === "button")?.text).toBe("Save"); }); it("truncates text to 80 characters", async () => { const long = "x".repeat(100); const result = await dump(body(el("button", {}, long))); expect(result.children.find((c) => c.tag === "button")?.text?.length).toBe( 80, ); }); it("falls back to innerText when element has no direct text nodes", async () => { // button wraps a span — no direct text node on button, innerText = span text const result = await dump(body(el("button", {}, el("span", {}, "Nested")))); expect(result.children.find((c) => c.tag === "button")?.text).toBe( "Nested", ); }); // ── Tree pruning / unwrapping ───────────────────────────────────────────── it("unwraps a non-significant div that has exactly one significant child", async () => { const result = await dump(body(el("div", {}, el("button", {}, "Click")))); const btn = result.children.find((c) => c.tag === "button"); expect(btn).toBeDefined(); expect( result.children.some( (c) => c.tag === "div" && !c.role && !c.testid && !c.qa, ), ).toBe(false); }); it("keeps a non-significant div that has more than one significant child", async () => { const result = await dump( body(el("div", {}, el("button", {}, "A"), el("button", {}, "B"))), ); const wrapper = result.children.find((c) => c.tag === "div"); expect(wrapper).toBeDefined(); expect(wrapper!.children).toHaveLength(2); }); it("discards non-significant childless elements", async () => { const result = await dump(body(el("div", {}), el("button", {}, "Keep"))); expect( result.children.find((c) => c.tag === "div" && c.children.length === 0), ).toBeUndefined(); expect(result.children.some((c) => c.tag === "button")).toBe(true); }); // ── Structural tags ─────────────────────────────────────────────────────── it("preserves nested structure inside a form", async () => { const result = await dump( body( el( "form", {}, el("input", { id: "n", type: "text", name: "name" }), el("button", {}, "Send"), ), ), ); const form = result.children.find((c) => c.tag === "form"); expect(form).toBeDefined(); expect(form!.children.find((c) => c.tag === "input")).toBeDefined(); expect(form!.children.find((c) => c.tag === "button")).toBeDefined(); }); it("preserves dialog element", async () => { const result = await dump( body(el("dialog", { role: "dialog" }, el("button", {}, "Close"))), ); expect(result.children.find((c) => c.tag === "dialog")).toBeDefined(); }); it("returns empty node when root has no visible significant content", async () => { const result = await dumpDom(makePage(el("div", {})), "div"); expect(result.tag).toBe("empty"); }); });