import { assert, assertEquals } from "@std/assert"; import { withDom } from "@berry/util/dom.ts"; import { rendering } from "./render.tsx"; import type { FileDiff } from "./types.ts"; const file = (lines: string[], path = "file.txt"): FileDiff => ({ path, from: null, status: "modified", modes: { old: "100644", new: "100644" }, binary: false, hunks: [{ old: 1, new: 1, lines }], }); async function page( test: (env: { root: HTMLElement; render: ReturnType; intersect(element: Element, near: boolean, visible?: boolean): void; settle(): Promise; }) => Promise, globals: Record = {}, ) { const observers: Visibility[] = []; class Visibility { targets = new Set(); constructor( readonly callback: IntersectionObserverCallback, readonly options?: IntersectionObserverInit, ) { observers.push(this); } observe(target: Element) { this.targets.add(target); } unobserve(target: Element) { this.targets.delete(target); } disconnect() { this.targets.clear(); } } await withDom(async (window) => { const root = document.createElement("div"); document.body.append(root); const render = rendering(root); try { await test({ root, render, intersect(element, near, visible = near) { for (const observer of observers) { if (!observer.targets.has(element)) continue; observer.callback( [{ target: element, isIntersecting: observer.options?.rootMargin ? near : visible, } as IntersectionObserverEntry], observer as unknown as IntersectionObserver, ); } }, settle: () => window.happyDOM.waitUntilComplete(), }); } finally { render.dispose(); } for (const observer of observers) assertEquals(observer.targets.size, 0); }, { globals: { IntersectionObserver: Visibility, ...globals } }); } Deno.test("large diffs keep all text searchable but only nearby chunks have rich rows", () => page(async ({ root, render, intersect, settle }) => { const lines = Array.from({ length: 10_000 }, (_, i) => `+unique line ${i}`); const content = render.file(file(lines), "unified", new Set(), () => {}); root.append(content.element); const chunks = [...root.querySelectorAll(".diff-chunk")]; assert(chunks.length > 10); assertEquals(root.querySelectorAll("table").length, 0); assert(root.textContent!.includes("unique line 9999")); assert(root.querySelectorAll("*").length < 1000); let previous: Element | undefined; for (const chunk of [chunks[0], chunks[1], chunks.at(-1)!]) { if (previous) intersect(previous, false); intersect(chunk, true); await settle(); assertEquals(root.querySelectorAll("table").length, 1); assert(root.querySelectorAll("tr").length < 200); assert(root.textContent!.includes("unique line 9999")); if (previous) assert(previous.querySelector("pre")); previous = chunk; } intersect(chunks[0], true); content.dispose(); await settle(); assertEquals(chunks[0].querySelector("table"), null); })); Deno.test("hydrating and evicting chunks leave native find matches and selections intact", () => page(async ({ root, render, intersect, settle }) => { const content = render.file(file(["+a searchable needle"]), "unified", new Set(), () => {}); root.append(content.element); const chunk = root.querySelector(".diff-chunk")!; const pre = chunk.querySelector("pre")!; const selection = document.getSelection()!; const range = document.createRange(); range.setStart(pre.firstChild!, 2); range.setEnd(pre.firstChild!, 12); selection.addRange(range); intersect(chunk, true); await settle(); assertEquals(chunk.querySelector("pre"), pre); assertEquals(selection.toString(), "searchable"); selection.removeAllRanges(); document.dispatchEvent(new Event("selectionchange")); await settle(); const table = chunk.querySelector("table")!; assert(table); range.selectNodeContents(table.querySelector("td.text")!); selection.addRange(range); intersect(chunk, false); await settle(); assertEquals(chunk.querySelector("table"), table); assertEquals(selection.toString(), "a searchable needle"); selection.removeAllRanges(); document.dispatchEvent(new Event("selectionchange")); await settle(); assertEquals(chunk.querySelector("table"), null); assertEquals(chunk.querySelector("pre")!.textContent, "a searchable needle\n"); content.dispose(); })); Deno.test("line pairing and word changes survive chunk boundaries in either layout", async () => { for (const layout of ["unified", "split"] as const) { await page(async ({ root, render, intersect, settle }) => { const lines = [ ...Array.from({ length: 150 }, (_, i) => `-const before_${i} = value;`), ...Array.from({ length: 150 }, (_, i) => `+const after_${i} = value;`), ]; const content = render.file(file(lines), layout, new Set(), () => {}); root.append(content.element); for (const chunk of root.querySelectorAll(".diff-chunk")) intersect(chunk, true); await settle(); assertEquals(root.querySelectorAll("td.text.del").length, 150); assertEquals(root.querySelectorAll("td.text.add").length, 150); assertEquals(root.querySelector("td.del mark.word")!.textContent, "before_0"); assertEquals(root.querySelector("td.add mark.word")!.textContent, "after_0"); if (layout === "split") { for (const [i, row] of [...root.querySelectorAll("tr")].entries()) { assertEquals(row.querySelector(".del")!.textContent, `const before_${i} = value;`); assertEquals(row.querySelector(".add")!.textContent, `const after_${i} = value;`); } } content.dispose(); }); } }); Deno.test("syntax work waits for visible chunks, skips departed files, and releases offscreen results", async () => { const requests: { id: number; source: string }[] = []; let worker: Highlighter | undefined; class Highlighter { onmessage?: (event: { data: { id: number; html: string } }) => void; constructor() { worker = this; } postMessage(request: typeof requests[number]) { requests.push(request); } reply() { for (const { id, source } of requests.splice(0)) { this.onmessage?.({ data: { id, html: `${source}` } }); } } } await page(async ({ root, render, intersect, settle }) => { const first = render.file(file(["-old", "+new"], "first.ts"), "unified", new Set(), () => {}); const second = render.file(file(["+second"], "second.ts"), "unified", new Set(), () => {}); root.append(first.element, second.element); const a = first.element.querySelector(".diff-chunk")!; const b = second.element.querySelector(".diff-chunk")!; intersect(a, true, false); await settle(); assertEquals(worker, undefined); intersect(a, true); intersect(b, true); await settle(); assertEquals(requests.map((request) => request.source), ["old", "new"]); intersect(a, false); intersect(b, false); await settle(); worker!.reply(); await settle(); assertEquals(requests.length, 0); assertEquals(root.querySelectorAll("a-k, table").length, 0); intersect(a, true); await settle(); assertEquals(requests.map((request) => request.source), ["old", "new"]); const table = a.querySelector("table")!; const selection = document.getSelection()!; const range = document.createRange(); range.selectNodeContents(a.querySelector("td.text")!); selection.addRange(range); worker!.reply(); await settle(); assertEquals(a.querySelector("table"), table); assertEquals(a.querySelector("a-k"), null); assertEquals(selection.toString(), "old"); selection.removeAllRanges(); document.dispatchEvent(new Event("selectionchange")); await settle(); assertEquals(a.querySelector("a-k")?.textContent, "old"); intersect(a, true); await settle(); assertEquals(requests.length, 0); intersect(b, true); await settle(); assertEquals(requests.map((request) => request.source), ["second"]); second.dispose(); second.element.remove(); worker!.reply(); await settle(); assertEquals(second.element.querySelector("a-k"), null); first.dispose(); }, { Worker: Highlighter }); });