| 1 | import { afterEach, expect, it, vi } from "vite-plus/test"; |
| 2 | import { bindPhoneTrackpad } from "./phoneTrackpad"; |
| 3 | |
| 4 | afterEach(() => vi.useRealTimers()); |
| 5 | |
| 6 | class Canvas extends EventTarget { |
| 7 | getBoundingClientRect() { |
| 8 | return { width: 400, height: 800 } as DOMRect; |
| 9 | } |
| 10 | } |
| 11 | const wheel = (ctrlKey = false) => |
| 12 | Object.assign(new Event("wheel", { cancelable: true }), { |
| 13 | deltaX: 16, |
| 14 | deltaY: -20, |
| 15 | deltaMode: 0, |
| 16 | ctrlKey, |
| 17 | }); |
| 18 | const gesture = (type: string, scale: number) => |
| 19 | Object.assign(new Event(type, { cancelable: true }), { scale }); |
| 20 | |
| 21 | it("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 | |
| 45 | it("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 | |
| 59 | it("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 | |
| 84 | it("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 | }); |