// @vitest-environment jsdom
//
// End-to-end smoke test for the optional smart-dom-reader entry, exercising the
// vendored library under jsdom. The pure-mapper correctness guarantee lives in
// utils/smart-dom-adapter.test.ts (no DOM, no library); this test confirms the
// vendored runtime loads and the provider wires up against a real document.
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
collectSmartDomContext,
createSmartDomReaderContextProvider
} from "./smart-dom-reader";
// jsdom implements no layout, so getBoundingClientRect()/offsetParent report the
// element as zero-size and the library's visibility filter would drop everything.
// includeHidden bypasses that filter so we can exercise real extraction under jsdom.
const JSDOM_OPTS = { extractionOptions: { includeHidden: true } } as const;
// This vitest jsdom environment doesn't expose CSS.escape (real browsers do). The
// vendored library calls it unguarded during selector generation, so shim it with the
// same fallback dom-context.ts uses.
function ensureCssEscape(): void {
const g = globalThis as unknown as { CSS?: { escape?: (s: string) => string } };
if (!g.CSS) g.CSS = {};
if (typeof g.CSS.escape !== "function") {
g.CSS.escape = (str: string) => str.replace(/([^\w-])/g, "\\$1");
}
}
describe("smart-dom-reader entry (jsdom)", () => {
beforeEach(() => {
ensureCssEscape();
document.body.innerHTML = "";
});
afterEach(() => {
document.body.innerHTML = "";
});
it("collects interactive elements from the light DOM", () => {
document.body.innerHTML = `
View cart
`;
const elements = collectSmartDomContext(JSDOM_OPTS);
const selectors = elements.map((e) => e.selector).join(" ");
// At minimum the button and link should be discovered and actionable.
expect(elements.length).toBeGreaterThan(0);
expect(selectors).toMatch(/checkout|Checkout/i);
});
it("pierces shadow DOM (full mode), surfacing elements the default TreeWalker reader misses", () => {
// The library pierces shadow roots in full mode, attaching shadow descendants as
// children of a semantic-container host; the adapter flattens those children. The
// default dom-context.ts TreeWalker cannot reach into shadow trees at all.
const host = document.createElement("article");
host.id = "wc-host";
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = ``;
const elements = collectSmartDomContext({
mode: "full",
extractionOptions: { includeHidden: true }
});
const texts = elements.map((e) => e.text).join(" | ");
expect(texts).toContain("Shadow action");
});
it("excludes the widget host (.persona-host) from results", () => {
document.body.innerHTML = `
`;
const elements = collectSmartDomContext(JSDOM_OPTS);
const selectors = elements.map((e) => e.selector);
expect(selectors.some((s) => s.includes("persona-host"))).toBe(false);
expect(elements.some((e) => e.text === "Buy now")).toBe(true);
expect(elements.some((e) => e.text === "Send")).toBe(false);
});
it("scopes extraction to `root`, ignoring elements outside the subtree", () => {
document.body.innerHTML = `
`;
const root = document.getElementById("content")!;
const elements = collectSmartDomContext({
root,
extractionOptions: { includeHidden: true }
});
expect(elements.some((e) => e.text === "Buy now")).toBe(true);
expect(elements.some((e) => e.text === "Sign up")).toBe(false);
});
it("pierces shadow DOM within a scoped `root`", () => {
const host = document.createElement("article");
host.id = "scoped-host";
const main = document.createElement("main");
main.id = "scoped-content";
main.appendChild(host);
document.body.appendChild(main);
host.attachShadow({ mode: "open" }).innerHTML =
``;
const elements = collectSmartDomContext({
root: main,
mode: "full",
extractionOptions: { includeHidden: true }
});
const texts = elements.map((e) => e.text).join(" | ");
expect(texts).toContain("Scoped shadow action");
});
it("provider returns formatted context under the configured key", async () => {
document.body.innerHTML = ``;
const provider = createSmartDomReaderContextProvider({
contextKey: "pageContext",
...JSDOM_OPTS
});
const result = await provider({ messages: [], config: {} as never });
expect(result).toBeTruthy();
expect(typeof (result as Record).pageContext).toBe("string");
expect((result as Record).pageContext).toContain("Continue");
});
it("returns [] for an empty document", () => {
document.body.innerHTML = "";
expect(collectSmartDomContext(JSDOM_OPTS)).toEqual([]);
});
});