| 1 | import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; |
| 2 | import type { PullRequestReviewGuideStop } from "@t3tools/contracts"; |
| 3 | import { describe, expect, it } from "vite-plus/test"; |
| 4 | |
| 5 | import { |
| 6 | excerptExpansionOldContents, |
| 7 | excerptRows, |
| 8 | fileDiffRows, |
| 9 | letterStopNotes, |
| 10 | rowsToPatch, |
| 11 | stepTourPosition, |
| 12 | } from "./reviewGuideTour.js"; |
| 13 | |
| 14 | const PATCH = `diff --git a/src/tray.ts b/src/tray.ts |
| 15 | --- a/src/tray.ts |
| 16 | +++ b/src/tray.ts |
| 17 | @@ -10,5 +10,6 @@ function tray() |
| 18 | const a = 1; |
| 19 | -const size = pick(); |
| 20 | +const size = "xs"; |
| 21 | +const variant = "tray"; |
| 22 | const b = 2; |
| 23 | const c = 3; |
| 24 | const d = 4; |
| 25 | @@ -40,3 +41,2 @@ function footer() |
| 26 | keep(); |
| 27 | -dropped(); |
| 28 | tail(); |
| 29 | `; |
| 30 | |
| 31 | function parse(patch: string) { |
| 32 | const file = parsePatchFiles(patch, `test:${patch.length}`)[0]?.files[0]; |
| 33 | if (file === undefined) throw new Error("patch did not parse"); |
| 34 | return file; |
| 35 | } |
| 36 | |
| 37 | const rows = fileDiffRows(parse(PATCH)); |
| 38 | |
| 39 | function stop(overrides: Partial<PullRequestReviewGuideStop>): PullRequestReviewGuideStop { |
| 40 | return { |
| 41 | title: "Stop", |
| 42 | summary: "", |
| 43 | attention: null, |
| 44 | excerpts: [], |
| 45 | files: [], |
| 46 | notes: [], |
| 47 | questions: [], |
| 48 | ...overrides, |
| 49 | }; |
| 50 | } |
| 51 | |
| 52 | describe("stepTourPosition", () => { |
| 53 | it("holds the tour to its first and last stops", () => { |
| 54 | expect(stepTourPosition(0, -1, 3)).toBe(0); |
| 55 | expect(stepTourPosition(0, 1, 3)).toBe(1); |
| 56 | expect(stepTourPosition(2, 1, 3)).toBe(2); |
| 57 | }); |
| 58 | }); |
| 59 | |
| 60 | describe("fileDiffRows", () => { |
| 61 | it("numbers both sides through context, deletions and additions", () => { |
| 62 | expect(rows.map((row) => [row.kind, row.oldLine, row.newLine])).toEqual([ |
| 63 | ["context", 10, 10], |
| 64 | ["deletion", 11, null], |
| 65 | ["addition", null, 11], |
| 66 | ["addition", null, 12], |
| 67 | ["context", 12, 13], |
| 68 | ["context", 13, 14], |
| 69 | ["context", 14, 15], |
| 70 | ["context", 40, 41], |
| 71 | ["deletion", 41, null], |
| 72 | ["context", 42, 42], |
| 73 | ]); |
| 74 | }); |
| 75 | }); |
| 76 | |
| 77 | describe("excerptRows", () => { |
| 78 | it("brings the replaced lines along with an excerpt that starts on an addition", () => { |
| 79 | const shown = excerptRows(rows, { side: "new", start: 11, end: 12 }); |
| 80 | expect(shown.map((row) => row.kind)).toEqual(["deletion", "addition", "addition"]); |
| 81 | }); |
| 82 | |
| 83 | it("selects old-side lines for a removal and stops at the hunk boundary", () => { |
| 84 | const shown = excerptRows(rows, { side: "old", start: 41, end: 41 }); |
| 85 | expect(shown.map((row) => [row.kind, row.oldLine])).toEqual([["deletion", 41]]); |
| 86 | }); |
| 87 | |
| 88 | it("returns nothing for a range outside the diff", () => { |
| 89 | expect(excerptRows(rows, { side: "new", start: 100, end: 120 })).toEqual([]); |
| 90 | }); |
| 91 | }); |
| 92 | |
| 93 | describe("rowsToPatch", () => { |
| 94 | it("cuts a slice that parses back to the same lines and numbers", () => { |
| 95 | const shown = excerptRows(rows, { side: "new", start: 10, end: 13 }); |
| 96 | const slice = parse(rowsToPatch("src/tray.ts", "src/tray.ts", shown)); |
| 97 | expect(fileDiffRows(slice).map((row) => [row.kind, row.oldLine, row.newLine])).toEqual( |
| 98 | shown.map((row) => [row.kind, row.oldLine, row.newLine]), |
| 99 | ); |
| 100 | }); |
| 101 | |
| 102 | it("starts a hunk per source hunk so separate hunks stay separate", () => { |
| 103 | const shown = excerptRows(rows, { side: "old", start: 14, end: 42 }); |
| 104 | const patch = rowsToPatch("src/tray.ts", "src/tray.ts", shown); |
| 105 | expect(patch.match(/^@@ /gm)).toHaveLength(2); |
| 106 | expect(patch).toContain("@@ -14,1 +15,1 @@"); |
| 107 | expect(patch).toContain("@@ -40,3 +41,2 @@"); |
| 108 | }); |
| 109 | }); |
| 110 | |
| 111 | describe("excerptExpansionOldContents", () => { |
| 112 | // The new file behind PATCH: lines 1-9, the hunk at 10-15, then lines up to 41-42 around the |
| 113 | // second hunk, which removed a line. |
| 114 | const newFile = Array.from({ length: 50 }, (_, index) => `line ${index + 1}\n`); |
| 115 | newFile.splice( |
| 116 | 9, |
| 117 | 6, |
| 118 | "const a = 1;\n", |
| 119 | 'const size = "xs";\n', |
| 120 | 'const variant = "tray";\n', |
| 121 | "const b = 2;\n", |
| 122 | "const c = 3;\n", |
| 123 | "const d = 4;\n", |
| 124 | ); |
| 125 | newFile.splice(40, 2, "keep();\n", "tail();\n"); |
| 126 | const newContents = newFile.join(""); |
| 127 | |
| 128 | it("puts back only the excerpt's lines and keeps both sides numbered as in the file", () => { |
| 129 | const shown = excerptRows(rows, { side: "new", start: 11, end: 12 }); |
| 130 | const old = excerptExpansionOldContents(newContents, shown); |
| 131 | if (old === null) throw new Error("expected contents"); |
| 132 | const oldLines = old.split("\n"); |
| 133 | // The excerpt's old side sits at its real old line. |
| 134 | expect(oldLines[10]).toBe("const size = pick();"); |
| 135 | // Around it, the new file's lines, so the two sides agree outside the excerpt. |
| 136 | expect(oldLines[9]).toBe("const a = 1;"); |
| 137 | expect(oldLines[11]).toBe("const b = 2;"); |
| 138 | // One line replaced by two: the old side is a line shorter, everything after lines up. |
| 139 | expect(oldLines.length).toBe(newContents.split("\n").length - 1); |
| 140 | }); |
| 141 | |
| 142 | it("does not try to expand an excerpt that spans hunks", () => { |
| 143 | const shown = excerptRows(rows, { side: "old", start: 14, end: 42 }); |
| 144 | expect(excerptExpansionOldContents(newContents, shown)).toBeNull(); |
| 145 | }); |
| 146 | }); |
| 147 | |
| 148 | describe("letterStopNotes", () => { |
| 149 | it("letters notes in guide order and pins each to the excerpt that holds its line", () => { |
| 150 | const { lettered, byExcerpt } = letterStopNotes( |
| 151 | stop({ |
| 152 | excerpts: [ |
| 153 | { path: "a.ts", side: "new", start: 1, end: 5, caption: "" }, |
| 154 | { path: "b.ts", side: "new", start: 10, end: 12, caption: "" }, |
| 155 | ], |
| 156 | notes: [ |
| 157 | { path: "b.ts", line: 11, title: "First", body: "" }, |
| 158 | { path: "a.ts", line: 2, title: "Second", body: "" }, |
| 159 | { path: "a.ts", line: 2, title: "Third", body: "" }, |
| 160 | { path: "c.ts", line: 1, title: "Unanchored", body: "" }, |
| 161 | ], |
| 162 | }), |
| 163 | ); |
| 164 | expect(lettered.map((entry) => entry.marker)).toEqual(["A", "B", "C", "D"]); |
| 165 | expect(byExcerpt).toEqual([ |
| 166 | [ |
| 167 | { |
| 168 | line: 2, |
| 169 | markers: [ |
| 170 | { marker: "B", title: "Second" }, |
| 171 | { marker: "C", title: "Third" }, |
| 172 | ], |
| 173 | }, |
| 174 | ], |
| 175 | [{ line: 11, markers: [{ marker: "A", title: "First" }] }], |
| 176 | ]); |
| 177 | }); |
| 178 | }); |