| 1 | import type { FileDiffMetadata } from "@pierre/diffs"; |
| 2 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | |
| 4 | import { orderDiffFiles } from "./pullRequestFileOrder.logic"; |
| 5 | |
| 6 | /** Only the path and the patch's own lines matter here; the viewer fills the rest in. */ |
| 7 | function file(name: string, additionLines: ReadonlyArray<string> = []): FileDiffMetadata { |
| 8 | return { name, hunks: [], additionLines, deletionLines: [] } as unknown as FileDiffMetadata; |
| 9 | } |
| 10 | |
| 11 | function order(files: ReadonlyArray<FileDiffMetadata>): Array<string> { |
| 12 | return orderDiffFiles(files).map((entry) => entry.name); |
| 13 | } |
| 14 | |
| 15 | describe("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 | }); |