1import {
2 DEFAULT_BROWSER_PROFILE_ID,
3 DEFAULT_CLIENT_SETTINGS,
4 FILL_PREVIEW_VIEWPORT,
5 type PreviewOpenInput,
6 type PreviewSessionSnapshot,
7 type ScopedThreadRef,
8} from "@t3tools/contracts";
9import * as Cause from "effect/Cause";
10import { AsyncResult } from "effect/reactivity";
11import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
12
13import * as browserDefaults from "~/browser/browserDefaults";
14import { BrowserSettingsReadError, openUrlInPreview } from "~/browser/openFileInPreview";
15import { __setClientSettingsForTests } from "~/hooks/useSettings";
16import { useRightPanelStore } from "~/rightPanelStore";
17import {
18 applyPreviewServerSnapshot,
19 readThreadPreviewState,
20 resetPreviewStateForTests,
21 setActivePreviewTab,
22} from "~/previewStateStore";
23
24import { openPreviewSession } from "./openPreviewSession";
25
26const threadRef = {
27 environmentId: "local" as ScopedThreadRef["environmentId"],
28 threadId: "thread-1" as ScopedThreadRef["threadId"],
29};
30
31const snapshot: PreviewSessionSnapshot = {
32 threadId: threadRef.threadId,
33 tabId: "tab-1",
34 navStatus: {
35 _tag: "Loading",
36 url: "https://t3.chat/",
37 title: "",
38 },
39 canGoBack: false,
40 canGoForward: false,
41 updatedAt: "2026-06-11T23:00:00.000Z",
42};
43
44beforeEach(() => {
45 resetPreviewStateForTests();
46 useRightPanelStore.setState({ byThreadKey: {}, threadPanelVisibilityByThreadKey: {} });
47 __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS);
48});
49
50afterEach(() => {
51 vi.restoreAllMocks();
52});
53
54describe("openPreviewSession", () => {
55 it("creates an idle tab without recording a recently visited URL", async () => {
56 const idleSnapshot: PreviewSessionSnapshot = {
57 ...snapshot,
58 tabId: "tab-blank",
59 navStatus: { _tag: "Idle" },
60 };
61 const open = vi.fn(async (_input: PreviewOpenInput) => AsyncResult.success(idleSnapshot));
62
63 await openPreviewSession({
64 openPreview: ({ input }) => open(input),
65 threadRef,
66 });
67
68 expect(open).toHaveBeenCalledWith({
69 threadId: "thread-1",
70 viewport: FILL_PREVIEW_VIEWPORT,
71 profileId: DEFAULT_BROWSER_PROFILE_ID,
72 });
73 expect(readThreadPreviewState(threadRef).snapshot).toEqual(idleSnapshot);
74 expect(readThreadPreviewState(threadRef).recentlySeenUrls).toEqual([]);
75 });
76
77 it("applies the RPC response without waiting for a preview event", async () => {
78 const open = vi.fn(async (_input: PreviewOpenInput) => AsyncResult.success(snapshot));
79
80 await openPreviewSession({
81 openPreview: ({ input }) => open(input),
82 threadRef,
83 url: "t3.chat",
84 });
85
86 expect(open).toHaveBeenCalledWith({
87 threadId: "thread-1",
88 url: "t3.chat",
89 viewport: FILL_PREVIEW_VIEWPORT,
90 profileId: DEFAULT_BROWSER_PROFILE_ID,
91 });
92 expect(readThreadPreviewState(threadRef).snapshot).toEqual(snapshot);
93 expect(readThreadPreviewState(threadRef).recentlySeenUrls).toEqual(["https://t3.chat/"]);
94 });
95
96 it("returns failures without mutating preview state", async () => {
97 const failure = new Error("preview unavailable");
98
99 const result = await openPreviewSession({
100 openPreview: async () => AsyncResult.failure(Cause.fail(failure)),
101 threadRef,
102 url: "t3.chat",
103 });
104
105 expect(result._tag).toBe("Failure");
106 expect(readThreadPreviewState(threadRef).snapshot).toBeNull();
107 expect(readThreadPreviewState(threadRef).recentlySeenUrls).toEqual([]);
108 });
109
110 it.each(["session", "link"] as const)(
111 "does not open a %s with unread settings and uses the saved profile on retry",
112 async (entryPoint) => {
113 const failure = new Error("Settings read failed");
114 vi.spyOn(browserDefaults, "resolveBrowserDefaults").mockRejectedValueOnce(failure);
115 const viewport = { _tag: "freeform", width: 1280, height: 720 } as const;
116 __setClientSettingsForTests({
117 ...DEFAULT_CLIENT_SETTINGS,
118 browserDefaultViewport: viewport,
119 browserDefaultProfileId: "work",
120 browserProfiles: [{ id: "work", name: "Work", kind: "persistent" }],
121 });
122 const openPreview = vi.fn(async () => AsyncResult.success(snapshot));
123 const input = { openPreview, threadRef, url: "https://t3.chat/" };
124 const open = entryPoint === "session" ? openPreviewSession : openUrlInPreview;
125
126 const result = await open(input);
127
128 expect(result._tag).toBe("Failure");
129 if (result._tag === "Failure") {
130 expect(Cause.squash(result.cause)).toBeInstanceOf(BrowserSettingsReadError);
131 expect(Cause.squash(result.cause)).toMatchObject({ cause: failure });
132 }
133 expect(openPreview).not.toHaveBeenCalled();
134 expect(readThreadPreviewState(threadRef).snapshot).toBeNull();
135 expect(readThreadPreviewState(threadRef).recentlySeenUrls).toEqual([]);
136
137 await expect(open(input)).resolves.toMatchObject({ _tag: "Success" });
138 expect(openPreview).toHaveBeenCalledExactlyOnceWith({
139 environmentId: threadRef.environmentId,
140 input: {
141 threadId: threadRef.threadId,
142 url: input.url,
143 viewport,
144 profileId: "work",
145 },
146 });
147 },
148 );
149});
150
151describe("openUrlInPreview from a link", () => {
152 it("opens under the source tab's profile instead of the default", async () => {
153 const openPreview = vi.fn(async (_arg: { input: PreviewOpenInput }) =>
154 AsyncResult.success(snapshot),
155 );
156
157 await openUrlInPreview({ openPreview, threadRef, url: "https://t3.chat/", profileId: "work" });
158
159 expect(openPreview.mock.calls[0]?.[0].input.profileId).toBe("work");
160 });
161
162 it("keeps the current tab active for a background open", async () => {
163 applyPreviewServerSnapshot(threadRef, { ...snapshot, tabId: "tab-current" });
164
165 await openUrlInPreview({
166 openPreview: async () => AsyncResult.success(snapshot),
167 threadRef,
168 url: "https://t3.chat/",
169 background: true,
170 });
171
172 const state = readThreadPreviewState(threadRef);
173 expect(state.activeTabId).toBe("tab-current");
174 expect(Object.keys(state.sessions).toSorted()).toEqual(["tab-1", "tab-current"]);
175 });
176
177 it("keeps a tab the user picked while a background open was in flight", async () => {
178 applyPreviewServerSnapshot(threadRef, { ...snapshot, tabId: "tab-current" });
179 applyPreviewServerSnapshot(threadRef, { ...snapshot, tabId: "tab-other" });
180 setActivePreviewTab(threadRef, "tab-current");
181
182 await openUrlInPreview({
183 openPreview: async () => {
184 // The server activates the new tab, then the user selects another one
185 // before the open resolves.
186 applyPreviewServerSnapshot(threadRef, snapshot);
187 setActivePreviewTab(threadRef, "tab-other");
188 return AsyncResult.success(snapshot);
189 },
190 threadRef,
191 url: "https://t3.chat/",
192 background: true,
193 });
194
195 expect(readThreadPreviewState(threadRef).activeTabId).toBe("tab-other");
196 });
197
198 it("keeps the new tab when the user picks it while a background open is in flight", async () => {
199 applyPreviewServerSnapshot(threadRef, { ...snapshot, tabId: "tab-current" });
200 setActivePreviewTab(threadRef, "tab-current");
201
202 await openUrlInPreview({
203 openPreview: async () => {
204 // The server activates the new tab, then the user clicks that same tab.
205 applyPreviewServerSnapshot(threadRef, snapshot);
206 useRightPanelStore.getState().openBrowser(threadRef, snapshot.tabId);
207 return AsyncResult.success(snapshot);
208 },
209 threadRef,
210 url: "https://t3.chat/",
211 background: true,
212 });
213
214 expect(readThreadPreviewState(threadRef).activeTabId).toBe(snapshot.tabId);
215 });
216
217 it("activates the new tab for a foreground open", async () => {
218 applyPreviewServerSnapshot(threadRef, { ...snapshot, tabId: "tab-current" });
219
220 await openUrlInPreview({
221 openPreview: async () => AsyncResult.success(snapshot),
222 threadRef,
223 url: "https://t3.chat/",
224 });
225
226 expect(readThreadPreviewState(threadRef).activeTabId).toBe("tab-1");
227 });
228});