1import type { FileDiffMetadata } from "@pierre/diffs";
2import { describe, expect, it } from "vite-plus/test";
3
4import {
5 isFileDiffCollapsed,
6 isLineInFileDiff,
7 toggleFileDiffFoldForViewed,
8} from "./pullRequestDiff.logic";
9
10/** Only the hunk ranges matter here; the viewer fills the rest in when it renders. */
11function fileWithHunks(
12 hunks: ReadonlyArray<{
13 deletionStart: number;
14 deletionCount: number;
15 additionStart: number;
16 additionCount: number;
17 }>,
18): FileDiffMetadata {
19 return { name: "src/app.ts", hunks } as unknown as FileDiffMetadata;
20}
21
22describe("isLineInFileDiff", () => {
23 const file = fileWithHunks([
24 { deletionStart: 10, deletionCount: 3, additionStart: 10, additionCount: 5 },
25 { deletionStart: 40, deletionCount: 0, additionStart: 42, additionCount: 2 },
26 ]);
27
28 it("places a line inside a hunk, on the side that hunk counts", () => {
29 expect(isLineInFileDiff(file, "right", 12)).toBe(true);
30 expect(isLineInFileDiff(file, "left", 11)).toBe(true);
31 });
32
33 it("includes the first line of a hunk and excludes the one past its last", () => {
34 // The boundaries are where an off-by-one would quietly move a conversation between lists.
35 expect(isLineInFileDiff(file, "right", 10)).toBe(true);
36 expect(isLineInFileDiff(file, "right", 14)).toBe(true);
37 expect(isLineInFileDiff(file, "right", 15)).toBe(false);
38 expect(isLineInFileDiff(file, "left", 9)).toBe(false);
39 expect(isLineInFileDiff(file, "left", 12)).toBe(true);
40 expect(isLineInFileDiff(file, "left", 13)).toBe(false);
41 });
42
43 it("keeps the two sides apart, since one line number means two lines", () => {
44 // The second hunk is a pure insertion: it deletes nothing, so nothing is on its left.
45 expect(isLineInFileDiff(file, "right", 43)).toBe(true);
46 expect(isLineInFileDiff(file, "left", 40)).toBe(false);
47 });
48
49 it("places nothing in a file whose hunks the host withheld", () => {
50 expect(isLineInFileDiff(fileWithHunks([]), "right", 1)).toBe(false);
51 });
52});
53
54describe("isFileDiffCollapsed", () => {
55 const NO_TOGGLES: ReadonlySet<string> = new Set();
56
57 it("opens every file before the reader has touched anything", () => {
58 expect(isFileDiffCollapsed("a.ts", null, NO_TOGGLES)).toBe(false);
59 expect(isFileDiffCollapsed("b.ts", null, NO_TOGGLES)).toBe(false);
60 });
61
62 it("opens every file once the toolbar has asked for it", () => {
63 // Pressing the toolbar clears the reader's own toggles, which is why the set is empty here.
64 expect(isFileDiffCollapsed("a.ts", "expanded", NO_TOGGLES)).toBe(false);
65 expect(isFileDiffCollapsed("b.ts", "expanded", NO_TOGGLES)).toBe(false);
66 });
67
68 it("folds every file again on the second press", () => {
69 expect(isFileDiffCollapsed("a.ts", "folded", NO_TOGGLES)).toBe(true);
70 expect(isFileDiffCollapsed("b.ts", "folded", NO_TOGGLES)).toBe(true);
71 });
72
73 it("keeps a file the reader folded closed as the next slice arrives", () => {
74 // The file keys grow with every slice, so the answer for one already folded must not depend
75 // on how many of them there are by then.
76 const toggled = new Set(["b.ts"]);
77 expect(isFileDiffCollapsed("b.ts", null, toggled)).toBe(true);
78 expect(isFileDiffCollapsed("c.ts", null, toggled)).toBe(false);
79 });
80
81 it("still answers to a toggle after either toolbar press", () => {
82 expect(isFileDiffCollapsed("a.ts", "expanded", new Set(["a.ts"]))).toBe(true);
83 expect(isFileDiffCollapsed("a.ts", "folded", new Set(["a.ts"]))).toBe(false);
84 });
85});
86
87describe("toggleFileDiffFoldForViewed", () => {
88 it("puts a file away when it is ticked off", () => {
89 // Files start expanded, so ticking one off is the case that has somewhere to go.
90 expect([...toggleFileDiffFoldForViewed("a.ts", true, null, new Set())]).toEqual(["a.ts"]);
91 });
92
93 it("brings a file back when the tick is taken off", () => {
94 expect([...toggleFileDiffFoldForViewed("a.ts", false, null, new Set(["a.ts"]))]).toEqual([]);
95 });
96
97 it("leaves the fold alone when it already says what the tick does", () => {
98 const folded = new Set(["a.ts"]);
99 expect(toggleFileDiffFoldForViewed("a.ts", true, null, folded)).toBe(folded);
100 });
101
102 it("moves against whatever the toolbar last asked for", () => {
103 // Everything is open, so ticking a file off has to fold that one against the default.
104 expect([...toggleFileDiffFoldForViewed("a.ts", true, "expanded", new Set())]).toEqual(["a.ts"]);
105 expect(toggleFileDiffFoldForViewed("a.ts", false, "expanded", new Set()).size).toBe(0);
106 });
107
108 it("touches only the file that was ticked", () => {
109 const toggled = new Set(["a.ts", "b.ts"]);
110 expect([...toggleFileDiffFoldForViewed("a.ts", false, null, toggled)]).toEqual(["b.ts"]);
111 });
112});