Skip to content
Source
Sign inSign up

reopenClosedView.test.ts

298 lines10.1 KB

1import {
2 DEFAULT_CLIENT_SETTINGS,
3 type PreviewSessionSnapshot,
4 type ScopedThreadRef,
5} from "@t3tools/contracts";
6import * as Cause from "effect/Cause";
7import { AsyncResult } from "effect/reactivity";
8import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
9​
10import { type ClosedViewEntry, useClosedViewStore } from "./closedViewStore";
11import { __setClientSettingsForTests } from "./hooks/useSettings";
12import { readThreadPreviewState, resetPreviewStateForTests } from "./previewStateStore";
13import { planNextReopen, type ReopenOwnerState, reopenClosedView } from "./reopenClosedView";
14import {
15 hostRightPanel,
16 selectThreadRightPanelState,
17 type ThreadRightPanelState,
18 useRightPanelStore,
19} from "./rightPanelStore";
20​
21const threadRef = {
22 environmentId: "local" as ScopedThreadRef["environmentId"],
23 threadId: "thread-1" as ScopedThreadRef["threadId"],
24};
25const snapshot: PreviewSessionSnapshot = {
26 threadId: threadRef.threadId,
27 tabId: "old-tab",
28 navStatus: { _tag: "Success", url: "https://example.com", title: "Example" },
29 canGoBack: false,
30 canGoForward: false,
31 viewport: { _tag: "freeform", width: 1200, height: 800 },
32 profileId: "work",
33 updatedAt: "2026-09-22T12:00:00.000Z",
34};
35​
36beforeEach(() => {
37 __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS);
38 resetPreviewStateForTests();
39 useClosedViewStore.setState({ entries: [] });
40 useRightPanelStore.setState({ byThreadKey: {}, userActionRevisionByThreadKey: {} });
41});
42​
43describe("reopenClosedView", () => {
44 it.each([
45 { relativePath: "src/app.ts", line: 18, folderTrail: undefined },
46 { relativePath: "apps", line: null, folderTrail: "apps/web/src" },
47 { relativePath: "", line: null, folderTrail: "apps/web/src" },
48 ])(
49 "restores the captured file or folder tab at '$relativePath'",
50 async ({ relativePath, line, folderTrail }) => {
51 const panels = useRightPanelStore.getState();
52 if (folderTrail === undefined) panels.openFile(threadRef, relativePath, line ?? undefined);
53 else {
54 panels.openFile(threadRef, folderTrail);
55 panels.navigateFolder(threadRef, `file:${folderTrail}`, relativePath, folderTrail);
56 }
57 const expected =
58 relativePath === ""
59 ? { kind: "files", id: "files", folderTrail }
60 : {
61 kind: "file",
62 id: `file:${relativePath}`,
63 relativePath,
64 revealLine: line,
65 revealRequestId: 1,
66 ...(folderTrail === undefined ? {} : { folderTrail }),
67 };
68 const before = selectThreadRightPanelState(
69 useRightPanelStore.getState().byThreadKey,
70 threadRef,
71 );
72 expect(before.surfaces).toEqual([expected]);
73 panels.closeSurface(threadRef, expected.id);
74 expect(
75 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef).surfaces,
76 ).toEqual([]);
77 const entries = useClosedViewStore.getState().entries;
78 expect(entries).toEqual([
79 { id: expect.any(String), kind: "panel-tab", threadRef, surface: expected },
80 ]);
81 const openPreview = vi.fn();
82 const reopened = await reopenClosedView(entries[0]!, {
83 openPreview,
84 workspaceAvailable: true,
85 });
86 expect(reopened).toBe(true);
87 const state = selectThreadRightPanelState(
88 useRightPanelStore.getState().byThreadKey,
89 threadRef,
90 );
91 expect(state.isOpen).toBe(true);
92 expect(state.activeSurfaceId).toBe(expected.id);
93 expect(state.surfaces).toEqual([expected]);
94 expect(openPreview).not.toHaveBeenCalled();
95 },
96 );
97​
98 it.each([
99 { hosted: false, folderTrail: "packages/contracts/src" },
100 { hosted: false, folderTrail: undefined },
101 { hosted: true, folderTrail: "packages/contracts/src" },
102 { hosted: true, folderTrail: undefined },
103 ])(
104 "preserves a hidden root tab with trail $folderTrail (hosted: $hosted)",
105 async ({ hosted, folderTrail }) => {
106 const panels = useRightPanelStore.getState();
107 panels.openFile(threadRef, "apps/web/src");
108 panels.navigateFolder(threadRef, "file:apps/web/src", "", "apps/web/src");
109 panels.closeSurface(threadRef, "files");
110 const entry = useClosedViewStore.getState().entries[0]!;
111 const hostRef = hosted
112 ? { ...threadRef, threadId: "host" as ScopedThreadRef["threadId"] }
113 : threadRef;
114 const unhost = hosted ? hostRightPanel(threadRef, hostRef) : undefined;
115 try {
116 if (folderTrail === undefined) panels.open(threadRef, "files");
117 else {
118 panels.openFile(threadRef, folderTrail);
119 panels.navigateFolder(threadRef, `file:${folderTrail}`, "", folderTrail);
120 }
121 panels.close(threadRef);
122 const before = selectThreadRightPanelState(
123 useRightPanelStore.getState().byThreadKey,
124 hostRef,
125 );
126 expect(before.isOpen).toBe(false);
127 const plan = planNextReopen([entry], () => ({
128 environmentKnown: true,
129 catalogReady: true,
130 ownerExists: true,
131 sideChatExists: true,
132 shellLive: true,
133 panel: before,
134 }));
135 expect(plan.restore).toBe(entry);
136 const openPreview = vi.fn();
137 expect(
138 await reopenClosedView(plan.restore!, { openPreview, workspaceAvailable: true }),
139 ).toBe(true);
140 const after = selectThreadRightPanelState(
141 useRightPanelStore.getState().byThreadKey,
142 hostRef,
143 );
144 expect(after.isOpen).toBe(true);
145 expect(after.activeSurfaceId).toBe("files");
146 expect(after.surfaces).toEqual([
147 { kind: "files", id: "files", ...(folderTrail === undefined ? {} : { folderTrail }) },
148 ]);
149 expect(openPreview).not.toHaveBeenCalled();
150 } finally {
151 unhost?.();
152 }
153 },
154 );
155​
156 it("does not reopen workspace tabs without an available project", async () => {
157 const options = {
158 openPreview: vi.fn(),
159 workspaceAvailable: false,
160 };
161 for (const surface of [
162 { kind: "files", id: "files", folderTrail: "apps/web/src" },
163 {
164 kind: "file",
165 id: "file:src/app.ts",
166 relativePath: "src/app.ts",
167 revealLine: null,
168 revealRequestId: 0,
169 },
170 {
171 kind: "file",
172 id: "file:apps",
173 relativePath: "apps",
174 revealLine: null,
175 revealRequestId: 1,
176 folderTrail: "apps/web/src",
177 },
178 ] as const) {
179 expect(await reopenClosedView({ kind: "panel-tab", threadRef, surface }, options)).toBe(
180 false,
181 );
182 }
183 expect(
184 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef).surfaces,
185 ).toEqual([]);
186 });
187​
188 it("recreates a browser tab with saved URL, viewport and profile, then selects its new ID", async () => {
189 const openPreview = vi.fn(async () => AsyncResult.success({ ...snapshot, tabId: "new-tab" }));
190 const result = await reopenClosedView(
191 { kind: "browser", threadRef, snapshot },
192 { openPreview, workspaceAvailable: false },
193 );
194 expect(result).toBe(true);
195 expect(openPreview).toHaveBeenCalledWith({
196 environmentId: threadRef.environmentId,
197 input: {
198 threadId: threadRef.threadId,
199 url: "https://example.com",
200 viewport: snapshot.viewport,
201 profileId: "work",
202 },
203 });
204 expect(readThreadPreviewState(threadRef).snapshot?.tabId).toBe("new-tab");
205 expect(
206 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef)
207 .activeSurfaceId,
208 ).toBe("browser:new-tab");
209 });
210​
211 it("leaves the panel unchanged when a browser resource fails to reopen", async () => {
212 const result = await reopenClosedView(
213 { kind: "browser", threadRef, snapshot },
214 {
215 openPreview: async () => AsyncResult.failure(Cause.fail(new Error("offline"))),
216 workspaceAvailable: false,
217 },
218 );
219 expect(result).toBe(false);
220 expect(
221 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef).surfaces,
222 ).toEqual([]);
223 });
224});
225​
226describe("planNextReopen", () => {
227 const entries: ClosedViewEntry[] = [
228 { id: "a", kind: "panel-tab", threadRef, surface: { kind: "diff", id: "diff" } },
229 { id: "b", kind: "panel-tab", threadRef, surface: { kind: "files", id: "files" } },
230 ];
231 const known: ReopenOwnerState = {
232 environmentKnown: true,
233 catalogReady: true,
234 ownerExists: true,
235 sideChatExists: true,
236 shellLive: true,
237 panel: { isOpen: false, activeSurfaceId: null, surfaces: [] },
238 };
239 const openDiff: ThreadRightPanelState = {
240 isOpen: true,
241 activeSurfaceId: "diff",
242 surfaces: [{ kind: "diff", id: "diff" }],
243 };
244​
245 it.each([
246 ["restores the newest entry", [known, known], [], "a"],
247 [
248 "drops a removed environment once the catalog is ready",
249 [{ ...known, environmentKnown: false }, known],
250 ["a"],
251 "b",
252 ],
253 [
254 "waits for the catalog before dropping",
255 [{ ...known, environmentKnown: false, catalogReady: false }, known],
256 [],
257 null,
258 ],
259 [
260 "skips but keeps a thread deleted from a live shell",
261 [{ ...known, ownerExists: false }, known],
262 [],
263 "b",
264 ],
265 [
266 "skips but keeps a side chat deleted from a live shell",
267 [{ ...known, sideChatExists: false }, known],
268 [],
269 "b",
270 ],
271 [
272 "waits when a cached shell lacks the side chat",
273 [{ ...known, sideChatExists: false, shellLive: false }, known],
274 [],
275 null,
276 ],
277 [
278 "waits when a cached shell lacks the thread",
279 [{ ...known, ownerExists: false, shellLive: false }, known],
280 [],
281 null,
282 ],
283 ["drops a tab that is already open", [{ ...known, panel: openDiff }, known], ["a"], "b"],
284 [
285 "restores a tab whose panel is hidden",
286 [{ ...known, panel: { ...openDiff, isOpen: false } }],
287 [],
288 "a",
289 ],
290 ] as const)("%s", (_name, owners, dropped, restored) => {
291 const plan = planNextReopen(
292 entries.slice(0, owners.length),
293 (e) => owners[entries.indexOf(e)]!,
294 );
295 expect(plan.drop.map((e) => e.id)).toEqual(dropped);
296 expect(plan.restore?.id ?? null).toBe(restored);
297 });
298});