1import type { FileDiffMetadata } from "@pierre/diffs";
2import { describe, expect, it } from "vite-plus/test";
3
4import { orderDiffFiles } from "./pullRequestFileOrder.logic";
5
6/** Only the path and the patch's own lines matter here; the viewer fills the rest in. */
7function file(name: string, additionLines: ReadonlyArray<string> = []): FileDiffMetadata {
8 return { name, hunks: [], additionLines, deletionLines: [] } as unknown as FileDiffMetadata;
9}
10
11function order(files: ReadonlyArray<FileDiffMetadata>): Array<string> {
12 return orderDiffFiles(files).map((entry) => entry.name);
13}
14
15describe("orderDiffFiles", () => {
16 it("places source before tests and generated files across path conventions", () => {
17 const source = ["src/app.ts", "src/dist.ts", "src/testing.ts"];
18 const tests = [
19 "src/__tests__/app.ts",
20 "src/app.spec.tsx",
21 "src/app.test.ts",
22 "test/app.ts",
23 "tests/helpers/app.ts",
24 ];
25 const generated = [
26 "apps/web/package-lock.json",
27 "dist/app.js",
28 "packages/core/vendor/lib.js",
29 "pnpm-lock.yaml",
30 "public/app.min.js",
31 "src/__snapshots__/app.ts",
32 "src/api.generated.ts",
33 "src/app.test.ts.snap",
34 ];
35 expect(
36 order([...generated, ...tests, ...source].toReversed().map((path) => file(path))),
37 ).toEqual([...source, ...tests, ...generated]);
38 });
39
40 it("answers an empty diff with an empty order", () => {
41 expect(order([])).toEqual([]);
42 });
43
44 it("puts a dependency before what imports it", () => {
45 expect(order([file("src/a.ts", ['import { b } from "./b";']), file("src/b.ts")])).toEqual([
46 "src/b.ts",
47 "src/a.ts",
48 ]);
49 });
50
51 it("follows a chain the whole way down", () => {
52 expect(
53 order([
54 file("src/a.ts", ['import { b } from "./b";']),
55 file("src/b.ts", ['import { c } from "./c";']),
56 file("src/c.ts"),
57 ]),
58 ).toEqual(["src/c.ts", "src/b.ts", "src/a.ts"]);
59 });
60
61 it("resolves a specifier that climbs out of the importer's directory", () => {
62 expect(
63 order([file("src/ui/a.ts", ['import { b } from "../lib/b";']), file("src/lib/b.ts")]),
64 ).toEqual(["src/lib/b.ts", "src/ui/a.ts"]);
65 });
66
67 it("resolves a directory specifier to that directory's index file", () => {
68 expect(
69 order([file("src/a.ts", ['import { b } from "./lib";']), file("src/lib/index.ts")]),
70 ).toEqual(["src/lib/index.ts", "src/a.ts"]);
71 });
72
73 it("falls back to the specifier's last segment when the path does not line up", () => {
74 // An aliased import (`~/lib/b`) resolves to nothing on disk here, but the diff has only one
75 // file that could be meant.
76 expect(order([file("src/a.ts", ['import { b } from "~/lib/b";']), file("lib/b.ts")])).toEqual([
77 "lib/b.ts",
78 "src/a.ts",
79 ]);
80 });
81
82 it("ignores an ambiguous last segment rather than guessing", () => {
83 expect(
84 order([
85 file("src/a.ts", ['import { b } from "~/somewhere/b";']),
86 file("one/b.ts"),
87 file("two/b.ts"),
88 ]),
89 ).toEqual(["one/b.ts", "src/a.ts", "two/b.ts"]);
90 });
91
92 it("resolves a specifier naming an extension, including the .js beside a .ts", () => {
93 expect(order([file("src/a.ts", ['import { b } from "./b.js";']), file("src/b.ts")])).toEqual([
94 "src/b.ts",
95 "src/a.ts",
96 ]);
97 });
98
99 it("reads require and bare imports too", () => {
100 expect(
101 order([
102 file("src/a.ts", ['const b = require("./b");', 'import "./c";']),
103 file("src/b.ts"),
104 file("src/c.ts"),
105 ]),
106 ).toEqual(["src/b.ts", "src/c.ts", "src/a.ts"]);
107 });
108
109 it("falls back to path order inside a cycle", () => {
110 expect(
111 order([
112 file("src/b.ts", ['import { a } from "./a";']),
113 file("src/a.ts", ['import { b } from "./b";']),
114 ]),
115 ).toEqual(["src/a.ts", "src/b.ts"]);
116 });
117
118 it("clusters unrelated files by path", () => {
119 expect(order([file("src/ui/b.ts"), file("src/lib/z.ts"), file("src/lib/a.ts")])).toEqual([
120 "src/lib/a.ts",
121 "src/lib/z.ts",
122 "src/ui/b.ts",
123 ]);
124 });
125
126 it("keeps the tiers apart and orders tests by the source they cover", () => {
127 expect(
128 order([
129 file("pnpm-lock.yaml"),
130 file("src/a.test.ts"),
131 file("src/__tests__/b.ts"),
132 file("src/a.ts", ['import { b } from "./b";']),
133 file("src/b.ts"),
134 file("dist/a.js"),
135 ]),
136 ).toEqual([
137 "src/b.ts",
138 "src/a.ts",
139 "src/__tests__/b.ts",
140 "src/a.test.ts",
141 "dist/a.js",
142 "pnpm-lock.yaml",
143 ]);
144 });
145
146 it("puts a test with no source in the diff after the ones that have theirs", () => {
147 expect(order([file("src/orphan.spec.ts"), file("src/a.test.ts"), file("src/a.ts")])).toEqual([
148 "src/a.ts",
149 "src/a.test.ts",
150 "src/orphan.spec.ts",
151 ]);
152 });
153
154 it("orders the same diff the same way whatever order it arrives in", () => {
155 const files = [
156 file("src/a.ts", ['import { b } from "./b";']),
157 file("src/b.ts"),
158 file("src/c.ts"),
159 file("src/a.test.ts"),
160 file("yarn.lock"),
161 ];
162 expect(order(files)).toEqual(order(files.toReversed()));
163 });
164});