66 lines
2.3 KiB
TypeScript
66 lines
2.3 KiB
TypeScript
|
|
import * as React from "react";
|
||
|
|
import axe from "axe-core";
|
||
|
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||
|
|
import { describe, expect, it, vi } from "vitest";
|
||
|
|
import {
|
||
|
|
AppShell,
|
||
|
|
Button,
|
||
|
|
FormField,
|
||
|
|
Input,
|
||
|
|
ThemeProvider,
|
||
|
|
useTheme,
|
||
|
|
} from "../src";
|
||
|
|
|
||
|
|
function ThemeControl() {
|
||
|
|
const { resolvedTheme, toggleTheme } = useTheme();
|
||
|
|
return <Button onClick={toggleTheme}>Current: {resolvedTheme}</Button>;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe("core UI contracts", () => {
|
||
|
|
it("persists and applies theme changes", () => {
|
||
|
|
render(<ThemeProvider defaultTheme="dark"><ThemeControl /></ThemeProvider>);
|
||
|
|
fireEvent.click(screen.getByRole("button", { name: "Current: dark" }));
|
||
|
|
expect(document.documentElement).toHaveClass("light");
|
||
|
|
expect(window.localStorage.getItem("jason-ui-theme")).toBe("light");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("associates form help and errors with the control", () => {
|
||
|
|
render(
|
||
|
|
<FormField label="Workspace name" description="Used in navigation" error="Name is required">
|
||
|
|
<Input />
|
||
|
|
</FormField>
|
||
|
|
);
|
||
|
|
const input = screen.getByLabelText("Workspace name");
|
||
|
|
expect(input).toHaveAttribute("aria-invalid", "true");
|
||
|
|
expect(input.getAttribute("aria-describedby")).toContain("-error");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("provides mobile navigation and current-page semantics", () => {
|
||
|
|
const onClick = vi.fn();
|
||
|
|
render(
|
||
|
|
<AppShell
|
||
|
|
brand="Acme"
|
||
|
|
nav={[{ label: "Dashboard", active: true, onClick }]}
|
||
|
|
>
|
||
|
|
Content
|
||
|
|
</AppShell>
|
||
|
|
);
|
||
|
|
fireEvent.click(screen.getByRole("button", { name: "Open navigation" }));
|
||
|
|
const current = screen.getAllByRole("button", { name: "Dashboard" }).at(-1)!;
|
||
|
|
expect(current).toHaveAttribute("aria-current", "page");
|
||
|
|
fireEvent.click(current);
|
||
|
|
expect(onClick).toHaveBeenCalledOnce();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("has no serious accessibility violations in representative primitives", async () => {
|
||
|
|
const { container } = render(
|
||
|
|
<main>
|
||
|
|
<FormField label="Email" description="We never share it."><Input type="email" /></FormField>
|
||
|
|
<Button>Save</Button>
|
||
|
|
</main>
|
||
|
|
);
|
||
|
|
const results = await axe.run(container, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa"] } });
|
||
|
|
expect(results.violations.filter((violation) => violation.impact === "serious" || violation.impact === "critical")).toEqual([]);
|
||
|
|
});
|
||
|
|
});
|