Open History to see the last commit
1import { RegistryContext } from "@effect/atom-react";
2import type { ReviewDiffPreviewSource } from "@t3tools/contracts";
3import { EnvironmentId } from "@t3tools/contracts";
4import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
5import * as Option from "effect/Option";
6import { expect, it, vi } from "vite-plus/test";
7import { userEvent } from "vite-plus/test/browser";
8import { act, render } from "../../../test/render";
9
10type PatchAtom = Atom.Writable<AsyncResult.AsyncResult<ReviewDiffPreviewSource, Error>>;
11const state = vi.hoisted(() => ({ patches: new Map<string, PatchAtom>() }));
12vi.mock("~/state/review", () => ({
13 reviewEnvironment: {
14 diffFilePatch: (target: {
15 input: { cacheKey: string; request: { file: { path: string } } };
16 }) => {
17 const key = JSON.stringify([target.input.cacheKey, target.input.request.file.path]);
18 let atom = state.patches.get(key);
19 if (!atom) {
20 atom = Atom.make<AsyncResult.AsyncResult<ReviewDiffPreviewSource, Error>>(
21 AsyncResult.initial(),
22 );
23 state.patches.set(key, atom);
24 }
25 return atom;
26 },
27 },
28}));
29
30import { useReviewFilePatches } from "./useReviewFilePatches";
31
32function source(paths: string[], hash: string): ReviewDiffPreviewSource {
33 return {
34 id: "working-tree",
35 kind: "working-tree",
36 title: "Uncommitted",
37 baseRef: "HEAD",
38 headRef: null,
39 diff: "",
40 diffHash: hash,
41 truncated: false,
42 patchesDeferred: true,
43 files: paths.map((path) => ({ path, previousPath: null, additions: 1, deletions: 1 })),
44 };
45}
46
47function Probe({ source }: { source: ReviewDiffPreviewSource }) {
48 const files = useReviewFilePatches({
49 environmentId: EnvironmentId.make("environment"),
50 cwd: "/project",
51 source,
52 baseRef: "HEAD",
53 ignoreWhitespace: false,
54 theme: "dark",
55 previewInput: { cwd: "/project", metadataOnly: true },
56 preview: null,
57 });
58 return (
59 <>
60 <button onClick={() => files.requestFile("f.txt")}>Open f</button>
61 <output>Pending files: {files.remainingFileCount}</output>
62 {files.renderableFiles.map((file) => (
63 <div key={`${file.name}:${file.type}`}>
64 <span>
65 {file.name}:{file.hunks.length ? "loaded" : "pending"}
66 </span>
67 <span>{file.type}</span>
68 </div>
69 ))}
70 </>
71 );
72}
73
74it("retains automatic and explicitly opened files through metadata insertion, refresh failure and retry", async () => {
75 state.patches.clear();
76 const registry = AtomRegistry.make();
77 using _registry = { [Symbol.dispose]: () => registry.dispose() };
78 const tracked = source(["b.txt", "c.txt", "d.txt", "e.txt", "f.txt"], "tracked");
79 const view = (source: ReviewDiffPreviewSource) => (
80 <RegistryContext.Provider value={registry}>
81 <Probe source={source} />
82 </RegistryContext.Provider>
83 );
84 await using screen = await render(view(tracked));
85 await userEvent.click(screen.getByRole("button", { name: "Open f" }));
86 const atom = [...state.patches].find(([key]) => JSON.parse(key)[1] === "f.txt")![1];
87 const loaded = AsyncResult.success({
88 ...tracked,
89 diff: "diff --git a/f.txt b/f.txt\n--- a/f.txt\n+++ b/f.txt\n@@ -1 +1 @@\n-old\n+new\n",
90 files: tracked.files!.filter((f) => f.path === "f.txt"),
91 });
92 await act(() => registry.set(atom, loaded));
93 await expect.element(screen.getByText("f.txt:loaded")).toBeVisible();
94 const automatic = [...state.patches].find(([key]) => JSON.parse(key)[1] === "e.txt")![1];
95 await act(() =>
96 registry.set(
97 automatic,
98 AsyncResult.success({
99 ...loaded.value,
100 diff: loaded.value.diff.replaceAll("f.txt", "e.txt"),
101 files: tracked.files!.filter((file) => file.path === "e.txt"),
102 }),
103 ),
104 );
105 await expect.element(screen.getByText("e.txt:loaded")).toBeVisible();
106 const complete = source(["a-new.txt", ...tracked.files!.map((f) => f.path)], "complete");
107 await screen.rerender(
108 view({
109 ...complete,
110 files: complete.files!.map((file) =>
111 file.path === "a-new.txt" ? { ...file, changeType: "new" } : file,
112 ),
113 }),
114 );
115 await expect.element(screen.getByText("a-new.txt:pending")).toBeVisible();
116 await expect.element(screen.getByText("new", { exact: true })).toBeVisible();
117 await expect.element(screen.getByText("e.txt:loaded")).toBeVisible();
118 await expect.element(screen.getByText("f.txt:loaded")).toBeVisible();
119 await act(() =>
120 registry.set(
121 atom,
122 AsyncResult.fail(new Error("refresh failed"), { previousSuccess: Option.some(loaded) }),
123 ),
124 );
125 await expect.element(screen.getByText("f.txt:loaded")).toBeVisible();
126 await act(() => registry.set(atom, loaded));
127 await expect.element(screen.getByText("f.txt:loaded")).toBeVisible();
128});
129
130it("preserves deletion and addition patches for a file replaced by a symlink", async () => {
131 state.patches.clear();
132 const registry = AtomRegistry.make();
133 using _registry = { [Symbol.dispose]: () => registry.dispose() };
134 const metadata = source(["entry"], "type-change");
135 await using screen = await render(
136 <RegistryContext.Provider value={registry}>
137 <Probe source={metadata} />
138 </RegistryContext.Provider>,
139 );
140 const atom = [...state.patches.values()][0]!;
141 await act(() =>
142 registry.set(
143 atom,
144 AsyncResult.success({
145 ...metadata,
146 diff: "diff --git a/entry b/entry\ndeleted file mode 100644\n--- a/entry\n+++ /dev/null\n@@ -1 +0,0 @@\n-before\ndiff --git a/entry b/entry\nnew file mode 120000\n--- /dev/null\n+++ b/entry\n@@ -0,0 +1 @@\n+target\n\\ No newline at end of file\n",
147 }),
148 ),
149 );
150 await expect.element(screen.getByText("deleted", { exact: true })).toBeVisible();
151 await expect.element(screen.getByText("new", { exact: true })).toBeVisible();
152 await expect.element(screen.getByText("Pending files: 0")).toBeVisible();
153});