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( Dialog content ); 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( Dialog content ); 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( Dialog content ); 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( Dialog content ); 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( Content ); 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( Alert content ); 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( Content ); 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( Dialog description ); 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( Content ); const dialog = screen.getByRole("dialog"); expect(dialog).toHaveAttribute("aria-label", "Custom accessible label"); }); it("close button has accessible label", () => { const onOpenChange = vi.fn(); render( Content ); 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( Content ); 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( Content ); 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( Content ); 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( Are you sure? ); 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( Content ); const dialog = screen.getByRole("dialog"); expect(dialog).toHaveClass("custom-class"); }); it("applies custom inline styles", () => { const onOpenChange = vi.fn(); const customStyles = { maxWidth: "600px" }; render( Content ); 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( Content ); 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( Content ); 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( Content ); 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( Content ); 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( Content ); 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( Content ); 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"); }); }); }); });