Files
IdeA/frontend/src/shared/ui/ui.test.tsx
Blomios 1a1429f10e feat(domain,application,infrastructure,backend,web-server,app-tauri,frontend): lot utilisateur #325 #326 — icônes projet avec watermark CLI custom et localisation FR/EN (QA verte)
Lot en un seul commit: les deux features partagent les mêmes contrats
(domain/settings.rs, DTO, commandes Tauri/HTTP, web-server) et les mêmes
surfaces UI — la séparation par fichiers était impossible sans chirurgie
de hunks risquant des états intermédiaires non compilables (convention
lot des batches #310-#313 et #315).

#325 — icône configurable par projet, watermark CLI custom:
- domaine: ProjectIconSet (devicon, simple-icons, lucide, idea),
  ProjectIconRef validé (nom ASCII 1..=64, -_. internes), champ icon
  optionnel sur ProjectSettings; sérialisation camelCase et try_from
  strict
- application: UpdateProjectSettings.execute accepte un patch
  theme_color OU icon (absent = inchangé, null = défaut produit), tests
  in-dependants avec store mémoire
- backend/app-tauri/web-server: ProjectIconRefDto, UpdateProjectSettingsRequestDto
  avec icon optionnel (désérialisation additive), commandes
  update_project_settings alignées Tauri/HTTP, tests DTO et HTTP
  (défaut null, set, clear, nom invalide rejeté)
- frontend: catalogue projectIcons (recherche EN), ProjectIconPicker en
  grille multi-lignes dans Paramètres projet, affichage de l'icône dans
  ProjectTabs et watermark grand format à faible opacité en fond de CLI
  custom; ports ProjectSettings.icon, Partial<ProjectSettings>,
  adapters tauri/mock/http et tests alignés

#326 — localisation FR/EN et traduction des textes IdeA:
- domaine: IdeaLanguage (fr | en), champ language optionnel sur
  IdeaUserSettings (absence = défaut produit, série additive validée)
- backend/web-server: UserSettingsDto.language, update_user_settings
  accepte language, tests HTTP alignés; fixtures structured_launch_d3
  mises à jour
- frontend: i18n/ (catalogue FR/EN, I18nProvider, scan anti-régression
  des sources visibles), GeneralSettings avec sélecteur de langue dans
  les paramètres, setupI18n dans vitest; migration de l'ensemble des
  textes hardcodés vers le catalogue sur toutes les surfaces (agents,
  layout, projets, tickets, sprints, web, plugins, permissions, git,
  templates, skills, mémoire, model servers, embedder, first-run,
  devices, scheduled tasks, workstate, dialogs), FR conservé par défaut;
  recherche d'icônes #325 en anglais conformément au ticket

QA verte sur feature/batch-325-326-project-icons-localisation. Aucun push.
2026-09-11 21:53:56 +02:00

164 lines
5.0 KiB
TypeScript

/**
* 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(<Button onClick={onClick}>Save</Button>);
fireEvent.click(screen.getByRole("button", { name: "Save" }));
expect(onClick).toHaveBeenCalledOnce();
});
it("is disabled and shows a spinner while loading", () => {
const onClick = vi.fn();
render(
<Button loading onClick={onClick}>
Save
</Button>,
);
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(<Button>Go</Button>);
expect(screen.getByRole("button", { name: "Go" }).getAttribute("type")).toBe(
"button",
);
});
});
describe("Input", () => {
it("flags aria-invalid when invalid", () => {
render(<Input aria-label="email" invalid />);
expect(screen.getByLabelText("email").getAttribute("aria-invalid")).toBe("true");
});
it("is not aria-invalid by default", () => {
render(<Input aria-label="email" />);
expect(screen.getByLabelText("email").getAttribute("aria-invalid")).toBeNull();
});
});
describe("Field", () => {
it("associates the label with the control via id", () => {
render(
<Field label="Project name">
{({ id, describedBy }) => (
<input id={id} aria-describedby={describedBy} />
)}
</Field>,
);
// getByLabelText resolves the label→control association.
expect(screen.getByLabelText("Project name")).toBeTruthy();
});
it("announces an error and wires aria-describedby", () => {
render(
<Field label="Root" error="must be absolute">
{({ id, describedBy }) => (
<input id={id} aria-describedby={describedBy} />
)}
</Field>,
);
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(<Tabs items={items} value="a" onSelect={onSelect} />);
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(
<Tabs items={items} value="a" onSelect={() => {}} />,
);
expect(screen.queryByRole("button", { name: "fermer alpha" })).toBeNull();
rerender(<Tabs items={items} value="a" onSelect={() => {}} 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(
<SmallDropdown
aria-label="test dropdown"
value=""
options={options}
onChange={onChange}
/>,
);
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(
<SmallDropdown
aria-label="test dropdown"
value=""
options={options}
onChange={onChange}
/>,
);
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");
});
});