import { describe, expect, it } from "vitest"; import { diffLines, diffStats, renderDiffHtml } from "./artifactDiff"; describe("diffLines", () => { it("returns only context lines for identical content", () => { const lines = diffLines("a\nb\nc", "a\nb\nc"); expect(lines).toEqual([ { type: "context", text: "a" }, { type: "context", text: "b" }, { type: "context", text: "c" }, ]); expect(diffStats(lines)).toEqual({ added: 0, removed: 0 }); }); it("emits deletion before addition for a replaced line", () => { expect(diffLines("a\nb\nc", "a\nB\nc")).toEqual([ { type: "context", text: "a" }, { type: "del", text: "b" }, { type: "add", text: "B" }, { type: "context", text: "c" }, ]); }); it("handles pure insertions and deletions", () => { expect(diffLines("a\nc", "a\nb\nc")).toEqual([ { type: "context", text: "a" }, { type: "add", text: "b" }, { type: "context", text: "c" }, ]); expect(diffLines("a\nb\nc", "a\nc")).toEqual([ { type: "context", text: "a" }, { type: "del", text: "b" }, { type: "context", text: "c" }, ]); }); it("handles changes at the very start and end", () => { expect(diffLines("x\nb", "y\nb")).toEqual([ { type: "del", text: "x" }, { type: "add", text: "y" }, { type: "context", text: "b" }, ]); expect(diffLines("a\nx", "a\ny")).toEqual([ { type: "context", text: "a" }, { type: "del", text: "x" }, { type: "add", text: "y" }, ]); }); it("keeps unchanged lines between multiple change regions", () => { expect(diffLines("a\nb\nc\nd\ne", "a\nB\nc\nd\nE")).toEqual([ { type: "context", text: "a" }, { type: "del", text: "b" }, { type: "add", text: "B" }, { type: "context", text: "c" }, { type: "context", text: "d" }, { type: "del", text: "e" }, { type: "add", text: "E" }, ]); }); it("ignores a trailing newline difference", () => { expect(diffLines("a\nb\n", "a\nb")).toEqual([ { type: "context", text: "a" }, { type: "context", text: "b" }, ]); }); it("falls back to delete-all/add-all when the changed region is huge", () => { const oldText = Array.from({ length: 1100 }, (_, i) => `old ${i}`).join("\n"); const newText = Array.from({ length: 1100 }, (_, i) => `new ${i}`).join("\n"); const lines = diffLines(oldText, newText); expect(lines).toHaveLength(2200); expect(lines.slice(0, 1100).every((l) => l.type === "del")).toBe(true); expect(lines.slice(1100).every((l) => l.type === "add")).toBe(true); }); it("counts added and removed lines", () => { const lines = diffLines("a\nb", "a\nB\nc"); expect(diffStats(lines)).toEqual({ added: 2, removed: 1 }); }); }); describe("renderDiffHtml", () => { it("tints changed lines and keeps context plain", () => { const html = renderDiffHtml([ { type: "context", text: "a" }, { type: "del", text: "b" }, { type: "add", text: "B" }, ]); const lines = html.split("\n"); expect(lines[0]).toBe(" a"); expect(lines[1]).toBe( `- b` ); expect(lines[2]).toBe( `+ B` ); }); it("escapes HTML in line content", () => { const html = renderDiffHtml([{ type: "add", text: `` }]); expect(html).toContain("<img & y>"); expect(html).not.toContain(" { // Fake highlighter wrapping the whole document in one token span const highlight = (text: string) => `${text}`; const html = renderDiffHtml( [ { type: "context", text: "a" }, { type: "add", text: "b" }, ], highlight ); const lines = html.split("\n"); // The span is closed and reopened so each rendered line is self-contained expect(lines[0]).toBe(` a`); expect(lines[1]).toBe( `+ ` + `b` ); }); it("emphasizes only the changed segment of a replaced line", () => { const html = renderDiffHtml( diffLines(`const el = create('div');`, `const el = create('span');`) ); const lines = html.split("\n"); expect(lines[0]).toContain(`div`); expect(lines[1]).toContain(`span`); }); it("counts entities as single characters when emphasizing", () => { // The "<" before the change escapes to < (one source character) const html = renderDiffHtml(diffLines(`if (a < b) return x;`, `if (a < b) return y;`)); const lines = html.split("\n"); expect(lines[0]).toContain(`<`); expect(lines[0]).toContain(`x`); expect(lines[1]).toContain(`y`); }); it("splits the emphasis chip around token tags instead of crossing them", () => { const highlight = (text: string) => text.replace(/\+/g, `+`); const html = renderDiffHtml(diffLines(`foo = bar1 + x;`, `foo = bar2 + y;`), highlight); const addLine = html.split("\n")[1]; // The changed segment crosses the operator token: the chip closes before // the tag and reopens inside it, so nesting stays valid expect(addLine).toContain(`2 `); expect(addLine).toContain( `+` ); expect(addLine).toContain(` y`); }); it("skips emphasis when the paired lines share almost nothing", () => { const html = renderDiffHtml([ { type: "del", text: "aaaaaaaaaa" }, { type: "add", text: "zzzzzzzzzz" }, ]); expect(html).not.toContain("diff-emph"); expect(html).toContain("diff-del"); expect(html).toContain("diff-add"); }); });