import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Modal, ModalHeader, ModalTitle, ModalContent, ModalFooter, } from "../Modal"; describe("Modal", () => { // Basic rendering tests describe("Rendering", () => { it("does not render when closed", () => { render(Modal content); expect(screen.queryByText("Modal content")).not.toBeInTheDocument(); }); it("renders when open", () => { render(Modal content); expect(screen.getByText("Modal content")).toBeInTheDocument(); }); it("renders with custom className", () => { render( Content ); const dialog = screen.getByRole("dialog", { hidden: true }); expect(dialog).toHaveClass("custom-modal"); }); it("renders with different sizes", () => { const { rerender } = render( Small ); let dialog = screen.getByRole("dialog", { hidden: true }); expect(dialog).toHaveClass("max-w-md"); rerender( Large ); dialog = screen.getByRole("dialog", { hidden: true }); expect(dialog).toHaveClass("max-w-2xl"); rerender( Full ); dialog = screen.getByRole("dialog", { hidden: true }); expect(dialog).toHaveClass("max-w-[95vw]"); }); }); // Props tests describe("Props", () => { it("renders close button by default", () => { render(Content); expect(screen.getByLabelText("Close")).toBeInTheDocument(); }); it("hides close button when showCloseButton is false", () => { render( Content ); expect(screen.queryByLabelText("Close")).not.toBeInTheDocument(); }); it("sets proper role", () => { const { rerender } = render( Dialog ); let dialog = screen.getByRole("dialog", { hidden: true }); expect(dialog).toHaveAttribute("role", "dialog"); rerender( Alert Dialog ); dialog = screen.getByRole("alertdialog", { hidden: true }); expect(dialog).toHaveAttribute("role", "alertdialog"); }); it("supports ARIA attributes", () => { render( Content ); const dialog = screen.getByRole("dialog", { hidden: true }); expect(dialog).toHaveAttribute("aria-labelledby", "modal-title"); expect(dialog).toHaveAttribute("aria-describedby", "modal-description"); }); }); // Interaction tests describe("Interactions", () => { it("calls onClose when close button is clicked", async () => { const handleClose = jest.fn(); const user = userEvent.setup(); render( Content ); await user.click(screen.getByLabelText("Close")); expect(handleClose).toHaveBeenCalledTimes(1); }); it("calls onOpenChange when close button is clicked", async () => { const handleOpenChange = jest.fn(); const user = userEvent.setup(); render( Content ); await user.click(screen.getByLabelText("Close")); expect(handleOpenChange).toHaveBeenCalledWith(false); }); it("calls onClose when overlay is clicked", async () => { const handleClose = jest.fn(); const user = userEvent.setup(); render( Content ); const overlay = screen.getByRole("dialog", { hidden: true, }).parentElement; await user.click(overlay!); expect(handleClose).toHaveBeenCalledTimes(1); }); it("does not close when overlay is clicked if closeOnOverlayClick is false", async () => { const handleClose = jest.fn(); const user = userEvent.setup(); render( Content ); const overlay = screen.getByRole("dialog", { hidden: true, }).parentElement; await user.click(overlay!); expect(handleClose).not.toHaveBeenCalled(); }); it("calls onClose when Escape key is pressed", async () => { const handleClose = jest.fn(); const user = userEvent.setup(); render( Content ); await user.keyboard("{Escape}"); expect(handleClose).toHaveBeenCalledTimes(1); }); it("does not close when Escape key is pressed if closeOnEscape is false", async () => { const handleClose = jest.fn(); const user = userEvent.setup(); render( Content ); await user.keyboard("{Escape}"); expect(handleClose).not.toHaveBeenCalled(); }); }); // Accessibility tests describe("Accessibility", () => { it("traps focus within modal", async () => { const user = userEvent.setup(); render( ); const firstButton = screen.getByText("First Button"); const secondButton = screen.getByText("Second Button"); // Initially, first button should be focused await waitFor(() => { expect(firstButton).toHaveFocus(); }); // Tab to second button await user.tab(); expect(secondButton).toHaveFocus(); // Tab should wrap to first button (excluding close button from tab order) await user.tab(); expect(firstButton).toHaveFocus(); // Shift+Tab from first should go to second await user.tab({ shift: true }); expect(secondButton).toHaveFocus(); }); it("focuses first focusable element on open", async () => { const { rerender } = render( ); rerender( ); await waitFor(() => { expect(screen.getByText("Focusable Button")).toHaveFocus(); }); }); it("restores focus to trigger element on close", async () => { const triggerButton = document.createElement("button"); triggerButton.textContent = "Open Modal"; document.body.appendChild(triggerButton); triggerButton.focus(); const { rerender } = render(
Modal Content
); rerender(
Modal Content
); // Close modal rerender(
Modal Content
); await waitFor(() => { expect(triggerButton).toHaveFocus(); }); document.body.removeChild(triggerButton); }); it("prevents body scroll when open", () => { const { rerender } = render(Content); expect(document.body).not.toHaveStyle("overflow: hidden"); rerender(Content); expect(document.body).toHaveStyle("overflow: hidden"); }); }); // Edge cases describe("Edge cases", () => { it("forwards ref correctly", () => { const ref = React.createRef(); render( Content ); expect(ref.current).toBeInstanceOf(HTMLDivElement); }); }); }); describe("ModalHeader", () => { it("renders with children", () => { render( Header content ); expect(screen.getByText("Header content")).toBeInTheDocument(); }); it("renders with custom className", () => { render( Header ); const header = screen.getByText("Header"); expect(header).toHaveClass("custom-header"); }); it("forwards ref correctly", () => { const ref = React.createRef(); render( Header ); expect(ref.current).toBeInstanceOf(HTMLDivElement); }); }); describe("ModalTitle", () => { it("renders with children", () => { render( Title content ); expect(screen.getByText("Title content")).toBeInTheDocument(); }); it("renders with different heading levels", () => { const { rerender } = render( Title ); let title = screen.getByText("Title"); expect(title.tagName).toBe("H1"); rerender( Title ); title = screen.getByText("Title"); expect(title.tagName).toBe("H2"); }); it("defaults to h2", () => { render( Default Title ); const title = screen.getByText("Default Title"); expect(title.tagName).toBe("H2"); }); it("forwards ref correctly", () => { const ref = React.createRef(); render( Title ); expect(ref.current).toBeInstanceOf(HTMLHeadingElement); }); }); describe("ModalContent", () => { it("renders with children", () => { render( Content here ); expect(screen.getByText("Content here")).toBeInTheDocument(); }); it("renders with custom className", () => { render( Content ); const content = screen.getByText("Content"); expect(content).toHaveClass("custom-content"); }); it("forwards ref correctly", () => { const ref = React.createRef(); render( Content ); expect(ref.current).toBeInstanceOf(HTMLDivElement); }); }); describe("ModalFooter", () => { it("renders with children", () => { render( Footer content ); expect(screen.getByText("Footer content")).toBeInTheDocument(); }); it("handles different justify options", () => { const { rerender } = render( Start ); let footer = screen.getByText("Start"); expect(footer).toHaveClass("justify-start"); rerender( Center ); footer = screen.getByText("Center"); expect(footer).toHaveClass("justify-center"); rerender( End ); footer = screen.getByText("End"); expect(footer).toHaveClass("justify-end"); rerender( Between ); footer = screen.getByText("Between"); expect(footer).toHaveClass("justify-between"); }); it("forwards ref correctly", () => { const ref = React.createRef(); render( Footer ); expect(ref.current).toBeInstanceOf(HTMLDivElement); }); });