import React from "react";
import { Modal } from "./index";
import { fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
// Mock react-modal
jest.mock("react-modal", () => {
const MockReactModal = ({
isOpen,
children,
className,
overlayClassName,
onRequestClose,
parentSelector,
}: any) => {
// Call parentSelector to cover the arrow function
if (parentSelector) parentSelector();
if (!isOpen) return null;
return (
);
};
MockReactModal.setAppElement = jest.fn();
return MockReactModal;
});
// Allow overriding the transition state for specific tests
let mockTransitionState: string | null = null;
const setMockTransitionState = (state: string | null) => {
mockTransitionState = state;
};
// Mock react-transition-group
jest.mock("react-transition-group", () => ({
Transition: ({ children, in: inProp }: any) => {
const state = mockTransitionState ?? (inProp ? "entered" : "exited");
return children(state);
},
}));
// Mock useBodyScrollLock
jest.mock("../../hooks/use-body-scroll-lock", () => jest.fn());
describe("Modal", () => {
const defaultProps = {
isOpen: true,
onRequestClose: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
describe("Rendering", () => {
it("renders when isOpen is true", () => {
render(Content);
expect(screen.getByTestId("react-modal")).toBeInTheDocument();
expect(screen.getByText("Content")).toBeInTheDocument();
});
it("does not render when isOpen is false", () => {
render(
Content
);
expect(screen.queryByTestId("react-modal")).not.toBeInTheDocument();
});
it("has correct displayName", () => {
expect(Modal.displayName).toBe("Modal");
});
it("renders children in body div", () => {
render(Hello World);
expect(screen.getByText("Hello World")).toBeInTheDocument();
});
});
describe("Title", () => {
it("renders title when provided", () => {
render();
expect(screen.getByText("My Modal")).toBeInTheDocument();
});
it("does not render title section when not provided", () => {
const { container } = render();
expect(container.querySelector(".border-b")).not.toBeInTheDocument();
});
});
describe("Close button", () => {
it("renders close button by default", () => {
render();
expect(screen.getByTestId("close-button")).toBeInTheDocument();
});
it("hides close button when hideCloseButton is true", () => {
render();
expect(screen.queryByTestId("close-button")).not.toBeInTheDocument();
});
it("calls onRequestClose when close button is clicked", () => {
const onRequestClose = jest.fn();
render();
fireEvent.click(screen.getByTestId("close-button"));
expect(onRequestClose).toHaveBeenCalledTimes(1);
});
it("renders close icon in close button", () => {
render();
expect(screen.getByText("close")).toBeInTheDocument();
});
});
describe("Size", () => {
it("applies md size classes by default", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("inset-[15%]");
});
it("applies xl size classes", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("inset-[5%]");
});
it("applies lg size classes", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("inset-[10%]");
});
it("applies sm size classes", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("w-[640px]");
});
it("applies xs size classes", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("w-[calc(100%-30px)]");
});
});
describe("Shape", () => {
it("applies default shape", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).not.toContain("rounded-3xl");
});
it("applies rounded shape", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("rounded-3xl");
});
});
describe("Centered", () => {
it("applies centered classes", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("top-0");
});
});
describe("CSS classes", () => {
it("applies custom className", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("custom-modal");
});
it("applies bodyClassName to body div", () => {
render();
const body = screen
.getByTestId("react-modal")
.querySelector(".custom-body");
expect(body).toBeInTheDocument();
});
it("applies closeButtonClassName", () => {
render();
const closeBtn = screen.getByTestId("close-button");
expect(closeBtn.className).toContain("custom-close");
});
});
describe("Body style", () => {
it("applies bodyStyle to body div", () => {
render(
Content
);
const body = screen.getByText("Content");
// Body content should be present inside the modal
expect(body).toBeInTheDocument();
});
});
describe("Animation", () => {
it("applies popper animation classes by default", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("transition-transform");
});
it("applies bottomSheet animation classes", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("ease-in-out");
});
it("applies exited state classes for popper when not transitioning", () => {
// Since our mock always returns 'entered' state when in=true,
// we can verify it includes entered classes
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("pointer-events-auto");
});
it("returns default popper classes for unknown transition state", () => {
setMockTransitionState("unmounted");
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("opacity-0");
expect(modal.className).toContain("translate-y-[-18px]");
setMockTransitionState(null);
});
it("returns default bottomSheet classes for unknown transition state", () => {
setMockTransitionState("unmounted");
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("opacity-0");
expect(modal.className).toContain("translate-y-full");
setMockTransitionState(null);
});
});
describe("Data attributes", () => {
it("passes data-testid to body div", () => {
render();
const elements = screen.getAllByTestId("modal-body");
expect(elements.length).toBeGreaterThan(0);
});
it("supports parentSelector prop", () => {
// Add a main element to the document
const main = document.createElement("main");
document.body.appendChild(main);
render(
HTMLElement)}
/>
);
expect(screen.getByTestId("react-modal")).toBeInTheDocument();
document.body.removeChild(main);
});
it("falls back to document.body when parentSelector element not found", () => {
render(
HTMLElement)
}
/>
);
expect(screen.getByTestId("react-modal")).toBeInTheDocument();
});
it("applies closeWrapperClassName", () => {
const { container } = render(
);
const wrapper = container.querySelector(".wrapper-class");
expect(wrapper).toBeInTheDocument();
});
it("applies bottomSheet animation classes", () => {
render();
const modal = screen.getByTestId("react-modal");
expect(modal.className).toContain("translate-y-0");
expect(modal.className).toContain("pointer-events-auto");
});
it("returns empty animation classes for unknown animation type", () => {
render();
const modal = screen.getByTestId("react-modal");
// No animation-specific classes, just the base classes
expect(modal.className).not.toContain("translate-y");
expect(modal.className).not.toContain("opacity-0");
});
});
});