import { render, fireEvent } from "@testing-library/react"; import React from "react"; import { ClickOutsideWrapper } from "../click_outside_wrapper"; describe("ClickOutsideWrapper", () => { let onClickOutsideMock: jest.Mock; beforeEach(() => { onClickOutsideMock = jest.fn(); }); afterEach(() => { onClickOutsideMock.mockClear(); }); it("renders children correctly", () => { const { container } = render(
Test Content
, ); expect(container.querySelector('[data-testid="child"]')).toBeTruthy(); }); it("calls onClickOutside when clicking outside the wrapper", () => { const { container } = render(
Inside
Outside
, ); const outsideElement = container.querySelector( '[data-testid="outside"]', ) as HTMLElement; fireEvent.mouseDown(outsideElement); expect(onClickOutsideMock).toHaveBeenCalledTimes(1); }); it("does not call onClickOutside when clicking inside the wrapper", () => { const { container } = render(
Inside
, ); const insideElement = container.querySelector( '[data-testid="inside"]', ) as HTMLElement; fireEvent.mouseDown(insideElement); expect(onClickOutsideMock).not.toHaveBeenCalled(); }); it("applies custom className", () => { const { container } = render(
Content
, ); const wrapper = container.firstChild as HTMLElement; expect(wrapper.className).toBe("custom-class"); }); it("applies custom style", () => { const customStyle = { backgroundColor: "red", padding: "10px" }; const { container } = render(
Content
, ); const wrapper = container.firstChild as HTMLElement; expect(wrapper.style.backgroundColor).toBe("red"); expect(wrapper.style.padding).toBe("10px"); }); it("does not call onClickOutside when clicking on element with ignoreClass", () => { const { container } = render(
Inside
Ignored
, ); const ignoredElement = container.querySelector( '[data-testid="ignored"]', ) as HTMLElement; fireEvent.mouseDown(ignoredElement); expect(onClickOutsideMock).not.toHaveBeenCalled(); }); it("calls onClickOutside when clicking on element without ignoreClass", () => { const { container } = render(
Inside
Not Ignored
, ); const notIgnoredElement = container.querySelector( '[data-testid="not-ignored"]', ) as HTMLElement; fireEvent.mouseDown(notIgnoredElement); expect(onClickOutsideMock).toHaveBeenCalledTimes(1); }); it("uses containerRef when provided", () => { const containerRef = React.createRef(); render(
Content
, ); expect(containerRef.current).toBeTruthy(); expect(containerRef.current?.tagName).toBe("DIV"); }); it("handles composedPath events (e.g. shadow DOM)", () => { render(
Inside
, ); const outsideNode = document.createElement("div"); document.body.appendChild(outsideNode); const event = new MouseEvent("mousedown", { bubbles: true, composed: true, }); Object.defineProperty(event, "composed", { value: true }); Object.defineProperty(event, "composedPath", { value: () => [outsideNode, document.body], }); outsideNode.dispatchEvent(event); expect(onClickOutsideMock).toHaveBeenCalled(); document.body.removeChild(outsideNode); }); it("cleans up event listener on unmount", () => { const removeEventListenerSpy = jest.spyOn(document, "removeEventListener"); const { unmount } = render(
Content
, ); unmount(); expect(removeEventListenerSpy).toHaveBeenCalledWith( "mousedown", expect.any(Function), ); removeEventListenerSpy.mockRestore(); }); it("invokes handler registered on document with composedPath target", () => { const addEventListenerSpy = jest.spyOn(document, "addEventListener"); const removeEventListenerSpy = jest.spyOn(document, "removeEventListener"); const { unmount } = render(
Inside
, ); const handlerEntry = addEventListenerSpy.mock.calls.find( ([type]) => type === "mousedown", ); const handler = handlerEntry?.[1] as EventListener; const outsideNode = document.createElement("div"); const mockEvent = { composed: true, composedPath: () => [outsideNode], target: outsideNode, } as unknown as MouseEvent; handler(mockEvent); expect(onClickOutsideMock).toHaveBeenCalledTimes(1); unmount(); addEventListenerSpy.mockRestore(); removeEventListenerSpy.mockRestore(); }); it("falls back to event.target when composedPath does not return nodes", () => { const addEventListenerSpy = jest.spyOn(document, "addEventListener"); render(
Inside
, ); const handlerEntry = addEventListenerSpy.mock.calls.find( ([type]) => type === "mousedown", ); const handler = handlerEntry?.[1] as EventListener; const outsideNode = document.createElement("div"); const mockEvent = { composed: true, composedPath: () => [{}], target: outsideNode, } as unknown as MouseEvent; handler(mockEvent); expect(onClickOutsideMock).toHaveBeenCalledTimes(1); addEventListenerSpy.mockRestore(); }); it("does not treat non-HTMLElement targets as ignored elements", () => { const addEventListenerSpy = jest.spyOn(document, "addEventListener"); render(
Inside
, ); const handlerEntry = addEventListenerSpy.mock.calls.find( ([type]) => type === "mousedown", ); const handler = handlerEntry?.[1] as EventListener; const textNode = document.createTextNode("outside"); const mockEvent = { composed: false, target: textNode, } as unknown as MouseEvent; handler(mockEvent); expect(onClickOutsideMock).toHaveBeenCalledTimes(1); addEventListenerSpy.mockRestore(); }); });