1import {
2 DEFAULT_CLIENT_SETTINGS,
3 EnvironmentId,
4 FILL_PREVIEW_VIEWPORT,
5 ThreadId,
6 type ClientSettings,
7 type DesktopPreviewBridge,
8} from "@t3tools/contracts";
9import { act } from "react";
10import { create, type ReactTestRenderer } from "react-test-renderer";
11import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
12
13const mocks = vi.hoisted(() => ({
14 getClientSettings: vi.fn<() => Promise<ClientSettings | null>>(),
15 setClientSettings: vi.fn<(settings: ClientSettings) => Promise<void>>(),
16 createTab: vi.fn<DesktopPreviewBridge["createTab"]>(),
17 closeTab: vi.fn<DesktopPreviewBridge["closeTab"]>(),
18 registerWebview: vi.fn<DesktopPreviewBridge["registerWebview"]>(),
19 getPreviewConfig: vi.fn<DesktopPreviewBridge["getPreviewConfig"]>(),
20 activeRecordings: new Set<string>(),
21}));
22
23vi.mock("~/localApi", () => ({
24 ensureLocalApi: () => ({ persistence: mocks }),
25}));
26
27vi.mock("~/components/preview/previewBridge", () => ({
28 previewBridge: {
29 createTab: mocks.createTab,
30 closeTab: mocks.closeTab,
31 registerWebview: mocks.registerWebview,
32 getPreviewConfig: mocks.getPreviewConfig,
33 },
34}));
35
36vi.mock("~/components/preview/usePreviewBridge", () => ({
37 usePreviewBridge: () => undefined,
38}));
39
40vi.mock("./browserRecording", () => ({
41 useActiveBrowserRecordingTabIds: () => mocks.activeRecordings,
42 stopBrowserRecording: async () => null,
43}));
44
45import {
46 __resetClientSettingsPersistenceForTests,
47 ensureClientSettingsHydrated,
48} from "~/hooks/useSettings";
49import { useBrowserSurfaceStore } from "./browserSurfaceStore";
50import * as desktopTabLifetime from "./desktopTabLifetime";
51import { HostedBrowserWebview } from "./HostedBrowserWebview";
52
53let renderer: ReactTestRenderer | undefined;
54
55function deferred<A>() {
56 let resolve!: (value: A) => void;
57 let reject!: (error: unknown) => void;
58 const promise = new Promise<A>((resolvePromise, rejectPromise) => {
59 resolve = resolvePromise;
60 reject = rejectPromise;
61 });
62 return { promise, resolve, reject };
63}
64
65beforeEach(() => {
66 __resetClientSettingsPersistenceForTests();
67 useBrowserSurfaceStore.setState({ activityByTabId: {}, byTabId: {} });
68 mocks.getClientSettings.mockReset();
69 mocks.setClientSettings.mockReset().mockResolvedValue(undefined);
70 mocks.createTab.mockReset().mockResolvedValue(undefined);
71 mocks.closeTab.mockReset().mockResolvedValue(undefined);
72 mocks.registerWebview.mockReset().mockResolvedValue(undefined);
73 mocks.getPreviewConfig.mockReset().mockResolvedValue({
74 partition: "persist:t3-preview-work",
75 webPreferences: "contextIsolation=yes",
76 preloadUrl: null,
77 });
78 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
79 vi.stubGlobal("window", globalThis);
80 vi.stubGlobal("navigator", { platform: "Linux" });
81 vi.stubGlobal(
82 "requestAnimationFrame",
83 vi.fn(() => 0),
84 );
85 vi.stubGlobal("cancelAnimationFrame", vi.fn());
86 vi.spyOn(console, "error").mockImplementation(() => undefined);
87});
88
89afterEach(async () => {
90 vi.useFakeTimers();
91 await act(() => renderer?.unmount());
92 renderer = undefined;
93 await vi.advanceTimersByTimeAsync(0);
94 vi.useRealTimers();
95 __resetClientSettingsPersistenceForTests();
96 useBrowserSurfaceStore.setState({ activityByTabId: {}, byTabId: {} });
97 vi.restoreAllMocks();
98 vi.unstubAllGlobals();
99});
100
101describe("HostedBrowserWebview settings hydration", () => {
102 it("starts a retained background tab only after a settings read succeeds on retry", async () => {
103 const firstRead = deferred<ClientSettings | null>();
104 const retryRead = deferred<ClientSettings | null>();
105 const tabCreation = deferred<void>();
106 mocks.getClientSettings
107 .mockReturnValueOnce(firstRead.promise)
108 .mockReturnValueOnce(retryRead.promise);
109 mocks.createTab.mockReturnValueOnce(tabCreation.promise);
110 const acquire = vi.spyOn(desktopTabLifetime, "acquireDesktopTab");
111 const createGuest = vi.fn((_attributes: unknown) =>
112 Object.assign(new EventTarget(), { getWebContentsId: () => 41 }),
113 );
114 const threadRef = {
115 environmentId: EnvironmentId.make("host-settings-retry"),
116 threadId: ThreadId.make("thread-settings-retry"),
117 };
118 const runtimeTabId = "retained-background-tab";
119 useBrowserSurfaceStore.getState().acquireActivity(runtimeTabId);
120
121 await act(() => {
122 renderer = create(
123 <HostedBrowserWebview
124 threadRef={threadRef}
125 tabId="server-tab"
126 runtimeTabId={runtimeTabId}
127 initialUrl="https://example.com"
128 viewport={FILL_PREVIEW_VIEWPORT}
129 pictureInPicture={false}
130 profileId="work"
131 zoomFactor={1.25}
132 />,
133 {
134 createNodeMock: (element) =>
135 element.type === "webview"
136 ? createGuest(element.props)
137 : { scrollLeft: 0, scrollTop: 0, scrollTo: () => undefined },
138 },
139 );
140 });
141
142 expect(mocks.getClientSettings).toHaveBeenCalledOnce();
143 expect(acquire).not.toHaveBeenCalled();
144 expect(createGuest).not.toHaveBeenCalled();
145 expect(mocks.createTab).not.toHaveBeenCalled();
146
147 const failure = new Error("Saved settings are unavailable");
148 await act(async () => {
149 const hydration = ensureClientSettingsHydrated();
150 firstRead.reject(failure);
151 await expect(hydration).rejects.toBe(failure);
152 });
153 expect(acquire).not.toHaveBeenCalled();
154 expect(createGuest).not.toHaveBeenCalled();
155 expect(mocks.createTab).not.toHaveBeenCalled();
156
157 let retry!: Promise<void>;
158 await act(() => {
159 retry = ensureClientSettingsHydrated();
160 });
161 expect(mocks.getClientSettings).toHaveBeenCalledTimes(2);
162 expect(acquire).not.toHaveBeenCalled();
163 expect(createGuest).not.toHaveBeenCalled();
164 expect(mocks.createTab).not.toHaveBeenCalled();
165
166 await act(async () => {
167 retryRead.resolve({
168 ...DEFAULT_CLIENT_SETTINGS,
169 browserDefaultZoomFactor: 1.25,
170 browserDefaultAppearance: "dark",
171 browserProfiles: [{ id: "work", name: "Work", kind: "persistent" }],
172 browserDefaultProfileId: "work",
173 });
174 await retry;
175 });
176
177 expect(acquire).toHaveBeenCalledExactlyOnceWith(runtimeTabId, undefined);
178 expect(mocks.getPreviewConfig).toHaveBeenCalledExactlyOnceWith(threadRef.environmentId, "work");
179 expect(createGuest).toHaveBeenCalledOnce();
180 expect(createGuest).toHaveBeenCalledWith(
181 expect.objectContaining({
182 partition: "persist:t3-preview-work",
183 src: "https://example.com",
184 }),
185 );
186 expect(mocks.createTab).toHaveBeenCalledExactlyOnceWith(runtimeTabId, {
187 zoomFactor: 1.25,
188 colorScheme: "dark",
189 });
190 expect(mocks.registerWebview).not.toHaveBeenCalled();
191
192 await act(async () => {
193 tabCreation.resolve();
194 await tabCreation.promise;
195 });
196 expect(mocks.registerWebview).toHaveBeenCalledExactlyOnceWith(runtimeTabId, 41);
197 expect(mocks.closeTab).not.toHaveBeenCalled();
198 expect(mocks.setClientSettings).not.toHaveBeenCalled();
199 });
200});