import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Dialog } from "./dialog";
import { DialogModal } from "./dialog-modal";
describe("Dialog", () => {
describe("Controlled Component Behavior", () => {
it("renders dialog when isOpen is true", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
expect(dialog).toBeInTheDocument();
expect(screen.getByText("Test Dialog")).toBeInTheDocument();
expect(screen.getByText("Dialog content")).toBeInTheDocument();
});
it("does not render dialog when isOpen is false", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.queryByRole("dialog");
// Dialog element exists in DOM but should not have 'open' attribute
if (dialog) {
expect(dialog).not.toHaveAttribute("open");
}
});
it("calls onOpenChange with false when close button is clicked", async () => {
const user = userEvent.setup();
const onOpenChange = vi.fn();
render(
);
const closeButton = screen.getByRole("button", { name: /close dialog/i });
await user.click(closeButton);
expect(onOpenChange).toHaveBeenCalledWith(false);
});
it("calls deprecated onClose callback for backward compatibility", async () => {
const user = userEvent.setup();
const onOpenChange = vi.fn();
const onClose = vi.fn();
render(
);
const closeButton = screen.getByRole("button", { name: /close dialog/i });
await user.click(closeButton);
expect(onOpenChange).toHaveBeenCalledWith(false);
expect(onClose).toHaveBeenCalled();
});
});
describe("Modal vs Non-Modal Behavior", () => {
it("renders as modal dialog by default (role='dialog')", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
expect(dialog).toBeInTheDocument();
expect(dialog).toHaveAttribute("aria-modal", "true");
});
it("renders as alert dialog when isAlertDialog is true", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("alertdialog");
expect(dialog).toBeInTheDocument();
// Alert dialogs (inline) should not have aria-modal
expect(dialog).not.toHaveAttribute("aria-modal", "true");
});
});
describe("Accessibility (ARIA Attributes)", () => {
it("links dialog to title with aria-labelledby", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
const title = screen.getByText("Accessible Dialog");
expect(dialog).toHaveAttribute("aria-labelledby");
expect(title).toHaveAttribute("id");
const labelledBy = dialog.getAttribute("aria-labelledby");
const titleId = title.getAttribute("id");
expect(labelledBy).toBe(titleId);
});
it("links dialog to content with aria-describedby", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
const content = screen.getByText("Dialog description").closest("section");
expect(dialog).toHaveAttribute("aria-describedby");
expect(content).toHaveAttribute("id");
const describedBy = dialog.getAttribute("aria-describedby");
const contentId = content?.getAttribute("id");
expect(describedBy).toBe(contentId);
});
it("applies custom aria-label when provided", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveAttribute("aria-label", "Custom accessible label");
});
it("close button has accessible label", () => {
const onOpenChange = vi.fn();
render(
);
const closeButton = screen.getByRole("button", { name: /close dialog/i });
expect(closeButton).toHaveAttribute("aria-label", "Close dialog");
});
});
describe("Dialog Footer", () => {
it("shows footer with cancel and confirm buttons by default", () => {
const onOpenChange = vi.fn();
const onConfirm = vi.fn();
render(
);
expect(screen.getByRole("button", { name: /cancel/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /confirm/i })).toBeInTheDocument();
});
it("hides footer when hideFooter is true", () => {
const onOpenChange = vi.fn();
render(
);
expect(screen.queryByRole("button", { name: /cancel/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
});
it("calls onConfirm when confirm button is clicked", async () => {
const user = userEvent.setup();
const onOpenChange = vi.fn();
const onConfirm = vi.fn();
render(
);
const confirmButton = screen.getByRole("button", { name: /confirm/i });
await user.click(confirmButton);
expect(onConfirm).toHaveBeenCalled();
});
it("uses custom button labels when provided", () => {
const onOpenChange = vi.fn();
const onConfirm = vi.fn();
render(
);
expect(screen.getByRole("button", { name: /delete/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /keep/i })).toBeInTheDocument();
});
});
describe("Custom Styling", () => {
it("applies custom className", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveClass("custom-class");
});
it("applies custom inline styles", () => {
const onOpenChange = vi.fn();
const customStyles = { maxWidth: "600px" };
render(
);
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveStyle({ maxWidth: "600px" });
});
});
describe("Size and Position Props", () => {
it("applies data-size attribute when size prop is provided", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveAttribute("data-size", "lg");
});
it("applies data-position attribute when position prop is provided", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveAttribute("data-position", "top");
});
it("does not apply data-size when size is undefined", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
expect(dialog).not.toHaveAttribute("data-size");
});
it("defaults to data-position center when position is not specified", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveAttribute("data-position", "center");
});
it("applies both data-size and data-position together", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveAttribute("data-size", "sm");
expect(dialog).toHaveAttribute("data-position", "right");
});
it("applies data-size='full' for full screen dialog", () => {
const onOpenChange = vi.fn();
render(
);
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveAttribute("data-size", "full");
});
});
});
describe("DialogModal", () => {
describe("Uncontrolled State Management", () => {
it("renders trigger button with custom label", () => {
render(
Content
);
const triggerButton = screen.getByRole("button", { name: /open my dialog/i });
expect(triggerButton).toBeInTheDocument();
});
it("opens dialog when trigger button is clicked", async () => {
const user = userEvent.setup();
render(
Dialog content
);
const triggerButton = screen.getByRole("button", { name: /open/i });
await user.click(triggerButton);
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("Dialog content")).toBeInTheDocument();
});
});
it("closes dialog when close button is clicked", async () => {
const user = userEvent.setup();
render(
Content
);
const triggerButton = screen.getByRole("button", { name: /open/i });
await user.click(triggerButton);
const closeButton = screen.getByRole("button", { name: /close dialog/i });
await user.click(closeButton);
await waitFor(() => {
const dialog = screen.queryByRole("dialog");
// Dialog should not have 'open' attribute when closed
if (dialog) {
expect(dialog).not.toHaveAttribute("open");
}
});
});
it("calls onClose callback when dialog is closed", async () => {
const user = userEvent.setup();
const onClose = vi.fn();
render(
Content
);
const triggerButton = screen.getByRole("button", { name: /open/i });
await user.click(triggerButton);
const closeButton = screen.getByRole("button", { name: /close dialog/i });
await user.click(closeButton);
await waitFor(() => {
expect(onClose).toHaveBeenCalled();
});
});
it("calls btnOnClick before opening dialog", async () => {
const user = userEvent.setup();
const btnOnClick = vi.fn();
render(
Content
);
const triggerButton = screen.getByRole("button", { name: /open/i });
await user.click(triggerButton);
expect(btnOnClick).toHaveBeenCalled();
});
});
describe("Focus Restoration", () => {
it("restores focus to trigger button after dialog closes", async () => {
const user = userEvent.setup();
render(
Content
);
const triggerButton = screen.getByRole("button", { name: /open dialog/i });
await user.click(triggerButton);
const closeButton = screen.getByRole("button", { name: /close dialog/i });
await user.click(closeButton);
// Wait for focus restoration (has 100ms delay in DialogModal)
await waitFor(
() => {
// Focus restoration happens after dialog closes
expect(triggerButton).toHaveFocus();
},
{ timeout: 300, interval: 50 }
);
});
});
describe("Button Props", () => {
it("applies custom button size", () => {
render(
Content
);
const triggerButton = screen.getByRole("button", { name: /open/i });
expect(triggerButton).toHaveAttribute("data-btn", "lg");
});
it("forwards additional button props", () => {
render(
Content
);
const triggerButton = screen.getByTestId("custom-trigger");
expect(triggerButton).toBeInTheDocument();
expect(triggerButton).not.toBeDisabled();
});
it("adds aria-haspopup='dialog' to the regular button trigger", () => {
render(
Content
);
const triggerButton = screen.getByRole("button", { name: /open/i });
expect(triggerButton).toHaveAttribute("aria-haspopup", "dialog");
});
});
describe("Icon Button Trigger", () => {
const TestIcon = () => (
);
it("renders IconButton when icon prop is provided", () => {
render(
}>
Content
);
const iconButton = screen.getByRole("button", { name: /settings/i });
expect(iconButton).toHaveAttribute("data-icon-btn");
});
it("renders regular Button when icon prop is not provided", () => {
render(
Content
);
const button = screen.getByRole("button", { name: /open/i });
expect(button).not.toHaveAttribute("data-icon-btn");
});
it("uses btnLabel as aria-label on the icon button", () => {
render(
}>
Content
);
const iconButton = screen.getByRole("button", { name: /settings/i });
expect(iconButton).toHaveAttribute("aria-label", "Settings");
});
it("passes btnLabel as visible label on the icon button", () => {
render(
}>
Content
);
// IconButton renders the label in a span with data-icon-label
const iconButton = screen.getByRole("button", { name: /settings/i });
expect(iconButton.querySelector("[data-icon-label]")).toHaveTextContent("Settings");
});
it("adds aria-haspopup='dialog' to the icon button trigger", () => {
render(
}>
Content
);
const iconButton = screen.getByRole("button", { name: /settings/i });
expect(iconButton).toHaveAttribute("aria-haspopup", "dialog");
});
it("opens dialog when icon button is clicked", async () => {
const user = userEvent.setup();
render(
}>
Dialog content
);
const iconButton = screen.getByRole("button", { name: /settings/i });
await user.click(iconButton);
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("Dialog content")).toBeInTheDocument();
});
});
it("applies btnSize to the icon button", () => {
render(
} btnSize="lg">
Content
);
const iconButton = screen.getByRole("button", { name: /settings/i });
expect(iconButton).toHaveAttribute("data-btn", "lg");
});
it("forwards btnProps to the icon button", () => {
render(
}
btnProps={{ "data-testid": "icon-trigger" }}
>
Content
);
expect(screen.getByTestId("icon-trigger")).toBeInTheDocument();
});
});
describe("Size and Position Props", () => {
it("forwards size and position to the Dialog element", async () => {
const user = userEvent.setup();
render(
Content
);
const triggerButton = screen.getByRole("button", { name: /open/i });
await user.click(triggerButton);
await waitFor(() => {
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveAttribute("data-size", "sm");
expect(dialog).toHaveAttribute("data-position", "right");
});
});
});
});