/** * Session Find — the pure matcher plus the DOM seam (owner, 2026-10-13). * The DOM seam runs under linkedom (styles resolve to defaults: display * block/inline behave; the hidden/overlay paths are exercised through the * selectors and inline styles linkedom DOES evaluate). The highlight/ * scroll lifecycle itself is browser-verified (SessionFind on the live * page), not re-implemented here. */ import React from "react"; (globalThis as Record).React = React; import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { parseHTML } from "linkedom"; import { findSessionTextMatches, stepSessionMatch } from "../src/core/session-find.ts"; import { sessionFindRanges, sessionRangeIdentity, sameSessionRange } from "../components/session-find-dom.ts"; import { clearSessionFindHighlights, injectFindStyles, sessionFindHasForegroundLayer } from "../components/session-find.tsx"; const dom = parseHTML(""); (globalThis as Record).window = dom.window; (globalThis as Record).document = dom.document; // linkedom ships no Highlight registry — stub the one field the clearer reads. (globalThis as Record).CSS = dom.window.CSS ?? { highlights: new Map() }; describe("findSessionTextMatches — literal, case-insensitive, regex-inert", () => { it("finds every literal occurrence, any case — substrings included", () => { assert.deepEqual(findSessionTextMatches("Cat and CAT", "cat"), [ { start: 0, end: 3 }, { start: 8, end: 11 }, ]); assert.deepEqual(findSessionTextMatches("scat", "cat"), [{ start: 1, end: 4 }]); }); it("regex metacharacters are inert", () => { assert.deepEqual(findSessionTextMatches("a.b axb ?*+ (x) [y]", "a.b"), [{ start: 0, end: 3 }]); assert.deepEqual(findSessionTextMatches("cost $5 now", "$5"), [{ start: 5, end: 7 }]); assert.deepEqual(findSessionTextMatches("a\\b", "a\\b"), [{ start: 0, end: 3 }]); }); it("empty query matches nothing; no query, no ranges", () => { assert.deepEqual(findSessionTextMatches("anything", ""), []); }); it("unicode-aware, and offsets are UTF-16 into the text as-is", () => { assert.deepEqual(findSessionTextMatches("α αα", "αα"), [{ start: 2, end: 4 }]); }); }); describe("stepSessionMatch — wraparound navigation", () => { it("steps forward and wraps; backward and wraps; empty stays empty", () => { assert.equal(stepSessionMatch(0, 3, 1), 1); assert.equal(stepSessionMatch(2, 3, 1), 0); assert.equal(stepSessionMatch(0, 3, -1), 2); assert.equal(stepSessionMatch(1, 3, -1), 0); assert.equal(stepSessionMatch(-1, 0, 1), -1); }); }); describe("sessionFindRanges — the displayed-text DOM seam", () => { function mount(html: string): void { document.body.innerHTML = html; } it("matches text spanning inline markup, never across blocks", () => { mount(`

chain rule lives here

chain rule

`); const ranges = sessionFindRanges(document.body as unknown as HTMLElement, "chain rule"); assert.equal(ranges.length, 2, "both paragraphs match; the first spans its "); const first = ranges[0]!; assert.equal(first.startContainer.nodeName, "#text"); assert.equal(first.toString().length, "chain rule".length); }); it("searches only marked session roots — chrome, dialogs, and the composer stay out", () => { mount(`

needle in the session

needle in chrome

needle in a dialog

`); const ranges = sessionFindRanges(document.body as unknown as HTMLElement, "needle"); assert.equal(ranges.length, 1); assert.ok((ranges[0]!.startContainer as Text).data.includes("session")); }); it("hidden subtrees never match: hidden attr, menus, katex MathML duplicate, draft editors (display:none is browser-verified)", () => { mount(`

needle visible

needle menu

needle mathml
needle path draft notes.pdf
`); const ranges = sessionFindRanges(document.body as unknown as HTMLElement, "needle"); assert.equal(ranges.length, 1); assert.ok((ranges[0]!.startContainer as Text).data.includes("visible")); }); it("collapsed whitespace matches: a phrase with spaces/tabs/newlines between words", () => { mount(`

the\n\tchain rule

`); const ranges = sessionFindRanges(document.body as unknown as HTMLElement, "the chain rule"); assert.equal(ranges.length, 1); }); it("code spans keep their literal whitespace (pre): no collapse", () => { mount(`
a  b

a b

`); assert.equal(sessionFindRanges(document.body as unknown as HTMLElement, "a b").length, 1); assert.equal(sessionFindRanges(document.body as unknown as HTMLElement, "a b").length, 1); }); it("empty query returns no ranges", () => { mount(`

text

`); assert.equal(sessionFindRanges(document.body as unknown as HTMLElement, "").length, 0); }); it("identity survives text appended after the match (streaming)", () => { mount(`

findme and more to come

`); const [range] = sessionFindRanges(document.body as unknown as HTMLElement, "findme"); const identity = sessionRangeIdentity(range!); const p = document.querySelector("p")!; p.firstChild!.textContent = "findme and more text streamed in"; const [again] = sessionFindRanges(document.body as unknown as HTMLElement, "findme"); assert.ok(sameSessionRange(again!, identity), "same node/offsets: refresh holds the active match"); }); }); describe("sessionFindHasForegroundLayer — the one-Esc law", () => { const mount = (html: string): void => { document.body.innerHTML = html; }; it("no surfaced layer when only session content exists", () => { mount(`

needle

`); assert.equal(sessionFindHasForegroundLayer(document), false); }); it("a surfaced menu/dialog/cheat sheet outranks the Find bar", () => { for (const html of [ '
', '
', '
', '
', ]) { mount(html); assert.equal(sessionFindHasForegroundLayer(document), true, html); } }); it("a closed Radix layer is not surfaced", () => { mount(`
`); assert.equal(sessionFindHasForegroundLayer(document), false); }); }); describe("injectFindStyles — ::highlight rules ship outside the PostCSS pipeline", () => { const mount = (html: string): void => { document.body.innerHTML = html; }; it("injects both rules once, idempotently", () => { document.head.innerHTML = ""; injectFindStyles(); injectFindStyles(); const styles = [...document.querySelectorAll("style")]; assert.equal(styles.length, 1, "one style element, never duplicated"); const css = styles[0]!.textContent ?? ""; assert.ok(css.includes("::highlight(lh-session-find) {")); assert.ok(css.includes("::highlight(lh-session-find-active) {")); }); }); describe("clearSessionFindHighlights — browser-API shaped, linkedom-safe", () => { const mount = (html: string): void => { document.body.innerHTML = html; }; it("clears registered highlights without touching the DOM", () => { mount(`

needle

`); const [range] = sessionFindRanges(document.body as unknown as HTMLElement, "needle"); // linkedom ships no Highlight constructor — the publisher guards on it. (CSS as { highlights?: Map }).highlights = new Map([["lh-session-find", {}]]); clearSessionFindHighlights(); assert.equal((CSS as { highlights?: Map }).highlights!.size, 0); assert.ok(range!, "a range existed and the DOM beneath it is untouched"); }); });