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( Are you sure? , ); 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( undefined} open={false} title="Hidden"> Body , ); 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( <> Name Welcome
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()); }); });