import React from "react"; import { useOutsideClick } from "./use-outside-click"; import { renderHook } from "@testing-library/react"; describe("useOutsideClick", () => { let callback: jest.Mock; beforeEach(() => { callback = jest.fn(); }); describe("Event listener lifecycle", () => { it("adds click event listener on mount", () => { const addSpy = jest.spyOn(document, "addEventListener"); const ref = { current: document.createElement("div") }; renderHook(() => useOutsideClick(ref, callback)); const clickCalls = addSpy.mock.calls.filter(c => c[0] === "click"); expect(clickCalls.length).toBeGreaterThanOrEqual(1); addSpy.mockRestore(); }); it("removes click event listener on unmount", () => { const removeSpy = jest.spyOn(document, "removeEventListener"); const ref = { current: document.createElement("div") }; const { unmount } = renderHook(() => useOutsideClick(ref, callback)); unmount(); const clickCalls = removeSpy.mock.calls.filter(c => c[0] === "click"); expect(clickCalls.length).toBeGreaterThanOrEqual(1); removeSpy.mockRestore(); }); }); describe("Click detection", () => { it("calls callback when clicking outside the ref element", () => { const inside = document.createElement("div"); const outside = document.createElement("button"); document.body.appendChild(inside); document.body.appendChild(outside); const ref = { current: inside }; renderHook(() => useOutsideClick(ref, callback)); outside.click(); expect(callback).toHaveBeenCalledTimes(1); document.body.removeChild(inside); document.body.removeChild(outside); }); it("does not call callback when clicking inside the ref element", () => { const container = document.createElement("div"); const child = document.createElement("span"); container.appendChild(child); document.body.appendChild(container); const ref = { current: container }; renderHook(() => useOutsideClick(ref, callback)); child.click(); expect(callback).not.toHaveBeenCalled(); document.body.removeChild(container); }); it("does not call callback when clicking on the ref element itself", () => { const el = document.createElement("div"); document.body.appendChild(el); const ref = { current: el }; renderHook(() => useOutsideClick(ref, callback)); el.click(); expect(callback).not.toHaveBeenCalled(); document.body.removeChild(el); }); it("calls callback when ref.current is null", () => { const ref = { current: null } as React.RefObject; renderHook(() => useOutsideClick(ref, callback)); document.body.click(); expect(callback).toHaveBeenCalledTimes(1); }); it("calls callback when ref is undefined-like", () => { const ref = {} as React.RefObject; renderHook(() => useOutsideClick(ref, callback)); document.body.click(); expect(callback).toHaveBeenCalledTimes(1); }); it("calls callback when ref is null", () => { renderHook(() => useOutsideClick(null as any, callback)); document.body.click(); expect(callback).toHaveBeenCalledTimes(1); }); }); describe("Multiple clicks", () => { it("calls callback for each outside click", () => { const inside = document.createElement("div"); document.body.appendChild(inside); const ref = { current: inside }; renderHook(() => useOutsideClick(ref, callback)); document.body.click(); document.body.click(); document.body.click(); expect(callback).toHaveBeenCalledTimes(3); document.body.removeChild(inside); }); }); describe("Re-render behavior", () => { it("updates listener on re-render with new callback", () => { const el = document.createElement("div"); document.body.appendChild(el); const callback1 = jest.fn(); const callback2 = jest.fn(); const ref = { current: el }; const { rerender } = renderHook(({ cb }) => useOutsideClick(ref, cb), { initialProps: { cb: callback1 }, }); document.body.click(); expect(callback1).toHaveBeenCalledTimes(1); rerender({ cb: callback2 }); document.body.click(); expect(callback2).toHaveBeenCalledTimes(1); document.body.removeChild(el); }); }); });