Skip to content
Source
Sign inSign up
Open History to see the last commit

projectFilesQueryState.test.ts

180 lines6.2 KB

1import type { ProjectReadFileResult } from "@t3tools/contracts";
2import { EnvironmentId } from "@t3tools/contracts";
3import { AsyncResult } from "effect/reactivity";
4import { afterEach, describe, expect, it, vi } from "vite-plus/test";
5​
6const registryTasks = vi.hoisted(() => new Set<() => void>());
7​
8vi.mock("~/rpc/atomRegistry", async () => {
9 const { AtomRegistry } = await import("effect/reactivity");
10 return {
11 appAtomRegistry: AtomRegistry.make({
12 scheduleTask: (task) => {
13 registryTasks.add(task);
14 return () => {
15 registryTasks.delete(task);
16 };
17 },
18 }),
19 };
20});
21​
22import { appAtomRegistry } from "~/rpc/atomRegistry";
23import { projectEnvironment } from "~/state/projects";
24import { FileSaveCoordinator } from "./fileSaveCoordinator";
25import {
26 clearProjectFileQueryData,
27 confirmProjectFileQueryData,
28 getOptimisticProjectFileQueryData,
29 getUnsavedProjectFileQueryData,
30 resolveProjectFileQueryData,
31 setProjectFileQueryData,
32} from "./projectFilesQueryState";
33​
34const environmentId = EnvironmentId.make("environment-project-files-query-test");
35const optimisticFile = projectEnvironment.optimisticFile({
36 environmentId,
37 cwd: "/repo",
38 relativePath: "convex.json",
39});
40​
41function drainRegistryTasks(): void {
42 while (registryTasks.size > 0) {
43 const tasks = [...registryTasks];
44 registryTasks.clear();
45 for (const task of tasks) task();
46 }
48​
49describe("project files queries", () => {
50 afterEach(() => {
51 clearProjectFileQueryData(environmentId, "/repo", "convex.json");
52 drainRegistryTasks();
53 vi.useRealTimers();
54 vi.unstubAllGlobals();
55 });
56​
57 it("resumes an unsaved draft after closing the preview and restoring write access", async () => {
58 vi.stubGlobal("window", {});
59 vi.useFakeTimers();
60 const closePreview = appAtomRegistry.mount(optimisticFile);
61 let canWrite = true;
62 const persist = vi.fn().mockResolvedValue(AsyncResult.success(undefined));
63 const onPendingChange = vi.fn();
64 const makeCoordinator = () =>
65 new FileSaveCoordinator({
66 debounceMs: 500,
67 canPersist: () => canWrite,
68 persist,
69 onPendingChange,
70 onConfirmed: (contents) =>
71 confirmProjectFileQueryData(environmentId, "/repo", "convex.json", contents),
72 });
73 const initial = makeCoordinator();
74 setProjectFileQueryData(environmentId, "/repo", "convex.json", "unsaved draft");
75 initial.change("unsaved draft");
76 canWrite = false;
77 initial.dispose();
78 closePreview();
79 await vi.runAllTimersAsync();
80 drainRegistryTasks();
81​
82 expect(persist).not.toHaveBeenCalled();
83 expect(onPendingChange).toHaveBeenLastCalledWith(true);
84 const unsaved = getUnsavedProjectFileQueryData(environmentId, "/repo", "convex.json");
85 expect(unsaved?.contents).toBe("unsaved draft");
86​
87 canWrite = true;
88 const reopened = makeCoordinator();
89 reopened.change(unsaved!.contents);
90 await vi.advanceTimersByTimeAsync(500);
91​
92 expect(persist).toHaveBeenCalledOnce();
93 expect(persist).toHaveBeenCalledWith("unsaved draft");
94 expect(onPendingChange).toHaveBeenLastCalledWith(false);
95 expect(getUnsavedProjectFileQueryData(environmentId, "/repo", "convex.json")).toBeNull();
96 reopened.dispose();
97 drainRegistryTasks();
98 expect(appAtomRegistry.getNodes().has(optimisticFile)).toBe(false);
99 });
100​
101 it("keeps a reopened editor's newer draft pending when the old editor's write finishes", async () => {
102 vi.stubGlobal("window", {});
103 vi.useFakeTimers();
104 let canWrite = true;
105 const saved = AsyncResult.success(undefined);
106 let finishFirstWrite!: (result: typeof saved) => void;
107 const firstWrite = new Promise<typeof saved>((resolve) => {
108 finishFirstWrite = resolve;
109 });
110 const persist = vi.fn().mockReturnValueOnce(firstWrite).mockResolvedValue(saved);
111 const onPendingChange = vi.fn();
112 const makeCoordinator = () =>
113 new FileSaveCoordinator({
114 debounceMs: 500,
115 canPersist: () => canWrite,
116 persist,
117 onPendingChange,
118 onConfirmed: (contents) =>
119 confirmProjectFileQueryData(environmentId, "/repo", "convex.json", contents),
120 });
121​
122 const initial = makeCoordinator();
123 setProjectFileQueryData(environmentId, "/repo", "convex.json", "first draft");
124 initial.change("first draft");
125 await vi.advanceTimersByTimeAsync(500);
126 initial.dispose();
127​
128 const reopened = makeCoordinator();
129 setProjectFileQueryData(environmentId, "/repo", "convex.json", "newer draft");
130 reopened.change("newer draft");
131 canWrite = false;
132 finishFirstWrite(saved);
133 await vi.runAllTimersAsync();
134​
135 expect(persist).toHaveBeenCalledOnce();
136 expect(onPendingChange).toHaveBeenLastCalledWith(true);
137 expect(getUnsavedProjectFileQueryData(environmentId, "/repo", "convex.json")?.contents).toBe(
138 "newer draft",
139 );
140​
141 canWrite = true;
142 reopened.change("newer draft");
143 await vi.advanceTimersByTimeAsync(500);
144 expect(persist).toHaveBeenLastCalledWith("newer draft");
145 expect(onPendingChange).toHaveBeenLastCalledWith(false);
146 expect(getUnsavedProjectFileQueryData(environmentId, "/repo", "convex.json")).toBeNull();
147 reopened.dispose();
148 });
149​
150 it("keeps the latest optimistic draft when an older write finishes", () => {
151 vi.stubGlobal("window", {});
152 const initial = {
153 relativePath: "convex.json",
154 contents: '{"nodeVersion":"20"}',
155 byteLength: 20,
156 truncated: false,
157 } satisfies ProjectReadFileResult;
158 setProjectFileQueryData(environmentId, "/repo", "convex.json", '{"nodeVersion":"220"}');
159 setProjectFileQueryData(environmentId, "/repo", "convex.json", '{"nodeVersion":"22"}');
160​
161 expect(getOptimisticProjectFileQueryData(environmentId, "/repo", "convex.json")?.contents).toBe(
162 '{"nodeVersion":"22"}',
163 );
164​
165 expect(
166 confirmProjectFileQueryData(environmentId, "/repo", "convex.json", '{"nodeVersion":"220"}'),
167 ).toBe(false);
168​
169 expect(resolveProjectFileQueryData(environmentId, "/repo", "convex.json", initial)).toEqual({
170 relativePath: "convex.json",
171 contents: '{"nodeVersion":"22"}',
172 byteLength: 20,
173 truncated: false,
174 });
175​
176 expect(
177 confirmProjectFileQueryData(environmentId, "/repo", "convex.json", '{"nodeVersion":"22"}'),
178 ).toBe(true);
179 });
180});