/** * @vitest-environment happy-dom */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { renderHook, act } from "@testing-library/react"; import { useRef } from "react"; import { useClickOutside } from "./useClickOutside"; function fireMouseDown(target: EventTarget) { target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); } describe("useClickOutside", () => { beforeEach(() => { vi.clearAllMocks(); }); it("does not call handler when enabled is false", () => { const handler = vi.fn(); renderHook(() => { const ref = useRef(document.createElement("div")); useClickOutside([ref], handler, false); return ref; }); act(() => fireMouseDown(document.body)); expect(handler).not.toHaveBeenCalled(); }); it("calls handler when clicking outside all refs", () => { const handler = vi.fn(); const el = document.createElement("div"); document.body.appendChild(el); renderHook(() => { const ref = useRef(el); useClickOutside([ref], handler, true); }); act(() => fireMouseDown(document.body)); expect(handler).toHaveBeenCalledTimes(1); document.body.removeChild(el); }); it("does not call handler when clicking inside the ref element", () => { const handler = vi.fn(); const el = document.createElement("div"); document.body.appendChild(el); renderHook(() => { const ref = useRef(el); useClickOutside([ref], handler, true); }); act(() => fireMouseDown(el)); expect(handler).not.toHaveBeenCalled(); document.body.removeChild(el); }); it("does not call handler when clicking inside a child of the ref element", () => { const handler = vi.fn(); const parent = document.createElement("div"); const child = document.createElement("button"); parent.appendChild(child); document.body.appendChild(parent); renderHook(() => { const ref = useRef(parent); useClickOutside([ref], handler, true); }); act(() => fireMouseDown(child)); expect(handler).not.toHaveBeenCalled(); document.body.removeChild(parent); }); it("does not call handler when clicking inside any of multiple refs", () => { const handler = vi.fn(); const el1 = document.createElement("div"); const el2 = document.createElement("div"); document.body.appendChild(el1); document.body.appendChild(el2); renderHook(() => { const ref1 = useRef(el1); const ref2 = useRef(el2); useClickOutside([ref1, ref2], handler, true); }); act(() => fireMouseDown(el1)); expect(handler).not.toHaveBeenCalled(); act(() => fireMouseDown(el2)); expect(handler).not.toHaveBeenCalled(); document.body.removeChild(el1); document.body.removeChild(el2); }); it("stops calling handler after enabled becomes false", () => { const handler = vi.fn(); const el = document.createElement("div"); document.body.appendChild(el); const { rerender } = renderHook( ({ enabled }: { enabled: boolean }) => { const ref = useRef(el); useClickOutside([ref], handler, enabled); }, { initialProps: { enabled: true } }, ); act(() => fireMouseDown(document.body)); expect(handler).toHaveBeenCalledTimes(1); rerender({ enabled: false }); act(() => fireMouseDown(document.body)); expect(handler).toHaveBeenCalledTimes(1); // no additional calls document.body.removeChild(el); }); it("removes listener on unmount", () => { const handler = vi.fn(); const el = document.createElement("div"); document.body.appendChild(el); const { unmount } = renderHook(() => { const ref = useRef(el); useClickOutside([ref], handler, true); }); unmount(); act(() => fireMouseDown(document.body)); expect(handler).not.toHaveBeenCalled(); document.body.removeChild(el); }); });