Spaces:
Paused
Paused
File size: 5,874 Bytes
fae625a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | 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(
`<span class="diff-line diff-del"><span class="diff-sign">- </span>b</span>`
);
expect(lines[2]).toBe(
`<span class="diff-line diff-add"><span class="diff-sign">+ </span>B</span>`
);
});
it("escapes HTML in line content", () => {
const html = renderDiffHtml([{ type: "add", text: `<img & y>` }]);
expect(html).toContain("<img & y>");
expect(html).not.toContain("<img");
});
it("keeps syntax highlighting spans that cross line boundaries", () => {
// Fake highlighter wrapping the whole document in one token span
const highlight = (text: string) => `<span class="hljs-string">${text}</span>`;
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(` <span class="hljs-string">a</span>`);
expect(lines[1]).toBe(
`<span class="diff-line diff-add"><span class="diff-sign">+ </span>` +
`<span class="hljs-string">b</span></span>`
);
});
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(`<span class="diff-emph">div</span>`);
expect(lines[1]).toContain(`<span class="diff-emph">span</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(`<span class="diff-emph">x</span>`);
expect(lines[1]).toContain(`<span class="diff-emph">y</span>`);
});
it("splits the emphasis chip around token tags instead of crossing them", () => {
const highlight = (text: string) => text.replace(/\+/g, `<span class="hljs-operator">+</span>`);
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(`<span class="diff-emph">2 </span>`);
expect(addLine).toContain(
`<span class="hljs-operator"><span class="diff-emph">+</span></span>`
);
expect(addLine).toContain(`<span class="diff-emph"> y</span>`);
});
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");
});
});
|