1import type { PreviewViewportSetting } from "@t3tools/contracts";
2import { describe, expect, it, vi } from "vite-plus/test";
3
4import {
5 BROWSER_VIEWPORT_COMMIT_TIMEOUT_MS,
6 commitBrowserViewportChange,
7 runBrowserViewportMutation,
8 subscribeBrowserViewportChange,
9} from "./browserViewportActions";
10
11describe("browserViewportActions", () => {
12 it("routes drag commits to the visible tab handler and cleans up exactly that handler", async () => {
13 const first = vi.fn(async () => undefined);
14 const second = vi.fn(async () => undefined);
15 const unsubscribeFirst = subscribeBrowserViewportChange("tab-1", first);
16 const unsubscribeSecond = subscribeBrowserViewportChange("tab-1", second);
17
18 unsubscribeFirst();
19 await commitBrowserViewportChange("tab-1", {
20 _tag: "freeform",
21 width: 900,
22 height: 700,
23 });
24 expect(first).not.toHaveBeenCalled();
25 expect(second).toHaveBeenCalledWith({ _tag: "freeform", width: 900, height: 700 });
26
27 unsubscribeSecond();
28 await expect(
29 commitBrowserViewportChange("tab-1", {
30 _tag: "freeform",
31 width: 800,
32 height: 600,
33 }),
34 ).rejects.toThrow("No visible browser viewport handler");
35 });
36
37 it("commits viewport changes in order for each tab", async () => {
38 let releaseFirst: (() => void) | undefined;
39 let markFirstStarted: (() => void) | undefined;
40 const firstPending = new Promise<void>((resolve) => {
41 releaseFirst = resolve;
42 });
43 const firstStarted = new Promise<void>((resolve) => {
44 markFirstStarted = resolve;
45 });
46 const calls: Array<number> = [];
47 const unsubscribe = subscribeBrowserViewportChange("tab-serial", async (setting) => {
48 if (setting._tag === "fill") return;
49 calls.push(setting.width);
50 if (setting.width === 800) {
51 markFirstStarted?.();
52 await firstPending;
53 }
54 });
55
56 const first = commitBrowserViewportChange("tab-serial", {
57 _tag: "freeform",
58 width: 800,
59 height: 600,
60 });
61 const second = commitBrowserViewportChange("tab-serial", {
62 _tag: "freeform",
63 width: 900,
64 height: 700,
65 });
66 await firstStarted;
67 expect(calls).toEqual([800]);
68
69 releaseFirst?.();
70 await Promise.all([first, second]);
71 expect(calls).toEqual([800, 900]);
72 unsubscribe();
73 });
74
75 it("serializes background mutations with visible viewport commits", async () => {
76 let releaseBackground: (() => void) | undefined;
77 const backgroundPending = new Promise<void>((resolve) => {
78 releaseBackground = resolve;
79 });
80 const calls: string[] = [];
81 const background = runBrowserViewportMutation("tab-shared", async () => {
82 calls.push("background");
83 await backgroundPending;
84 });
85 const unsubscribe = subscribeBrowserViewportChange("tab-shared", async () => {
86 calls.push("visible");
87 });
88 const visible = commitBrowserViewportChange("tab-shared", {
89 _tag: "freeform",
90 width: 900,
91 height: 700,
92 });
93
94 await vi.waitFor(() => expect(calls).toEqual(["background"]));
95 releaseBackground?.();
96 await Promise.all([background, visible]);
97
98 expect(calls).toEqual(["background", "visible"]);
99 unsubscribe();
100 });
101
102 it("does not let a timed-out handler overtake a newer viewport commit", async () => {
103 vi.useFakeTimers();
104 try {
105 let releaseFirst: (() => void) | undefined;
106 const delayed = new Promise<void>((resolve) => {
107 releaseFirst = resolve;
108 });
109 const handler = vi.fn(async (_setting: PreviewViewportSetting): Promise<void> => undefined);
110 handler.mockImplementationOnce(() => delayed).mockResolvedValueOnce(undefined);
111 const unsubscribe = subscribeBrowserViewportChange("tab-timeout", handler);
112 const first = commitBrowserViewportChange("tab-timeout", {
113 _tag: "freeform",
114 width: 800,
115 height: 600,
116 });
117 const firstResult = expect(first).rejects.toThrow(
118 "Timed out committing the browser viewport for tab tab-timeout",
119 );
120 const second = commitBrowserViewportChange("tab-timeout", {
121 _tag: "freeform",
122 width: 900,
123 height: 700,
124 });
125
126 await vi.advanceTimersByTimeAsync(BROWSER_VIEWPORT_COMMIT_TIMEOUT_MS);
127 await firstResult;
128 expect(handler).toHaveBeenCalledTimes(1);
129
130 releaseFirst?.();
131 await second;
132
133 expect(handler).toHaveBeenCalledTimes(2);
134 expect(handler.mock.calls[1]?.[0]).toMatchObject({ width: 900, height: 700 });
135 unsubscribe();
136 } finally {
137 vi.useRealTimers();
138 }
139 });
140});