1import { afterEach, expect, it, vi } from "vite-plus/test";
2import { bindPhoneTrackpad } from "./phoneTrackpad";
3
4afterEach(() => vi.useRealTimers());
5
6class Canvas extends EventTarget {
7 getBoundingClientRect() {
8 return { width: 400, height: 800 } as DOMRect;
9 }
10}
11const wheel = (ctrlKey = false) =>
12 Object.assign(new Event("wheel", { cancelable: true }), {
13 deltaX: 16,
14 deltaY: -20,
15 deltaMode: 0,
16 ctrlKey,
17 });
18const gesture = (type: string, scale: number) =>
19 Object.assign(new Event(type, { cancelable: true }), { scale });
20
21it("consumes scrolling and page zoom only on the bound canvas and releases listeners on detach", () => {
22 const canvas = new Canvas();
23 const navigate = vi.fn(() => true);
24 const listeners = vi.spyOn(canvas, "addEventListener");
25 const binding = bindPhoneTrackpad(canvas, { navigate, endWheel: vi.fn() });
26 const swipe = wheel();
27 canvas.dispatchEvent(swipe);
28 const pinch = wheel(true);
29 canvas.dispatchEvent(pinch);
30 expect(swipe.defaultPrevented).toBe(true);
31 expect(pinch.defaultPrevented).toBe(true);
32 expect(navigate.mock.calls).toEqual([[{ type: "orbit", x: -0.04, y: 0.025 }]]);
33 expect(
34 listeners.mock.calls.every(
35 ([, , options]) => typeof options === "object" && options.passive === false,
36 ),
37 ).toBe(true);
38 binding.dispose();
39 const detached = wheel(true);
40 canvas.dispatchEvent(detached);
41 expect(detached.defaultPrevented).toBe(false);
42 expect(navigate).toHaveBeenCalledOnce();
43});
44
45it("consumes Safari pinch without zooming the model and resumes orbit after cancellation", () => {
46 const canvas = new Canvas();
47 const navigate = vi.fn(() => true);
48 const binding = bindPhoneTrackpad(canvas, { navigate, endWheel: vi.fn() });
49 canvas.dispatchEvent(gesture("gesturestart", 1));
50 canvas.dispatchEvent(gesture("gesturechange", 1.2));
51 canvas.dispatchEvent(wheel(true));
52 expect(navigate).not.toHaveBeenCalled();
53 binding.cancel();
54 canvas.dispatchEvent(wheel());
55 expect(navigate).toHaveBeenCalledOnce();
56 binding.dispose();
57});
58
59it("normalizes Chrome and Safari pinch, expires wheel sequences and cancels on detach", () => {
60 vi.useFakeTimers();
61 const canvas = new Canvas();
62 const pinch = { begin: vi.fn(() => true), move: vi.fn(), end: vi.fn() };
63 const navigate = vi.fn(() => true);
64 const binding = bindPhoneTrackpad(canvas, { navigate, endWheel: vi.fn() }, pinch);
65 canvas.dispatchEvent(wheel(true));
66 canvas.dispatchEvent(wheel(true));
67 expect(pinch.begin).toHaveBeenCalledOnce();
68 expect(pinch.move.mock.calls).toEqual([[0.2], [0.2]]);
69 vi.advanceTimersByTime(180);
70 expect(pinch.end).toHaveBeenCalledOnce();
71 canvas.dispatchEvent(gesture("gesturestart", 1));
72 canvas.dispatchEvent(gesture("gesturechange", 1.2));
73 expect(pinch.move).toHaveBeenLastCalledWith(Math.log(1.2));
74 canvas.dispatchEvent(wheel(true));
75 expect(pinch.move).toHaveBeenCalledTimes(3);
76 binding.dispose();
77 const count = pinch.move.mock.calls.length;
78 vi.advanceTimersByTime(500);
79 canvas.dispatchEvent(wheel(true));
80 expect(pinch.move).toHaveBeenCalledTimes(count);
81 expect(navigate).not.toHaveBeenCalled();
82});
83
84it("keeps a paused wheel orbit active until native release or browser fallback", () => {
85 vi.useFakeTimers();
86 const canvas = new Canvas();
87 const navigate = vi.fn(() => true);
88 const endWheel = vi.fn();
89 const binding = bindPhoneTrackpad(canvas, { navigate, endWheel });
90 canvas.dispatchEvent(wheel());
91 vi.advanceTimersByTime(500);
92 expect(endWheel).not.toHaveBeenCalled();
93 binding.endOrbit();
94 expect(endWheel).toHaveBeenCalledOnce();
95 canvas.dispatchEvent(wheel());
96 vi.advanceTimersByTime(1199);
97 expect(endWheel).toHaveBeenCalledOnce();
98 vi.advanceTimersByTime(1);
99 expect(endWheel).toHaveBeenCalledTimes(2);
100 canvas.dispatchEvent(wheel());
101 canvas.dispatchEvent(Object.assign(wheel(), { momentum: true }));
102 expect(endWheel).toHaveBeenCalledTimes(3);
103 expect(navigate).toHaveBeenCalledTimes(3);
104 binding.dispose();
105});