1import {
2 DEFAULT_PREVIEW_APPEARANCE,
3 DEFAULT_PREVIEW_ZOOM_FACTOR,
4 DEFAULT_CLIENT_SETTINGS,
5 EnvironmentId,
6 ThreadId,
7} from "@t3tools/contracts";
8import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
9
10const { closeTab, createTab, stopBrowserRecording } = vi.hoisted(() => ({
11 closeTab: vi.fn<(tabId: string) => Promise<void>>(async () => undefined),
12 createTab: vi.fn<() => Promise<void>>(),
13 stopBrowserRecording: vi.fn(async () => null),
14}));
15
16vi.mock("~/components/preview/previewBridge", () => ({
17 previewBridge: { closeTab, createTab },
18}));
19
20vi.mock("./browserRecording", () => ({
21 stopBrowserRecording,
22}));
23
24import { acquireDesktopTab } from "./desktopTabLifetime";
25import { __setClientSettingsForTests } from "~/hooks/useSettings";
26
27const DEFAULT_TAB_STATE = {
28 zoomFactor: DEFAULT_PREVIEW_ZOOM_FACTOR,
29 colorScheme: DEFAULT_PREVIEW_APPEARANCE,
30};
31import { previewRuntimeTabId } from "./previewRuntimeTabId";
32
33describe("desktopTabLifetime", () => {
34 beforeEach(() => {
35 __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS);
36 closeTab.mockClear();
37 createTab.mockClear();
38 stopBrowserRecording.mockClear();
39 vi.stubGlobal("window", globalThis);
40 });
41
42 afterEach(() => {
43 vi.useRealTimers();
44 vi.unstubAllGlobals();
45 vi.restoreAllMocks();
46 });
47
48 it("keeps identical server tab ids from two environments in separate desktop slots", async () => {
49 vi.useFakeTimers();
50 createTab.mockResolvedValue(undefined);
51 const tabA = previewRuntimeTabId(
52 {
53 environmentId: EnvironmentId.make("environment-a"),
54 threadId: ThreadId.make("thread-a"),
55 },
56 "epoch-a",
57 "tab_1",
58 );
59 const tabB = previewRuntimeTabId(
60 {
61 environmentId: EnvironmentId.make("environment-b"),
62 threadId: ThreadId.make("thread-b"),
63 },
64 "epoch-b",
65 "tab_1",
66 );
67
68 const first = acquireDesktopTab(tabA);
69 const second = acquireDesktopTab(tabB);
70 await Promise.all([first.ready, second.ready]);
71
72 expect(createTab).toHaveBeenCalledWith(tabA, DEFAULT_TAB_STATE);
73 expect(createTab).toHaveBeenCalledWith(tabB, DEFAULT_TAB_STATE);
74 expect(createTab).toHaveBeenCalledTimes(2);
75
76 first.release();
77 second.release();
78 await vi.advanceTimersByTimeAsync(0);
79 });
80
81 it("stops recording before closing the final desktop tab lease", async () => {
82 vi.useFakeTimers();
83 let resolveStop: (() => void) | undefined;
84 stopBrowserRecording.mockReturnValueOnce(
85 new Promise<null>((resolve) => {
86 resolveStop = () => resolve(null);
87 }),
88 );
89 createTab.mockResolvedValueOnce(undefined);
90
91 const lease = acquireDesktopTab("tab_recording_cleanup");
92 await lease.ready;
93 lease.release();
94 await vi.advanceTimersByTimeAsync(0);
95
96 expect(stopBrowserRecording).toHaveBeenCalledWith("tab_recording_cleanup");
97 expect(closeTab).not.toHaveBeenCalled();
98
99 resolveStop?.();
100 await Promise.resolve();
101 await Promise.resolve();
102 expect(closeTab).toHaveBeenCalledWith("tab_recording_cleanup");
103 });
104
105 it("waits for an in-flight close before recreating a reacquired tab", async () => {
106 vi.useFakeTimers();
107 let resolveClose: (() => void) | undefined;
108 createTab.mockResolvedValue(undefined);
109 closeTab.mockReturnValueOnce(
110 new Promise<void>((resolve) => {
111 resolveClose = resolve;
112 }),
113 );
114
115 const initial = acquireDesktopTab("tab_close_reacquire");
116 await initial.ready;
117 initial.release();
118 await vi.advanceTimersByTimeAsync(0);
119
120 expect(closeTab).toHaveBeenCalledWith("tab_close_reacquire");
121
122 const reacquired = acquireDesktopTab("tab_close_reacquire");
123 expect(createTab).toHaveBeenCalledTimes(1);
124
125 resolveClose?.();
126 await reacquired.ready;
127 expect(createTab).toHaveBeenCalledTimes(2);
128 });
129});