test(modal): add unit tests (#2145)

This commit is contained in:
Eric Liu 2025-04-12 12:55:39 -07:00 committed by GitHub
commit d4ca8b5c97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 341 additions and 93 deletions

View file

@ -1,25 +0,0 @@
<script lang="ts">
import { Button, Modal } from "carbon-components-svelte";
let open = false;
</script>
<Button on:click={() => (open = true)}>Create database</Button>
<Modal
bind:open
modalHeading="Create database"
primaryButtonText="Confirm"
secondaryButtons={[{ text: "Cancel" }, { text: "Duplicate" }]}
secondaryButtonText="Cancel"
on:click:button--secondary={({ detail }) => {
console.log(detail);
open = false;
}}
on:open
on:close
on:submit
on:click:button--primary
>
<p>Create a new Cloudant database in the US South region.</p>
</Modal>

View file

@ -0,0 +1,57 @@
<script lang="ts">
import { Modal } from "carbon-components-svelte";
import type { ComponentProps } from "svelte";
export let open = false;
export let modalHeading = "";
export let modalLabel = "";
export let modalAriaLabel = "";
export let iconDescription = "Close the modal";
export let hasForm = false;
export let hasScrollingContent = false;
export let primaryButtonText = "";
export let primaryButtonDisabled = false;
export let primaryButtonIcon = undefined;
export let shouldSubmitOnEnter = true;
export let secondaryButtonText = "";
export let secondaryButtons: ComponentProps<Modal>["secondaryButtons"] =
undefined;
export let selectorPrimaryFocus = "[data-modal-primary-focus]";
export let preventCloseOnClickOutside = false;
export let size: ComponentProps<Modal>["size"] = undefined;
export let danger = false;
export let alert = false;
export let passiveModal = false;
</script>
<Modal
{open}
{modalHeading}
{modalLabel}
{modalAriaLabel}
{iconDescription}
{hasForm}
{hasScrollingContent}
{primaryButtonText}
{primaryButtonDisabled}
{primaryButtonIcon}
{shouldSubmitOnEnter}
{secondaryButtonText}
{secondaryButtons}
{selectorPrimaryFocus}
{preventCloseOnClickOutside}
{size}
{danger}
{alert}
{passiveModal}
on:open={() => console.log("open")}
on:close={() => console.log("close")}
on:submit={() => console.log("submit")}
on:click:button--primary={() => console.log("click:button--primary")}
on:click:button--secondary={(e) =>
console.log("click:button--secondary", e.detail)}
on:transitionend={(e) => console.log("transitionend", e.detail)}
>
<slot />
<input id="test-focus" data-testid="test-focus" />
</Modal>

284
tests/Modal/Modal.test.ts Normal file
View file

@ -0,0 +1,284 @@
import { render, screen } from "@testing-library/svelte";
import { tick } from "svelte";
import { user } from "../setup-tests";
import ModalTest from "./Modal.test.svelte";
describe("Modal", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders with default props", async () => {
const { container } = render(ModalTest, {
props: {
open: true,
modalHeading: "Test Modal",
primaryButtonText: "Save",
secondaryButtonText: "Cancel",
},
});
// Check if modal container is rendered
const modalContainer = container.querySelector(".bx--modal-container");
expect(modalContainer).toBeInTheDocument();
// Check if modal heading is rendered
expect(screen.getByText("Test Modal")).toBeInTheDocument();
// Check if buttons are rendered
expect(screen.getByText("Save")).toBeInTheDocument();
expect(screen.getByText("Cancel")).toBeInTheDocument();
// Check if close button is rendered
const closeButton = screen.getByLabelText("Close the modal");
expect(closeButton).toBeInTheDocument();
// Check if modal has correct ARIA attributes
expect(modalContainer).toHaveAttribute("role", "dialog");
expect(modalContainer).toHaveAttribute("aria-modal", "true");
expect(modalContainer).toHaveAttribute("aria-label", "Test Modal");
});
it("renders with basic structure", () => {
render(ModalTest, {
props: {
open: true,
modalHeading: "Test Modal",
primaryButtonText: "Save",
secondaryButtonText: "Cancel",
},
});
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("Test Modal")).toBeInTheDocument();
expect(screen.getByText("Save")).toBeInTheDocument();
expect(screen.getByText("Cancel")).toBeInTheDocument();
expect(screen.getByLabelText("Close the modal")).toBeInTheDocument();
expect(screen.getByRole("dialog")).toHaveAttribute("aria-modal", "true");
});
it("opens and closes properly", async () => {
const consoleLog = vi.spyOn(console, "log");
const { component } = render(ModalTest, {
props: {
open: false,
modalHeading: "Test Modal",
},
});
// Open the modal
component.$set({ open: true });
await tick();
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(consoleLog).toHaveBeenCalledWith("open");
// Close the modal
component.$set({ open: false });
await tick();
expect(consoleLog).toHaveBeenCalledWith("close");
});
it("handles form submission", async () => {
const consoleLog = vi.spyOn(console, "log");
render(ModalTest, {
props: {
open: true,
hasForm: true,
modalHeading: "Form Modal",
primaryButtonText: "Save",
},
});
const primaryButton = screen.getByRole("button", { name: "Save" });
await user.click(primaryButton);
expect(consoleLog).toHaveBeenCalledWith("submit");
expect(consoleLog).toHaveBeenCalledWith("click:button--primary");
});
it("handles button clicks", async () => {
const consoleLog = vi.spyOn(console, "log");
render(ModalTest, {
props: {
open: true,
primaryButtonText: "Save",
secondaryButtonText: "Cancel",
},
});
await user.click(screen.getByText("Save"));
expect(consoleLog).toHaveBeenCalledWith("click:button--primary");
await user.click(screen.getByText("Cancel"));
expect(consoleLog).toHaveBeenCalledWith("click:button--secondary", {
text: "Cancel",
});
});
it("supports different modal sizes", () => {
type Size = "xs" | "sm" | "lg";
const sizeMappings = {
xs: "bx--modal-container--xs",
sm: "bx--modal-container--sm",
lg: "bx--modal-container--lg",
} as const;
// Test specific sizes
(Object.keys(sizeMappings) as Size[]).forEach((size) => {
const { unmount } = render(ModalTest, {
props: {
open: true,
size,
modalHeading: `${size} Modal`,
},
});
const modal = screen.getByRole("dialog");
expect(modal).toHaveClass(sizeMappings[size]);
unmount();
});
// Test default (medium) size
const { unmount } = render(ModalTest, {
props: {
open: true,
modalHeading: "Medium Modal",
},
});
const modal = screen.getByRole("dialog");
expect(modal).toHaveClass("bx--modal-container");
expect(modal).not.toHaveClass("bx--modal-container--xs");
expect(modal).not.toHaveClass("bx--modal-container--sm");
expect(modal).not.toHaveClass("bx--modal-container--lg");
unmount();
});
it("supports danger and alert variants", () => {
render(ModalTest, {
props: {
open: true,
danger: true,
alert: true,
modalHeading: "Danger Alert Modal",
primaryButtonText: "Delete",
},
});
const primaryButton = screen.getByRole("button", { name: "Delete" });
expect(primaryButton).toHaveClass("bx--btn--danger");
const modal = screen.getByRole("alertdialog");
expect(modal).toHaveAttribute("aria-label", "Danger Alert Modal");
});
it("handles scrolling content", () => {
render(ModalTest, {
props: {
open: true,
hasScrollingContent: true,
modalHeading: "Scrolling Modal",
},
});
const modalBody = screen.getByRole("region");
expect(modalBody).toHaveClass("bx--modal-scroll-content");
});
it("should focus close button when open", async () => {
render(ModalTest, {
props: {
open: true,
},
});
const closeButton = screen.getByLabelText("Close the modal");
expect(closeButton).toHaveFocus();
});
it("respects the selectorPrimaryFocus prop", async () => {
render(ModalTest, {
props: {
open: true,
modalHeading: "Focus Test",
selectorPrimaryFocus: "#test-focus",
},
});
expect(screen.getByTestId("test-focus")).toHaveFocus();
});
it("prevents closing when clicking outside if configured", async () => {
const { component } = render(ModalTest, {
props: {
open: true,
preventCloseOnClickOutside: true,
modalHeading: "Prevent Close Test",
},
});
const closeHandler = vi.fn();
component.$on("close", closeHandler);
// Click outside the modal
await user.click(document.body);
expect(closeHandler).not.toHaveBeenCalled();
});
it("supports passive modal variant", () => {
render(ModalTest, {
props: {
open: true,
passiveModal: true,
modalHeading: "Passive Modal",
primaryButtonText: "Save",
secondaryButtonText: "Cancel",
},
});
// Verify close button is in header
const closeButton = screen.getByLabelText("Close the modal");
expect(closeButton.closest(".bx--modal-header")).toBeInTheDocument();
// Verify no footer is present
expect(
screen.queryByRole("button", { name: "Save" }),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Cancel" }),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Close the modal" }),
).toBeInTheDocument();
});
it("handles closing through various methods", async () => {
const consoleLog = vi.spyOn(console, "log");
const { component } = render(ModalTest, {
props: {
open: true,
modalHeading: "Close Test Modal",
},
});
// Close via escape key
await user.keyboard("{Escape}");
expect(consoleLog).toHaveBeenCalledWith("close");
component.$set({ open: true });
await tick();
expect(consoleLog).toHaveBeenCalledWith("open");
// Close via clicking outside
await user.click(document.body);
expect(consoleLog).toHaveBeenCalledWith("close");
component.$set({ open: true });
await tick();
// Close via close button
const closeButton = screen.getByLabelText("Close the modal");
await user.click(closeButton);
expect(consoleLog).toHaveBeenCalledWith("close");
});
});

