import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
Badge,
Button,
Checkbox,
Dialog,
EkklesiaUiStyles,
EmptyState,
ErrorState,
Field,
IconButton,
List,
ListItem,
LoadingState,
Panel,
Select,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Tabs,
TextInput,
Toast,
ToastStack,
} from "../src/index.js";
declare global {
// eslint-disable-next-line no-var
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
}
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
function render(node: React.ReactNode): void {
act(() => root.render(node));
}
describe("Ekklesia elements", () => {
it("renders a button that does not submit a form by accident", () => {
render();
const button = container.querySelector("button");
expect(button?.getAttribute("type")).toBe("button");
expect(button?.className).toContain("ek-button--primary");
expect(button?.className).toContain("ek-focusable");
});
it("requires an accessible name on an icon-only button", () => {
render(×);
expect(container.querySelector("button")?.getAttribute("aria-label")).toBe("Remove slide");
});
it("associates a field's label, hint, and error with its control", () => {
render(
{(control) => }
,
);
const label = container.querySelector("label");
const input = container.querySelector("input");
const error = container.querySelector(".ek-field__error");
expect(input?.id).toBeTruthy();
expect(label?.getAttribute("for")).toBe(input?.id);
expect(input?.getAttribute("aria-invalid")).toBe("true");
expect(input?.getAttribute("aria-describedby")?.split(" ")).toContain(error?.id);
expect(error?.getAttribute("role")).toBe("alert");
});
it("renders a select and a labelled checkbox as real form controls", () => {
render(
<>
>,
);
expect(container.querySelector("select")?.className).toContain("ek-control");
expect(container.querySelector('input[type="checkbox"]')).not.toBeNull();
expect(container.querySelector(".ek-checkbox")?.textContent).toContain("Enabled");
});
it("renders a panel with an accessible heading and its actions", () => {
render(
New} description="What this panel is." title="Slides">
Body
,
);
expect(container.querySelector("h2")?.textContent).toBe("Slides");
expect(container.querySelector(".ek-panel__description")?.textContent).toBe(
"What this panel is.",
);
expect(container.querySelector(".ek-panel__header button")?.textContent).toBe("New");
});
it("renders a dialog in place, labels it, and dismisses it on Escape", () => {
const onDismiss = vi.fn();
render(
,
);
const dialog = container.querySelector('[role="dialog"]');
expect(dialog?.getAttribute("aria-modal")).toBe("true");
expect(
container.querySelector(`#${dialog?.getAttribute("aria-labelledby")}`)?.textContent,
).toBe("Delete slide");
// Rendered inside the caller's own subtree, never portalled to document.body: an artifact's
// dialog must not escape its isolated root.
expect(document.body.querySelector(":scope > [role='dialog']")).toBeNull();
act(() => {
document.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Escape" }));
});
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it("renders nothing for a closed dialog", () => {
render(
,
);
expect(container.querySelector('[role="dialog"]')).toBeNull();
});
it("exposes tabs with ARIA roles and arrow-key movement", () => {
const onSelect = vi.fn();
const tabs = [
{ content:
One
, id: "one", label: "One" },
{ content: Two
, id: "two", label: "Two" },
];
render();
const tabButtons = [...container.querySelectorAll('[role="tab"]')];
expect(tabButtons.map((tab) => tab.getAttribute("aria-selected"))).toEqual(["true", "false"]);
expect(tabButtons.map((tab) => tab.getAttribute("tabindex"))).toEqual(["0", "-1"]);
expect(container.querySelector('[role="tabpanel"]')?.textContent).toBe("One");
act(() => {
tabButtons[0]?.dispatchEvent(
new KeyboardEvent("keydown", { bubbles: true, key: "ArrowRight" }),
);
});
expect(onSelect).toHaveBeenCalledWith("two");
});
it("renders table and list primitives with header semantics", () => {
render(
<>
First
>,
);
expect(container.querySelector("table")?.getAttribute("aria-label")).toBe("Slides");
expect(container.querySelector("th")?.getAttribute("scope")).toBe("col");
expect(container.querySelector("tbody tr")?.getAttribute("aria-selected")).toBe("true");
expect(container.querySelector(".ek-list__item")?.textContent).toBe("First");
});
it("never signals status with colour alone", () => {
render(
<>
Running
Everything went through.
>,
);
expect(container.querySelector(".ek-badge__symbol")?.textContent).toBe("✓");
expect(container.querySelector(".ek-badge__symbol")?.getAttribute("aria-hidden")).toBe("true");
const toasts = [...container.querySelectorAll(".ek-toast")];
expect(toasts[0]?.textContent).toContain("✓");
expect(toasts[0]?.getAttribute("aria-live")).toBe("polite");
expect(toasts[1]?.textContent).toContain("✕");
expect(toasts[1]?.getAttribute("aria-live")).toBe("assertive");
});
it("renders empty, error, and loading states with the right announcements", () => {
render(
<>
>,
);
expect(container.querySelector(".ek-message")?.textContent).toContain("No slides yet");
expect(container.querySelector('[role="alert"]')?.textContent).toContain("This could not load");
const loading = container.querySelector('[role="status"]');
expect(loading?.getAttribute("aria-live")).toBe("polite");
expect(loading?.textContent).toContain("Loading slides…");
expect(container.querySelector(".ek-spinner")?.getAttribute("aria-hidden")).toBe("true");
});
it("puts its stylesheet in whichever root it is rendered into", () => {
const host = document.createElement("div");
container.appendChild(host);
const shadow = host.attachShadow({ mode: "open" });
const shadowRoot = createRoot(shadow);
act(() =>
shadowRoot.render(
<>
>,
),
);
expect(shadow.querySelector("style[data-ekklesia-ui]")?.textContent).toContain(".ek-button");
// The host document never receives the artifact's copy.
expect(document.head.querySelector("style[data-ekklesia-ui]")).toBeNull();
act(() => shadowRoot.unmount());
});
});