/** * @vitest-environment jsdom */ import React, { useCallback, useRef, type RefObject } from "react"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import type { View } from "react-native"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useHoverSafeZone } from "./use-hover-safe-zone"; vi.mock("@/constants/platform", () => ({ isWeb: true, })); interface RectInput { left: number; right: number; top: number; bottom: number; } let root: Root | null = null; let container: HTMLElement | null = null; beforeEach(() => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); }); afterEach(() => { if (root) { act(() => { root?.unmount(); }); } root = null; container?.remove(); container = null; vi.unstubAllGlobals(); vi.restoreAllMocks(); }); function installRect(node: HTMLDivElement | null, rect: RectInput): void { if (!node) return; node.getBoundingClientRect = () => ({ ...rect, x: rect.left, y: rect.top, width: rect.right - rect.left, height: rect.bottom - rect.top, toJSON: () => rect, }) as DOMRect; } function Harness({ onEnterSafeZone, onLeaveSafeZone, }: { onEnterSafeZone: () => void; onLeaveSafeZone: () => void; }) { const triggerRef = useRef(null); const contentRef = useRef(null); useHoverSafeZone({ enabled: true, triggerRef: triggerRef as unknown as RefObject, contentRef: contentRef as unknown as RefObject, onEnterSafeZone, onLeaveSafeZone, }); const handleTriggerRef = useCallback((node: HTMLDivElement | null) => { triggerRef.current = node; installRect(node, { left: 0, right: 100, top: 20, bottom: 60 }); }, []); const handleContentRef = useCallback((node: HTMLDivElement | null) => { contentRef.current = node; installRect(node, { left: 120, right: 240, top: 20, bottom: 120 }); }, []); return ( <>
); } function pointerMove(x: number, y: number): void { document.dispatchEvent( new window.MouseEvent("pointermove", { bubbles: true, clientX: x, clientY: y, }), ); } describe("useHoverSafeZone", () => { it("tracks transitions across trigger, bridge, content, and outside space", () => { const onEnterSafeZone = vi.fn(); const onLeaveSafeZone = vi.fn(); act(() => { root?.render(); }); act(() => pointerMove(110, 40)); expect(onEnterSafeZone).toHaveBeenCalledTimes(1); expect(onLeaveSafeZone).not.toHaveBeenCalled(); act(() => pointerMove(300, 40)); expect(onLeaveSafeZone).toHaveBeenCalledTimes(1); act(() => pointerMove(130, 40)); expect(onEnterSafeZone).toHaveBeenCalledTimes(2); }); it("refreshes the safe-zone enter callback while moving inside", () => { const onEnterSafeZone = vi.fn(); const onLeaveSafeZone = vi.fn(); act(() => { root?.render(); }); act(() => pointerMove(110, 40)); act(() => pointerMove(130, 40)); expect(onEnterSafeZone).toHaveBeenCalledTimes(2); expect(onLeaveSafeZone).not.toHaveBeenCalled(); }); it("treats leaving the browser window as leaving the safe zone", () => { const onEnterSafeZone = vi.fn(); const onLeaveSafeZone = vi.fn(); act(() => { root?.render(); }); act(() => { window.dispatchEvent(new window.MouseEvent("pointerout", { bubbles: true })); }); expect(onLeaveSafeZone).toHaveBeenCalledTimes(1); act(() => { window.dispatchEvent(new window.FocusEvent("blur")); }); expect(onLeaveSafeZone).toHaveBeenCalledTimes(1); }); });