259 lines
10 KiB
TypeScript
259 lines
10 KiB
TypeScript
import { TranslationProvider } from "@suaveplan/react-i18n";
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { renderComponent } from "@suaveplan/testing-dom";
|
|
import { createRef } from "react";
|
|
import { createTestI18n } from "../../../tests/setup/test-i18n.js";
|
|
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
|
|
import { lightTheme } from "../../theme/themes/light.js";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "./alert-dialog.js";
|
|
import {
|
|
alertDialogActionVariants,
|
|
alertDialogCancelVariants,
|
|
alertDialogContentVariants,
|
|
alertDialogDescriptionVariants,
|
|
alertDialogFooterVariants,
|
|
alertDialogHeaderVariants,
|
|
alertDialogOverlayVariants,
|
|
alertDialogTitleVariants,
|
|
} from "./alert-dialog.variants.js";
|
|
|
|
async function withProviders(
|
|
node: React.ReactElement
|
|
): Promise<React.ReactElement> {
|
|
const i18n = await createTestI18n({ locale: "en-GB" });
|
|
return (
|
|
<TranslationProvider i18n={i18n}>
|
|
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
|
|
{node}
|
|
</ThemeProvider>
|
|
</TranslationProvider>
|
|
);
|
|
}
|
|
|
|
describe("<AlertDialog>", () => {
|
|
it("opens via the trigger and renders alertdialog role", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const tree = await withProviders(
|
|
<AlertDialog>
|
|
<AlertDialogTrigger>Delete</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogTitle>Confirm</AlertDialogTitle>
|
|
<AlertDialogAction>OK</AlertDialogAction>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
const { getByText, findByRole, user } = renderComponent(tree);
|
|
await user.click(getByText("Delete"));
|
|
const alertDialog = await findByRole("alertdialog");
|
|
expect(alertDialog).not.toBeNull();
|
|
});
|
|
|
|
it("Escape closes the dialog (cancel)", { timeout: 5000 }, async () => {
|
|
const tree = await withProviders(
|
|
<AlertDialog>
|
|
<AlertDialogTrigger>Delete</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogTitle>Confirm</AlertDialogTitle>
|
|
<AlertDialogAction>OK</AlertDialogAction>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
const { getByText, findByRole, queryByRole, user } =
|
|
renderComponent(tree);
|
|
const trigger = getByText("Delete") as HTMLElement;
|
|
await user.click(trigger);
|
|
await findByRole("alertdialog");
|
|
await user.keyboard("{Escape}");
|
|
expect(queryByRole("alertdialog")).toBeNull();
|
|
expect(document.activeElement).toBe(trigger);
|
|
});
|
|
|
|
it("AlertDialogAction closes the dialog when activated", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const tree = await withProviders(
|
|
<AlertDialog defaultOpen>
|
|
<AlertDialogContent>
|
|
<AlertDialogTitle>Heading</AlertDialogTitle>
|
|
<AlertDialogDescription>Body</AlertDialogDescription>
|
|
<AlertDialogAction>Confirm</AlertDialogAction>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
const { findByRole, queryByRole, user } = renderComponent(tree);
|
|
const action = await findByRole("button", { name: "Confirm" });
|
|
await user.click(action);
|
|
expect(queryByRole("alertdialog")).toBeNull();
|
|
});
|
|
|
|
it("AlertDialogCancel closes the dialog when activated", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const tree = await withProviders(
|
|
<AlertDialog defaultOpen>
|
|
<AlertDialogContent>
|
|
<AlertDialogTitle>Confirm</AlertDialogTitle>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
const { findByText, queryByRole, user } = renderComponent(tree);
|
|
await user.click(await findByText("Cancel"));
|
|
expect(queryByRole("alertdialog")).toBeNull();
|
|
});
|
|
|
|
it("does NOT close on overlay backdrop click (modal confirmation)", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const tree = await withProviders(
|
|
<AlertDialog defaultOpen>
|
|
<AlertDialogContent>
|
|
<AlertDialogTitle>Modal</AlertDialogTitle>
|
|
<AlertDialogAction>OK</AlertDialogAction>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
const { findByRole, queryByRole, user } = renderComponent(tree);
|
|
const dialog = await findByRole("alertdialog");
|
|
// Find the overlay (sibling of the content) and click it.
|
|
const overlay = dialog.parentElement?.querySelector(
|
|
"[data-state]:not([role])"
|
|
) as HTMLElement | null;
|
|
if (overlay !== null) {
|
|
await user.click(overlay);
|
|
}
|
|
expect(queryByRole("alertdialog")).not.toBeNull();
|
|
});
|
|
|
|
it("forwards refs on every sub-component", { timeout: 5000 }, async () => {
|
|
const triggerRef = createRef<HTMLButtonElement>();
|
|
const contentRef = createRef<HTMLDivElement>();
|
|
const titleRef = createRef<HTMLHeadingElement>();
|
|
const descRef = createRef<HTMLParagraphElement>();
|
|
const headerRef = createRef<HTMLDivElement>();
|
|
const footerRef = createRef<HTMLDivElement>();
|
|
const actionRef = createRef<HTMLButtonElement>();
|
|
const cancelRef = createRef<HTMLButtonElement>();
|
|
const tree = await withProviders(
|
|
<AlertDialog defaultOpen>
|
|
<AlertDialogTrigger ref={triggerRef}>Open</AlertDialogTrigger>
|
|
<AlertDialogContent ref={contentRef}>
|
|
<AlertDialogHeader ref={headerRef}>
|
|
<AlertDialogTitle ref={titleRef}>T</AlertDialogTitle>
|
|
<AlertDialogDescription ref={descRef}>
|
|
D
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter ref={footerRef}>
|
|
<AlertDialogCancel ref={cancelRef}>C</AlertDialogCancel>
|
|
<AlertDialogAction ref={actionRef}>A</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
renderComponent(tree);
|
|
expect(triggerRef.current).not.toBeNull();
|
|
expect(contentRef.current).not.toBeNull();
|
|
expect(titleRef.current).not.toBeNull();
|
|
expect(descRef.current).not.toBeNull();
|
|
expect(headerRef.current).not.toBeNull();
|
|
expect(footerRef.current).not.toBeNull();
|
|
expect(actionRef.current).not.toBeNull();
|
|
expect(cancelRef.current).not.toBeNull();
|
|
});
|
|
|
|
it("merges custom className", { timeout: 5000 }, async () => {
|
|
const tree = await withProviders(
|
|
<AlertDialog defaultOpen>
|
|
<AlertDialogContent className="c-extra">
|
|
<AlertDialogHeader className="h-extra">
|
|
<AlertDialogTitle className="t-extra">
|
|
T
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription className="d-extra">
|
|
D
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter className="f-extra">
|
|
<AlertDialogAction className="a-extra">
|
|
OK
|
|
</AlertDialogAction>
|
|
<AlertDialogCancel className="x-extra">
|
|
Cancel
|
|
</AlertDialogCancel>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
const { findByRole } = renderComponent(tree);
|
|
const dialog = await findByRole("alertdialog");
|
|
expect(dialog.className).toContain("c-extra");
|
|
expect(dialog.querySelector(".h-extra")).not.toBeNull();
|
|
expect(dialog.querySelector(".t-extra")).not.toBeNull();
|
|
expect(dialog.querySelector(".d-extra")).not.toBeNull();
|
|
expect(dialog.querySelector(".f-extra")).not.toBeNull();
|
|
expect(dialog.querySelector(".a-extra")).not.toBeNull();
|
|
expect(dialog.querySelector(".x-extra")).not.toBeNull();
|
|
});
|
|
|
|
it("applies the size variant to AlertDialogContent", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const tree = await withProviders(
|
|
<AlertDialog defaultOpen>
|
|
<AlertDialogContent size="xl">
|
|
<AlertDialogTitle>T</AlertDialogTitle>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
const { findByRole } = renderComponent(tree);
|
|
const dialog = await findByRole("alertdialog");
|
|
expect(dialog.className).toContain("max-w-4xl");
|
|
});
|
|
|
|
it("renders Cancel + Action buttons with their canonical styling", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const tree = await withProviders(
|
|
<AlertDialog defaultOpen>
|
|
<AlertDialogContent>
|
|
<AlertDialogTitle>T</AlertDialogTitle>
|
|
<AlertDialogAction>OK</AlertDialogAction>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
const { findByText } = renderComponent(tree);
|
|
const action = await findByText("OK");
|
|
const cancel = await findByText("Cancel");
|
|
expect(action.className).toContain("bg-primary");
|
|
expect(cancel.className).toContain("border");
|
|
});
|
|
|
|
it("exposes every alert-dialog cva resolver", { timeout: 5000 }, () => {
|
|
expect(alertDialogOverlayVariants().length).toBeGreaterThan(0);
|
|
for (const size of ["sm", "default", "lg", "xl"] as const) {
|
|
expect(alertDialogContentVariants({ size }).length).toBeGreaterThan(
|
|
0
|
|
);
|
|
}
|
|
expect(alertDialogHeaderVariants().length).toBeGreaterThan(0);
|
|
expect(alertDialogFooterVariants().length).toBeGreaterThan(0);
|
|
expect(alertDialogTitleVariants().length).toBeGreaterThan(0);
|
|
expect(alertDialogDescriptionVariants().length).toBeGreaterThan(0);
|
|
expect(alertDialogActionVariants().length).toBeGreaterThan(0);
|
|
expect(alertDialogCancelVariants().length).toBeGreaterThan(0);
|
|
});
|
|
});
|