/** * LD — design-system component tests. Pure UI, rendered with React Testing * Library; no backend, no gateways. We assert behaviour and accessibility * (roles, names, aria-*) rather than exact Tailwind classes, so the visual * design can evolve without breaking the suite. */ import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { Button } from "./Button"; import { Input } from "./Input"; import { Field } from "./Field"; import { SmallDropdown } from "./SmallDropdown"; import { Tabs } from "./Tabs"; describe("Button", () => { it("renders its label and fires onClick", () => { const onClick = vi.fn(); render(); fireEvent.click(screen.getByRole("button", { name: "Save" })); expect(onClick).toHaveBeenCalledOnce(); }); it("is disabled and shows a spinner while loading", () => { const onClick = vi.fn(); render( , ); const btn = screen.getByRole("button", { name: /Save/ }) as HTMLButtonElement; expect(btn.disabled).toBe(true); expect(screen.getByRole("status")).toBeTruthy(); // spinner fireEvent.click(btn); expect(onClick).not.toHaveBeenCalled(); }); it("defaults to type=button to avoid accidental form submits", () => { render(); expect(screen.getByRole("button", { name: "Go" }).getAttribute("type")).toBe( "button", ); }); }); describe("Input", () => { it("flags aria-invalid when invalid", () => { render(); expect(screen.getByLabelText("email").getAttribute("aria-invalid")).toBe("true"); }); it("is not aria-invalid by default", () => { render(); expect(screen.getByLabelText("email").getAttribute("aria-invalid")).toBeNull(); }); }); describe("Field", () => { it("associates the label with the control via id", () => { render( {({ id, describedBy }) => ( )} , ); // getByLabelText resolves the label→control association. expect(screen.getByLabelText("Project name")).toBeTruthy(); }); it("announces an error and wires aria-describedby", () => { render( {({ id, describedBy }) => ( )} , ); const alert = screen.getByRole("alert"); expect(alert.textContent).toBe("must be absolute"); const input = screen.getByLabelText("Root"); expect(input.getAttribute("aria-describedby")).toBe(alert.id); }); }); describe("Tabs", () => { const items = [ { id: "a", label: "alpha" }, { id: "b", label: "beta" }, ]; it("marks the active tab with aria-selected and reports selection", () => { const onSelect = vi.fn(); render(); const tabs = screen.getAllByRole("tab"); expect(tabs).toHaveLength(2); const [alpha, beta] = tabs; expect(alpha.getAttribute("aria-selected")).toBe("true"); expect(beta.getAttribute("aria-selected")).toBe("false"); fireEvent.click(beta); expect(onSelect).toHaveBeenCalledWith("b"); }); it("renders a close control per tab only when onClose is given", () => { const onClose = vi.fn(); const { rerender } = render( {}} />, ); expect(screen.queryByRole("button", { name: "fermer alpha" })).toBeNull(); rerender( {}} onClose={onClose} />); fireEvent.click(screen.getByRole("button", { name: "fermer beta" })); expect(onClose).toHaveBeenCalledWith("b"); }); }); describe("SmallDropdown", () => { const options = [ { value: "", label: "None" }, { value: "a", label: "Alpha" }, { value: "b", label: "Beta", disabled: true }, ]; it("opens a listbox and reports the selected value", () => { const onChange = vi.fn(); render( , ); fireEvent.click(screen.getByLabelText("test dropdown")); fireEvent.click(screen.getByRole("option", { name: "Alpha" })); expect(onChange).toHaveBeenCalledWith("a"); expect(screen.queryByRole("listbox")).toBeNull(); }); it("supports keyboard selection and ignores disabled options", () => { const onChange = vi.fn(); render( , ); const trigger = screen.getByLabelText("test dropdown"); fireEvent.keyDown(trigger, { key: "ArrowDown" }); fireEvent.keyDown(document, { key: "ArrowDown" }); fireEvent.keyDown(document, { key: "Enter" }); expect(onChange).toHaveBeenCalledWith("a"); }); });