Files
kandev/apps/web/components/diff/use-diff-viewer-state.test.ts
T

158 lines
5.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { parsePatchFiles } from "@pierre/diffs";
import type { FileDiffMetadata, DiffLineAnnotation } from "@pierre/diffs";
import {
buildHunkAnnotations,
buildWalkthroughSelectedLines,
type HunkOutputs,
} from "./use-diff-viewer-state";
import type { RevertBlockInfo } from "./diff-viewer";
import type { AnnotationMetadata } from "./use-diff-annotation-renderer";
const FILE = "src/foo.ts";
const DIFF_HEADER = [`diff --git a/${FILE} b/${FILE}`, `--- a/${FILE}`, `+++ b/${FILE}`];
function freshOut(): HunkOutputs {
return {
result: [] as DiffLineAnnotation<AnnotationMetadata>[],
lineMap: new Map<string, string>(),
revertMap: new Map<string, RevertBlockInfo>(),
};
}
function parseSinglePatch(diff: string): FileDiffMetadata {
const patches = parsePatchFiles(diff);
const file = patches[0]?.files[0];
if (!file) throw new Error("parsePatchFiles returned no files");
return file;
}
describe("buildHunkAnnotations", () => {
it("reconstructs revert oldLines by slicing deletionLines via deletionLineIndex", () => {
// Mixed change: 1 context, 2 deletions, 2 additions, 1 trailing context.
const diff = [
...DIFF_HEADER,
"@@ -1,5 +1,5 @@",
" keep",
"-remove one",
"-remove two",
"+add one",
"+add two",
" end",
"",
].join("\n");
const out = freshOut();
buildHunkAnnotations(parseSinglePatch(diff), out);
expect(out.revertMap.size).toBe(1);
const info = [...out.revertMap.values()][0];
// addStart begins after the leading 1 line of context, so line 2.
expect(info.addStart).toBe(2);
expect(info.addCount).toBe(2);
// The library hands us *line text* via deletionLines + deletionLineIndex;
// the hunk-walker is responsible for picking the right slice and stripping
// trailing newlines. If the addressing scheme drifts in a future bump this
// assertion will fail rather than silently delivering wrong "old lines".
expect(info.oldLines).toEqual(["remove one", "remove two"]);
});
it("emits a hunk-actions annotation anchored at the previous context line", () => {
const diff = [...DIFF_HEADER, "@@ -1,3 +1,3 @@", " keep", "-old", "+new", " tail", ""].join(
"\n",
);
const out = freshOut();
buildHunkAnnotations(parseSinglePatch(diff), out);
expect(out.result).toHaveLength(1);
const ann = out.result[0];
// additions side because aLen > 0; anchored on the previous context line (line 1).
expect(ann.side).toBe("additions");
expect(ann.lineNumber).toBe(1);
expect(ann.metadata).toMatchObject({ type: "hunk-actions" });
});
it("maps each added/deleted line to its changeBlock id in lineMap", () => {
const diff = [...DIFF_HEADER, "@@ -1,3 +1,3 @@", " keep", "-old", "+new", " tail", ""].join(
"\n",
);
const out = freshOut();
buildHunkAnnotations(parseSinglePatch(diff), out);
const cbId = [...out.revertMap.keys()][0];
// Line 2 is the modified line on both sides (after the 1-line context prefix).
expect(out.lineMap.get("additions:2")).toBe(cbId);
expect(out.lineMap.get("deletions:2")).toBe(cbId);
});
it("handles a pure-deletion change (aLen === 0): empty addCount, side='deletions'", () => {
const diff = [...DIFF_HEADER, "@@ -1,3 +1,2 @@", " keep", "-removed", " tail", ""].join("\n");
const out = freshOut();
buildHunkAnnotations(parseSinglePatch(diff), out);
expect(out.revertMap.size).toBe(1);
const info = [...out.revertMap.values()][0];
expect(info.addCount).toBe(0);
expect(info.oldLines).toEqual(["removed"]);
expect(out.result[0].side).toBe("deletions");
});
it("handles a pure-addition change (dLen === 0): empty oldLines, side='additions'", () => {
const diff = [...DIFF_HEADER, "@@ -1,2 +1,3 @@", " keep", "+added", " tail", ""].join("\n");
const out = freshOut();
buildHunkAnnotations(parseSinglePatch(diff), out);
expect(out.revertMap.size).toBe(1);
const info = [...out.revertMap.values()][0];
expect(info.addCount).toBe(1);
expect(info.oldLines).toEqual([]);
expect(out.result[0].side).toBe("additions");
});
});
const WALKTHROUGH_PATH = "apps/web/main.ts";
describe("buildWalkthroughSelectedLines", () => {
it("returns the active walkthrough line range for a matching file", () => {
expect(
buildWalkthroughSelectedLines(
{ path: WALKTHROUGH_PATH, repository_name: "frontend" },
{ file: "main.ts", repo: "frontend", line: 10, line_end: 12, text: "explain" },
),
).toEqual({ side: "additions", start: 10, end: 12 });
});
it("returns null for mismatched repos", () => {
expect(
buildWalkthroughSelectedLines(
{ path: WALKTHROUGH_PATH, repository_name: "backend" },
{ file: "main.ts", repo: "frontend", line: 10, text: "explain" },
),
).toBeNull();
});
it("defaults line_end to line when the walkthrough step has no line_end", () => {
expect(
buildWalkthroughSelectedLines(
{ path: WALKTHROUGH_PATH, repository_name: "frontend" },
{ file: "main.ts", repo: "frontend", line: 15, text: "see this" },
),
).toEqual({ side: "additions", start: 15, end: 15 });
});
it("returns null when walkthrough annotations are disabled", () => {
expect(
buildWalkthroughSelectedLines(
{ path: WALKTHROUGH_PATH, repository_name: "frontend" },
{ file: "main.ts", repo: "frontend", line: 15, text: "see this" },
false,
),
).toBeNull();
});
});