sdlc-hub/web/src/ui/ui.test.tsx
Andrey Epifancev 13e7ae35bc feat(web): иконки и базовые компоненты дизайн-системы (#65)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:04:23 +04:00

88 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router";
import { afterEach, describe, expect, it, vi } from "vitest";
import { Button, Chip, IconButton, IconRefresh, Notice, Segmented, ThemeSwitch } from "./index.ts";
function stubMatchMedia(dark: boolean) {
vi.stubGlobal("matchMedia", (q: string) => ({ matches: dark && q.includes("dark"), media: q, addEventListener() {}, removeEventListener() {} }));
}
describe("Button", () => {
it("loading: aria-busy, недоступна, иконка не показана", () => {
render(<Button loading icon={<IconRefresh />}>Обновить</Button>);
const b = screen.getByRole("button", { name: /Обновить/ });
expect(b.getAttribute("aria-busy")).toBe("true");
expect((b as HTMLButtonElement).disabled).toBe(true);
expect(b.querySelector("svg")).toBeNull();
expect(b.querySelector(".ui-spinner")).not.toBeNull();
});
it("по умолчанию type=button, aria-pressed пробрасывается", () => {
render(<Button aria-pressed={true}>Фильтр</Button>);
const b = screen.getByRole("button", { name: "Фильтр" });
expect(b.getAttribute("type")).toBe("button");
expect(b.getAttribute("aria-pressed")).toBe("true");
expect(b.className).toContain("ui-btn--secondary");
});
});
describe("IconButton", () => {
it("доступное имя из label", () => {
render(<IconButton label="Закрыть" icon={<IconRefresh />} />);
expect(screen.getByRole("button", { name: "Закрыть" }).getAttribute("title")).toBe("Закрыть");
});
});
describe("Segmented", () => {
const options = [{ value: "a", label: "А" }, { value: "b", label: "Б" }];
it("кнопками: radiogroup, onChange, aria-checked", () => {
const onChange = vi.fn();
render(<Segmented label="Режим" value="a" options={options} onChange={onChange} />);
expect(screen.getByRole("radiogroup", { name: "Режим" })).toBeTruthy();
expect(screen.getByRole("radio", { name: "А" }).getAttribute("aria-checked")).toBe("true");
expect(screen.getByRole("radio", { name: "Б" }).getAttribute("aria-checked")).toBe("false");
fireEvent.click(screen.getByRole("radio", { name: "Б" }));
expect(onChange).toHaveBeenCalledWith("b");
});
it("ссылками: nav, aria-current у активной", () => {
render(
<MemoryRouter initialEntries={["/b"]}>
<Segmented label="Разделы" value="b" options={[{ value: "a", label: "А", href: "/a" }, { value: "b", label: "Б", href: "/b" }]} />
</MemoryRouter>,
);
expect(screen.getByRole("navigation", { name: "Разделы" })).toBeTruthy();
expect(screen.getByRole("link", { name: "Б" }).getAttribute("aria-current")).toBe("page");
expect(screen.getByRole("link", { name: "А" }).hasAttribute("aria-current")).toBe(false);
});
});
describe("ThemeSwitch", () => {
afterEach(() => document.documentElement.removeAttribute("data-theme"));
it("системная тёмная -> checked; щелчки переключают light и dark", () => {
stubMatchMedia(true);
render(<ThemeSwitch />);
const sw = screen.getByRole("switch", { name: "Тёмная тема" });
expect(sw.getAttribute("aria-checked")).toBe("true");
fireEvent.click(sw);
expect(localStorage.getItem("graph-theme")).toBe("light");
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
expect(sw.getAttribute("aria-checked")).toBe("false");
fireEvent.click(sw);
expect(localStorage.getItem("graph-theme")).toBe("dark");
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
});
});
describe("Chip и Notice", () => {
it("Chip: кнопка снятия вызывает onRemove", () => {
const onRemove = vi.fn();
render(<Chip onRemove={onRemove} removeLabel="Снять фильтр Спринт">Спринт</Chip>);
fireEvent.click(screen.getByRole("button", { name: "Снять фильтр Спринт" }));
expect(onRemove).toHaveBeenCalledOnce();
});
it("Notice: error — alert, info — status", () => {
render(<><Notice tone="error" title="Сбой">текст</Notice><Notice tone="info">инфо</Notice></>);
expect(screen.getByRole("alert").textContent).toContain("Сбой");
expect(screen.getByRole("status").textContent).toContain("инфо");
});
});