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);
});
});