View file

@ -1,17 +0,0 @@
<script lang="ts">
import { Modal } from "carbon-components-svelte";
</script>
<Modal
size="xs"
open
modalHeading="Create database"
primaryButtonText="Confirm"
secondaryButtonText="Cancel"
on:click:button--secondary
on:open
on:close
on:submit
>
<p>Create a new Cloudant database in the US South region.</p>
</Modal>

View file

@ -1,17 +0,0 @@
<script lang="ts">
import { Modal } from "carbon-components-svelte";
</script>
<Modal
size="lg"
open
modalHeading="Create database"
primaryButtonText="Confirm"
secondaryButtonText="Cancel"
on:click:button--secondary
on:open
on:close
on:submit
>
<p>Create a new Cloudant database in the US South region.</p>
</Modal>

View file

@ -1,17 +0,0 @@
<script lang="ts">
import { Modal } from "carbon-components-svelte";
</script>
<Modal
preventCloseOnClickOutside
open
modalHeading="Create database"
primaryButtonText="Confirm"
secondaryButtonText="Cancel"
on:click:button--secondary
on:open
on:close
on:submit
>
<p>Create a new Cloudant database in the US South region.</p>
</Modal>

View file

@ -1,17 +0,0 @@
<script lang="ts">
import { Modal } from "carbon-components-svelte";
</script>
<Modal
size="sm"
open
modalHeading="Create database"
primaryButtonText="Confirm"
secondaryButtonText="Cancel"
on:click:button--secondary
on:open
on:close
on:submit
>
<p>Create a new Cloudant database in the US South region.</p>
</Modal>