1import { act, type PointerEvent as ReactPointerEvent, useLayoutEffect } from "react";
2import { create, type ReactTestRenderer } from "react-test-renderer";
3import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
4
5import { commitBrowserViewportChange } from "./browserViewportActions";
6import { useBrowserViewportResize } from "./useBrowserViewportResize";
7
8vi.mock("./browserViewportActions", () => ({ commitBrowserViewportChange: vi.fn(async () => {}) }));
9
10let renderer: ReactTestRenderer;
11let result: ReturnType<typeof useBrowserViewportResize>;
12let events: EventTarget;
13let captured = false;
14const target = Object.assign(new EventTarget(), {
15 setPointerCapture: () => {
16 captured = true;
17 },
18 hasPointerCapture: () => captured,
19 releasePointerCapture: () => {
20 captured = false;
21 },
22});
23
24function pointer(clientX: number, options: { button?: number; pointerId?: number } = {}) {
25 return {
26 button: options.button ?? 0,
27 pointerId: options.pointerId ?? 1,
28 clientX,
29 clientY: 100,
30 target,
31 currentTarget: target,
32 preventDefault() {},
33 stopPropagation() {},
34 } as unknown as ReactPointerEvent<HTMLButtonElement>;
35}
36
37function dispatch(type: string, clientX = 100, destination = events) {
38 destination.dispatchEvent(
39 Object.assign(new Event(type, { cancelable: true }), { pointerId: 1, clientX, clientY: 100 }),
40 );
41}
42
43function Viewport({ tabId = "one" }: { tabId?: string }) {
44 const resize = useBrowserViewportResize({
45 tabId,
46 viewport: { _tag: "freeform", width: 800, height: 600 },
47 zoomFactor: 1,
48 containerSize: { width: 1200, height: 900 },
49 deviceToolbarVisible: true,
50 aspectRatio: null,
51 });
52 useLayoutEffect(() => {
53 result = resize;
54 });
55 return null;
56}
57
58beforeEach(async () => {
59 events = new EventTarget();
60 captured = false;
61 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
62 vi.stubGlobal("window", {
63 addEventListener: events.addEventListener.bind(events),
64 removeEventListener: events.removeEventListener.bind(events),
65 });
66 await act(() => {
67 renderer = create(<Viewport />);
68 });
69});
70
71afterEach(async () => {
72 await act(() => renderer.unmount());
73 vi.unstubAllGlobals();
74 vi.clearAllMocks();
75});
76
77it("commits the final release position even without a move frame", async () => {
78 await act(async () => {
79 result.handleResizePointerDown("east", pointer(100));
80 dispatch("pointerup", 225);
81 });
82 expect(commitBrowserViewportChange).toHaveBeenCalledExactlyOnceWith("one", {
83 _tag: "freeform",
84 width: 1050,
85 height: 600,
86 });
87 expect(captured).toBe(false);
88});
89
90it.each(["cancel", "lost capture", "blur", "unmount", "tab switch"])(
91 "abandons the resize after %s",
92 async (reason) => {
93 await act(() => {
94 result.handleResizePointerDown("east", pointer(100));
95 dispatch("pointermove", 175);
96 });
97 expect(result.effectiveViewport).toEqual({ _tag: "freeform", width: 950, height: 600 });
98 await act(() => {
99 if (reason === "cancel") dispatch("pointercancel");
100 if (reason === "lost capture") dispatch("lostpointercapture", 100, target);
101 if (reason === "blur") dispatch("blur");
102 if (reason === "unmount") renderer.unmount();
103 if (reason === "tab switch") renderer.update(<Viewport tabId="two" />);
104 });
105 await act(() => dispatch("pointerup", 300));
106 expect(commitBrowserViewportChange).not.toHaveBeenCalled();
107 expect(captured).toBe(false);
108 if (reason !== "unmount")
109 expect(result.effectiveViewport).toEqual({ _tag: "freeform", width: 800, height: 600 });
110 },
111);
112
113it("ignores secondary buttons and selectable descendants", async () => {
114 await act(() => {
115 result.handleResizePointerDown("east", pointer(100, { button: 2 }));
116 result.handleResizePointerDown("east", {
117 ...pointer(100),
118 target: {},
119 } as ReactPointerEvent<HTMLButtonElement>);
120 dispatch("pointerup", 225);
121 });
122 expect(commitBrowserViewportChange).not.toHaveBeenCalled();
123 expect(captured).toBe(false);
124});