1import type { PreviewSessionSnapshot, ScopedThreadRef } from "@t3tools/contracts";
2import * as Cause from "effect/Cause";
3import { AsyncResult } from "effect/reactivity";
4import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
5
6import {
7 openTerminalLinkInPreview,
8 TerminalLinkPreviewOpenError,
9} from "./openTerminalLinkInPreview";
10
11vi.mock("~/previewStateStore", () => ({
12 applyPreviewServerSnapshot: vi.fn(),
13 isPreviewSupportedInRuntime: () => true,
14}));
15
16vi.mock("~/rightPanelStore", () => ({
17 useRightPanelStore: {
18 getState: () => ({ openBrowser: vi.fn() }),
19 },
20}));
21
22const browserDefaultsMocks = vi.hoisted(() => ({
23 resolve: vi.fn(),
24}));
25
26vi.mock("~/browser/browserDefaults", () => ({
27 resolveBrowserDefaults: browserDefaultsMocks.resolve,
28 browserDefaultOpenViewport: (defaults: { viewport: unknown }) => defaults.viewport,
29 browserDefaultOpenProfileId: (defaults: { profileId: string }) => defaults.profileId,
30}));
31
32const linkTargetMocks = vi.hoisted(() => ({
33 preference: vi.fn<() => "system" | "app">(),
34}));
35
36vi.mock("~/browser/browserLinkTarget", () => ({
37 resolveBrowserLinkTargetPreference: async () => linkTargetMocks.preference(),
38 isWebUrl: (url: string) => /^https?:/u.test(url),
39}));
40
41const hydratedDefaults = {
42 viewport: { _tag: "fixed", width: 1280, height: 720 } as const,
43 profileId: "work",
44};
45
46const threadRef = {
47 environmentId: "local" as ScopedThreadRef["environmentId"],
48 threadId: "thread-1" as ScopedThreadRef["threadId"],
49};
50
51const snapshot: PreviewSessionSnapshot = {
52 threadId: threadRef.threadId,
53 tabId: "tab-1",
54 navStatus: { _tag: "Idle" },
55 canGoBack: false,
56 canGoForward: false,
57 updatedAt: "2026-06-20T00:00:00.000Z",
58};
59
60beforeEach(() => {
61 browserDefaultsMocks.resolve.mockReset();
62 browserDefaultsMocks.resolve.mockResolvedValue(hydratedDefaults);
63 linkTargetMocks.preference.mockReturnValue("app");
64});
65
66afterEach(() => {
67 vi.restoreAllMocks();
68});
69
70describe("openTerminalLinkInPreview", () => {
71 it.each(["target", "defaults"] as const)(
72 "does not open either browser when reading %s fails",
73 async (setting) => {
74 const failure = new Error("Settings read failed");
75 if (setting === "target") {
76 linkTargetMocks.preference.mockImplementationOnce(() => {
77 throw failure;
78 });
79 } else {
80 browserDefaultsMocks.resolve.mockRejectedValueOnce(failure);
81 }
82 const fallbackToBrowser = vi.fn();
83 const openPreview = vi.fn(async () => AsyncResult.success(snapshot));
84
85 await expect(
86 openTerminalLinkInPreview({
87 url: "https://example.com/docs",
88 threadRef,
89 openPreview,
90 fallbackToBrowser,
91 forceBrowser: false,
92 }),
93 ).rejects.toBe(failure);
94 expect(fallbackToBrowser).not.toHaveBeenCalled();
95 expect(openPreview).not.toHaveBeenCalled();
96 },
97 );
98
99 it("opens in the system browser while that is the configured target", async () => {
100 linkTargetMocks.preference.mockReturnValue("system");
101 const fallbackToBrowser = vi.fn();
102 const openPreview = vi.fn(async () => AsyncResult.success(snapshot));
103
104 await openTerminalLinkInPreview({
105 url: "http://localhost:3000/",
106 threadRef,
107 openPreview,
108 fallbackToBrowser,
109 forceBrowser: false,
110 });
111
112 expect(fallbackToBrowser).toHaveBeenCalledOnce();
113 expect(openPreview).not.toHaveBeenCalled();
114 });
115
116 it("opens public URLs in-app too, not only local servers", async () => {
117 const fallbackToBrowser = vi.fn();
118 const openPreview = vi.fn(async () => AsyncResult.success(snapshot));
119
120 await openTerminalLinkInPreview({
121 url: "https://example.com/docs",
122 threadRef,
123 openPreview,
124 fallbackToBrowser,
125 forceBrowser: false,
126 });
127
128 expect(openPreview).toHaveBeenCalledOnce();
129 expect(fallbackToBrowser).not.toHaveBeenCalled();
130 });
131
132 it("waits for hydrated viewport and profile defaults before opening", async () => {
133 let hydrate: ((defaults: typeof hydratedDefaults) => void) | undefined;
134 browserDefaultsMocks.resolve.mockImplementationOnce(
135 () =>
136 new Promise<typeof hydratedDefaults>((resolve) => {
137 hydrate = resolve;
138 }),
139 );
140 const openPreview = vi.fn(async () => AsyncResult.success(snapshot));
141
142 const opening = openTerminalLinkInPreview({
143 url: "http://localhost:3000/",
144 threadRef,
145 openPreview,
146 fallbackToBrowser: vi.fn(),
147 forceBrowser: false,
148 });
149
150 await vi.waitFor(() => expect(browserDefaultsMocks.resolve).toHaveBeenCalledOnce());
151 expect(openPreview).not.toHaveBeenCalled();
152 hydrate?.(hydratedDefaults);
153 await opening;
154
155 expect(openPreview).toHaveBeenCalledWith({
156 environmentId: "local",
157 input: {
158 threadId: "thread-1",
159 url: "http://localhost:3000/",
160 viewport: hydratedDefaults.viewport,
161 profileId: hydratedDefaults.profileId,
162 },
163 });
164 });
165
166 it("preserves the complete preview failure cause before falling back", async () => {
167 const rpcError = new Error("preview unavailable");
168 const cause = Cause.combine(Cause.fail(rpcError), Cause.die("preview defect"));
169 const fallbackToBrowser = vi.fn();
170 const reportError = vi.spyOn(console, "error").mockImplementation(() => undefined);
171
172 await openTerminalLinkInPreview({
173 url: "http://127.0.0.1:5173/",
174 threadRef,
175 openPreview: async () => AsyncResult.failure(cause),
176 fallbackToBrowser,
177 forceBrowser: false,
178 });
179
180 expect(fallbackToBrowser).toHaveBeenCalledOnce();
181 expect(reportError).toHaveBeenCalledOnce();
182 const error = reportError.mock.calls[0]?.[0];
183 expect(error).toBeInstanceOf(TerminalLinkPreviewOpenError);
184 expect(error).toMatchObject({
185 environmentId: "local",
186 threadId: "thread-1",
187 targetOrigin: "http://127.0.0.1:5173",
188 cause,
189 });
190 expect(error.message).not.toContain("preview unavailable");
191 });
192
193 it("does not report or fall back when opening the preview is interrupted", async () => {
194 const fallbackToBrowser = vi.fn();
195 const reportError = vi.spyOn(console, "error").mockImplementation(() => undefined);
196
197 await openTerminalLinkInPreview({
198 url: "http://localhost:5173/",
199 threadRef,
200 openPreview: async () => AsyncResult.failure(Cause.interrupt()),
201 fallbackToBrowser,
202 forceBrowser: false,
203 });
204
205 expect(reportError).not.toHaveBeenCalled();
206 expect(fallbackToBrowser).not.toHaveBeenCalled();
207 });
208
209 it("opens in the system browser when Ctrl or Command is held", async () => {
210 const fallbackToBrowser = vi.fn();
211 const openPreview = vi.fn(async () => AsyncResult.success(snapshot));
212
213 await openTerminalLinkInPreview({
214 url: "https://example.com/docs",
215 threadRef,
216 openPreview,
217 fallbackToBrowser,
218 forceBrowser: true,
219 });
220
221 expect(fallbackToBrowser).toHaveBeenCalledOnce();
222 expect(openPreview).not.toHaveBeenCalled();
223 });
224});