/**
* 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");
});
});