// @vitest-environment jsdom import { act, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useSwipeToDismiss } from "../use-swipe-to-dismiss.js"; const PANEL_WIDTH = 300; function makeTouchEvent(clientX: number, clientY = 0): React.TouchEvent { return { touches: [{ clientX, clientY }], currentTarget: { getBoundingClientRect: () => ({ width: PANEL_WIDTH }), }, } as unknown as React.TouchEvent; } describe("useSwipeToDismiss", () => { beforeEach(() => { vi.spyOn(Date, "now").mockReturnValue(0); }); afterEach(() => { vi.restoreAllMocks(); }); it("starts with offsetX 0 and isSwiping false", () => { const { result } = renderHook(() => useSwipeToDismiss(vi.fn())); expect(result.current.offsetX).toBe(0); expect(result.current.isSwiping).toBe(false); }); it("horizontal drag updates offsetX and sets isSwiping", () => { const { result } = renderHook(() => useSwipeToDismiss(vi.fn())); act(() => result.current.handlers.onTouchStart(makeTouchEvent(0))); act(() => result.current.handlers.onTouchMove(makeTouchEvent(50))); expect(result.current.offsetX).toBe(50); expect(result.current.isSwiping).toBe(true); }); it("vertical drag is ignored after direction lock", () => { const { result } = renderHook(() => useSwipeToDismiss(vi.fn())); act(() => result.current.handlers.onTouchStart(makeTouchEvent(0, 0))); // Move vertically > 10px to lock vertical act(() => result.current.handlers.onTouchMove(makeTouchEvent(0, 20))); expect(result.current.offsetX).toBe(0); }); it("small movement does not lock direction", () => { const { result } = renderHook(() => useSwipeToDismiss(vi.fn())); act(() => result.current.handlers.onTouchStart(makeTouchEvent(0))); act(() => result.current.handlers.onTouchMove(makeTouchEvent(5))); // No direction locked yet, no update expect(result.current.offsetX).toBe(0); expect(result.current.isSwiping).toBe(false); }); it("leftward drag is clamped to 0", () => { const { result } = renderHook(() => useSwipeToDismiss(vi.fn())); act(() => result.current.handlers.onTouchStart(makeTouchEvent(100))); act(() => result.current.handlers.onTouchMove(makeTouchEvent(50))); expect(result.current.offsetX).toBe(0); }); it("calls onDismiss when ratio exceeds threshold", () => { const onDismiss = vi.fn(); const { result } = renderHook(() => useSwipeToDismiss(onDismiss)); act(() => result.current.handlers.onTouchStart(makeTouchEvent(0))); // Move > 35% of panel width (300 * 0.35 = 105) act(() => result.current.handlers.onTouchMove(makeTouchEvent(120))); vi.spyOn(Date, "now").mockReturnValue(5000); // slow swipe act(() => result.current.handlers.onTouchEnd()); expect(onDismiss).toHaveBeenCalled(); }); it("calls onDismiss with fast velocity", () => { const onDismiss = vi.fn(); const { result } = renderHook(() => useSwipeToDismiss(onDismiss)); act(() => result.current.handlers.onTouchStart(makeTouchEvent(0))); // Small distance but fast act(() => result.current.handlers.onTouchMove(makeTouchEvent(30))); // Very fast: 30px in 0.1s = 300px/s, velocity = 300/300 = 1.0 > 0.5 vi.spyOn(Date, "now").mockReturnValue(100); act(() => result.current.handlers.onTouchEnd()); expect(onDismiss).toHaveBeenCalled(); }); it("does not dismiss when below thresholds", () => { const onDismiss = vi.fn(); const { result } = renderHook(() => useSwipeToDismiss(onDismiss)); act(() => result.current.handlers.onTouchStart(makeTouchEvent(0))); // Small distance, slow speed act(() => result.current.handlers.onTouchMove(makeTouchEvent(20))); vi.spyOn(Date, "now").mockReturnValue(5000); act(() => result.current.handlers.onTouchEnd()); expect(onDismiss).not.toHaveBeenCalled(); expect(result.current.offsetX).toBe(0); expect(result.current.isSwiping).toBe(false); }); });