158 lines
5.5 KiB
TypeScript
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();
|
|
});
|
|
});
|