import { describe, expect, test } from "bun:test" import { parseHtml, selectorPath } from "./html-lite.mjs" import { buildProbeSource, PATH_FUNCTION_SOURCE, PROBE_VERSION, ProbeError } from "./layout-probe.mjs" // A minimal element adapter over the html-lite tree: exactly the DOM surface the path rule reads. function adapt(node, parentAdapter, cache) { if (cache.has(node)) return cache.get(node) const el = { tagName: node.tag.toUpperCase(), id: node.attrs.id ?? "", parentElement: parentAdapter, children: [] } cache.set(node, el) el.children = node.children.filter((c) => c.tag !== "#text").map((c) => adapt(c, el, cache)) return el } function pathRule() { return new Function(`${PATH_FUNCTION_SOURCE}; return ulwSelectorPath;`)() } const FIXTURE = `

A

one

two

c

B

x

t

y

` describe("PATH_FUNCTION_SOURCE", () => { test("#given the html-lite tree of a fixture #when the in-page path rule runs on an adapter of every element #then it equals html-lite selectorPath", () => { // given const doc = parseHtml(FIXTURE) const ids = new Map() const cache = new Map() const elements = [] const walk = (node, parentAdapter) => { if (node.tag === "#text") return const el = adapt(node, parentAdapter, cache) if (node.tag !== "#root" && node.parent) elements.push([node, el]) if (el.id) ids.set(el.id, (ids.get(el.id) ?? 0) + 1) for (const child of node.children) walk(child, el) } walk(doc, null) const rule = pathRule() // when const mismatches = elements .filter(([node]) => node.tag !== "html" && node.tag !== "head" && node.tag !== "#root") .map(([node, el]) => [selectorPath(node), rule(el, (id) => ids.get(id) ?? 0)]) .filter(([expected, actual]) => expected !== actual) // then expect(elements.length).toBeGreaterThan(10) expect(mismatches).toEqual([]) }) test("#given a duplicated id #when a descendant path is built #then the duplicate is not used as an anchor", () => { const doc = parseHtml(FIXTURE) const p = [] const find = (n) => { if (n.tag === "p" && n.parent?.attrs.id === "dup") p.push(n); n.children.forEach(find) } find(doc) expect(selectorPath(p[0])).toBe("#top > section:nth-of-type(3) > div:nth-of-type(2) > p") }) }) describe("buildProbeSource", () => { test("#given defaults #when built #then it is one async IIFE expression that parses and carries the version, cap, and image wait", () => { const src = buildProbeSource() expect(src).toMatch(/^\(async \(\) => \{[\s\S]*\}\)\(\)\s*$/) expect(src.includes('img.loading = "eager"')).toBe(true) expect(() => new Function(`return ${src}`)).not.toThrow() expect(src.includes("const CAP = 400;")).toBe(true) expect(src.includes(`probe_version: ${PROBE_VERSION}`)).toBe(true) }) test("#given cap 50 and a custom root #when built #then both are embedded as literals", () => { const src = buildProbeSource({ cap: 50, root: "article.report" }) expect(src.includes("const CAP = 50;")).toBe(true) expect(src.includes('document.querySelector("article.report")')).toBe(true) }) test("#given cap 0 or a fractional cap #when built #then a typed ProbeError is thrown", () => { expect(() => buildProbeSource({ cap: 0 })).toThrow(ProbeError) expect(() => buildProbeSource({ cap: 1.5 })).toThrow(ProbeError) }) test("#given an empty root #when built #then a typed ProbeError is thrown", () => { expect(() => buildProbeSource({ root: "" })).toThrow(ProbeError) expect(() => buildProbeSource({ root: " " })).toThrow(ProbeError) }) test("#given a root containing a double quote #when built #then the source still parses", () => { const src = buildProbeSource({ root: 'main[data-x="1"]' }) expect(() => new Function(`return ${src}`)).not.toThrow() }) })