// @vitest-environment jsdom
import { describe, expect, it } from "vitest";
import { verifyDocumentAnchorSelector } from "@paperclipai/shared";
import {
buildAnchorFromContainerSelection,
getContainerTextOffset,
rangesForNormalizedSpan,
} from "./document-annotation-selection";
const MARKDOWN = `# Plan
We **should** keep the current markdown stack for the first version.
- Highlight a text segment in a plan document.
- Anchor comments without mutating markdown.
## Acceptance
The annotation feature is ready when the basic flow works.`;
const RENDERED_HTML = `
Plan
We should keep the current markdown stack for the first version.
- Highlight a text segment in a plan document.
- Anchor comments without mutating markdown.
Acceptance
The annotation feature is ready when the basic flow works.
`;
function makeContainer(): HTMLElement {
const div = document.createElement("div");
div.innerHTML = RENDERED_HTML;
document.body.appendChild(div);
return div.firstElementChild as HTMLElement;
}
function selectText(container: HTMLElement, needle: string): Range {
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
let node = walker.nextNode();
while (node) {
const data = (node as Text).data;
const index = data.indexOf(needle);
if (index !== -1) {
const range = document.createRange();
range.setStart(node, index);
range.setEnd(node, index + needle.length);
return range;
}
node = walker.nextNode();
}
throw new Error(`Could not find "${needle}" in container`);
}
describe("buildAnchorFromContainerSelection", () => {
it("produces a selector that verifies against the same markdown", () => {
const container = makeContainer();
const range = selectText(container, "current markdown stack");
const offset = getContainerTextOffset(container, range);
expect(offset).not.toBeNull();
const anchor = buildAnchorFromContainerSelection({
markdown: MARKDOWN,
containerOffset: offset!,
});
expect(anchor).not.toBeNull();
const verified = verifyDocumentAnchorSelector({
markdown: MARKDOWN,
selector: anchor!.selector,
});
expect(verified.ok).toBe(true);
expect(verified.anchor?.selectedText).toBe("current markdown stack");
});
it("returns null for empty selections", () => {
const container = makeContainer();
const range = document.createRange();
range.setStart(container, 0);
range.setEnd(container, 0);
const offset = getContainerTextOffset(container, range);
expect(offset).toBeNull();
});
it("returns null when selection is outside container", () => {
const container = makeContainer();
const outside = document.createElement("div");
outside.textContent = "outside";
document.body.appendChild(outside);
const range = document.createRange();
range.selectNodeContents(outside);
const offset = getContainerTextOffset(container, range);
expect(offset).toBeNull();
});
});
describe("rangesForNormalizedSpan", () => {
it("walks DOM text nodes to find span ranges", () => {
const container = makeContainer();
const ranges = rangesForNormalizedSpan({
container,
selectedText: "Highlight a text segment",
});
expect(ranges.length).toBeGreaterThan(0);
const merged = ranges.map((range) => range.toString()).join("");
expect(merged.replace(/\s+/g, " ")).toContain("Highlight a text segment");
});
it("returns an empty array if selected text is missing", () => {
const container = makeContainer();
const ranges = rangesForNormalizedSpan({
container,
selectedText: "this string does not exist in the document",
});
expect(ranges).toEqual([]);
});
});