1import { scopeThreadRef } from "@t3tools/client-runtime/environment";
2import {
3 type EnvironmentId,
4 INCOGNITO_BROWSER_PROFILE_ID,
5 type PreviewSessionSnapshot,
6 ThreadId,
7} from "@t3tools/contracts";
8import { beforeEach, describe, expect, it } from "vite-plus/test";
9
10import { type ClosedViewEntry, useClosedViewStore } from "./closedViewStore";
11
12const refA = scopeThreadRef("env-1" as EnvironmentId, ThreadId.make("thread-A"));
13const refB = scopeThreadRef("env-2" as EnvironmentId, ThreadId.make("thread-A"));
14const diff = (threadRef: typeof refA) =>
15 ({ kind: "panel-tab", threadRef, surface: { kind: "diff", id: "diff" } }) as const;
16
17beforeEach(() => {
18 useClosedViewStore.setState({ entries: [] });
19});
20
21const snapshot: PreviewSessionSnapshot = {
22 threadId: refA.threadId,
23 tabId: "private-tab",
24 profileId: INCOGNITO_BROWSER_PROFILE_ID,
25 navStatus: { _tag: "Success", url: "https://private.example", title: "Private page" },
26 canGoBack: false,
27 canGoForward: false,
28 updatedAt: "2026-10-06T17:00:00.000Z",
29};
30
31describe("closedViewStore", () => {
32 it.each([null, {}, { entries: null }, { entries: {} }, { entries: "invalid" }])(
33 "rehydrates malformed older history %j as empty history",
34 async (state) => {
35 useClosedViewStore.getState().remember(diff(refA));
36 const { storage, name } = useClosedViewStore.persist.getOptions();
37 await storage!.setItem(name!, {
38 state: state as { entries: ClosedViewEntry[] },
39 version: 0,
40 });
41 await useClosedViewStore.persist.rehydrate();
42 expect(useClosedViewStore.persist.hasHydrated()).toBe(true);
43 expect(useClosedViewStore.getState().entries).toEqual([]);
44 expect(await storage!.getItem(name!)).toEqual({ state: { entries: [] }, version: 2 });
45 },
46 );
47
48 it.each([
49 { id: "bad", kind: "browser", threadRef: refA, snapshot: {} },
50 {
51 id: "bad",
52 kind: "browser",
53 threadRef: refA,
54 snapshot: { ...snapshot, profileId: "default", navStatus: null },
55 },
56 { id: "bad", kind: "panel-tab", surface: { kind: "diff", id: "diff" } },
57 { id: "bad", kind: "panel-tab", threadRef: refA },
58 { id: "bad", kind: "panel-tab", threadRef: {}, surface: { kind: "diff", id: "diff" } },
59 { id: "bad", kind: "panel-tab", threadRef: refA, surface: {} },
60 { id: "bad", kind: "panel-tab", threadRef: refA, surface: { kind: "file", id: "file:x" } },
61 {
62 id: "bad",
63 kind: "panel-tab",
64 threadRef: refA,
65 surface: { kind: "device", id: "device:x", target: {} },
66 },
67 {
68 id: "bad",
69 kind: "panel-tab",
70 threadRef: refA,
71 surface: { kind: "pull-request", id: "pull-request:x", repository: "owner/repo", number: 1 },
72 },
73 ])("discards incomplete saved views %j while retaining valid history", async (entry) => {
74 const id = useClosedViewStore.getState().remember(diff(refA));
75 const { storage, name } = useClosedViewStore.persist.getOptions();
76 await storage!.setItem(name!, {
77 state: { entries: [entry as ClosedViewEntry, ...useClosedViewStore.getState().entries] },
78 version: 1,
79 });
80 await useClosedViewStore.persist.rehydrate();
81 expect(useClosedViewStore.persist.hasHydrated()).toBe(true);
82 expect(useClosedViewStore.getState().entries).toEqual([{ ...diff(refA), id }]);
83 expect(() => useClosedViewStore.getState().remember(diff(refB))).not.toThrow();
84 });
85
86 it("preserves every supported saved panel type and its restore data", async () => {
87 const surfaces = [
88 { kind: "diff", id: "diff" },
89 { kind: "files", id: "files" },
90 { kind: "pull-requests", id: "pull-requests" },
91 { kind: "preview", id: "browser:new", resourceId: null },
92 { kind: "preview", id: "browser:saved", resourceId: "saved" },
93 { kind: "device", id: "device" },
94 {
95 kind: "device",
96 id: "device:phone",
97 title: "My phone",
98 target: { hostId: "mac", deviceId: "phone", platform: "ios", name: "iPhone" },
99 },
100 { kind: "file", id: "file:x", relativePath: "x", revealLine: 12, revealRequestId: 1 },
101 {
102 kind: "file",
103 id: "attachment:a",
104 relativePath: "report.pdf",
105 revealLine: null,
106 revealRequestId: 0,
107 attachment: {
108 type: "file",
109 id: "a",
110 name: "report.pdf",
111 mimeType: "application/pdf",
112 sizeBytes: 1,
113 },
114 },
115 {
116 kind: "pull-request",
117 id: "pull-request:x",
118 projectId: "project",
119 repository: "owner/repo",
120 number: 1,
121 environmentId: "env-1",
122 host: "github.com",
123 url: "https://github.com/owner/repo/pull/1",
124 },
125 ];
126 const entries = surfaces.map((surface, index) => ({
127 id: String(index),
128 kind: "panel-tab",
129 threadRef: refA,
130 surface,
131 })) as ClosedViewEntry[];
132 const { storage, name } = useClosedViewStore.persist.getOptions();
133 await storage!.setItem(name!, { state: { entries }, version: 0 });
134 await useClosedViewStore.persist.rehydrate();
135 expect(useClosedViewStore.persist.hasHydrated()).toBe(true);
136 expect(useClosedViewStore.getState().entries).toEqual(entries);
137 });
138
139 it("keeps private tabs available in memory without saving their metadata", async () => {
140 const store = useClosedViewStore.getState();
141 const publicId = store.remember(diff(refA));
142 const privateId = store.remember({ kind: "browser", threadRef: refA, snapshot });
143 expect(useClosedViewStore.getState().entries.map((entry) => entry.id)).toEqual([
144 privateId,
145 publicId,
146 ]);
147 const { storage, name } = useClosedViewStore.persist.getOptions();
148 const saved = JSON.stringify(await storage!.getItem(name!));
149 expect(saved).not.toContain("private.example");
150 expect(saved).not.toContain("Private page");
151 await useClosedViewStore.persist.rehydrate();
152 expect(useClosedViewStore.getState().entries.map((entry) => entry.id)).toEqual([publicId]);
153 });
154
155 it("removes private metadata and malformed entries from history saved by earlier versions", async () => {
156 const store = useClosedViewStore.getState();
157 const publicId = store.remember(diff(refA));
158 const browserId = store.remember({
159 kind: "browser",
160 threadRef: refA,
161 snapshot: {
162 ...snapshot,
163 tabId: "public-tab",
164 profileId: "default",
165 navStatus: { _tag: "Success", url: "https://public.example", title: "Public page" },
166 },
167 });
168 store.remember({ kind: "browser", threadRef: refA, snapshot });
169 const { storage, name } = useClosedViewStore.persist.getOptions();
170 await storage!.setItem(name!, {
171 state: {
172 entries: [
173 null,
174 {},
175 { kind: "browser" },
176 { kind: "browser", snapshot: null },
177 ...useClosedViewStore.getState().entries,
178 ] as ClosedViewEntry[],
179 },
180 version: 0,
181 });
182 await useClosedViewStore.persist.rehydrate();
183 expect(useClosedViewStore.persist.hasHydrated()).toBe(true);
184 expect(useClosedViewStore.getState().entries.map((entry) => entry.id)).toEqual([
185 browserId,
186 publicId,
187 ]);
188 const saved = JSON.stringify(await storage!.getItem(name!));
189 expect(saved).not.toContain("private.example");
190 expect(saved).not.toContain("Private page");
191 });
192
193 it("keeps newest first, moves a re-closed tab to the front, and caps at 20", () => {
194 const store = useClosedViewStore.getState();
195 store.remember(diff(refA));
196 store.remember(diff(refB));
197 store.remember(diff(refA));
198 expect(useClosedViewStore.getState().entries).toMatchObject([
199 { threadRef: refA },
200 { threadRef: refB },
201 ]);
202
203 for (let index = 0; index < 24; index++) {
204 store.remember({
205 kind: "panel-tab",
206 threadRef: refA,
207 surface: {
208 kind: "file",
209 id: `file:${index}`,
210 relativePath: `${index}.ts`,
211 revealLine: null,
212 revealRequestId: 0,
213 },
214 });
215 }
216 const entries = useClosedViewStore.getState().entries;
217 expect(entries).toHaveLength(20);
218 expect(entries[0]).toMatchObject({ surface: { id: "file:23" } });
219 });
220});