Tests frontend
Testez ce que l'utilisateur voit et fait, pas comment le composant est construit. Associez avec naki-tdd pour la boucle,
naki-e2e-playwright pour les flux navigateur complets et naki-accessibility pour la sémantique. Suivez la règle
skill du projet si elle existe (par exemple naki-myapp).
Quand l'utiliser
- Un nouveau composant, hook ou page nécessite des tests.
- Un test échoue après un refactor qui n'a pas changé le comportement (signe de couplage à l'implémentation).
- Les tests sont instables, utilisent
waitForpartout, ou moquent la moitié de l'application.
Règles essentielles
- Interrogez comme un utilisateur. Priorité :
getByRole(avecname),getByLabelText,getByPlaceholderText,getByText,getByDisplayValue. UtilisezgetByTestIduniquement quand rien d'accessible n'existe. - Interagissez avec
userEvent, pasfireEvent. Créez-le avecconst user = userEvent.setup()et attendez chaque appel. - Affirmez sur la sortie : texte visible, rôles, valeurs de formulaire, appels à un endpoint réseau. N'affirmez jamais sur les variables d'état, les internes des hooks, les noms de classes CSS ou les méthodes d'instance des composants.
- Simulez le réseau, pas vos modules. Utilisez les gestionnaires MSW au niveau HTTP. Ne mockez un module que pour des limites extérieures réelles (temps, aléatoire, API du navigateur non présentes dans jsdom).
- Utilisez
findBy*pour les choses qui apparaissent plus tard. RéservezwaitForpour les assertions qui ne sont pas une requête, et ne mettez jamais d'effets de bord danswaitFor. - Un comportement par test, nommé comme une phrase :
affiche une erreur quand le serveur renvoie 500. - Chaque test configure son propre état. Pas de dépendance d'ordre ; réinitialisez les handlers, les timers et le stockage dans
afterEach. - Fournissez de vrais fournisseurs via un helper
renderWithProviders(routeur, client de requête avec réessai désactivé, thème) au lieu de les moquer. - L'accessibilité est testable. Un échec de
getByRole("button", { name: "Enregistrer" })signale souvent un vrai bug d'accessibilité. Ajoutez optionnellement des vérificationsvitest-axepour les écrans clés. - La couverture est un signal, pas un objectif. Couvrez les branches qui comptent : états d'erreur, états vides, permissions.
Aide-mémoire des requêtes
| Élément | Requête |
|---|---|
| bouton | getByRole("button", { name: /enregistrer/i }) |
| lien | getByRole("link", { name: "Docs" }) |
| champ texte avec label | getByRole("textbox", { name: "Email" }) ou getByLabelText("Email") |
| case à cocher | getByRole("checkbox", { name: "Se souvenir de moi" }) |
| titre | getByRole("heading", { level: 2, name: "Alertes" }) |
| ligne de tableau | getAllByRole("row") puis within(row) |
| dialogue | getByRole("dialog", { name: "Supprimer l'alerte" }) |
| absence | expect(queryByText("Erreur")).not.toBeInTheDocument() |
Workflow
- Listez les comportements : états de rendu (chargement, vide, données, erreur) et interactions.
- Ajoutez des gestionnaires MSW pour les endpoints que le composant appelle ; par défaut en succès.
- Écrivez un test par comportement, en écrasant les handlers avec
server.use(...)pour les cas d'erreur. - Lancez
npx vitest run path/to/file.test.tsx, puis toute la suite. - Cassez intentionnellement l'implémentation une fois (commentez une ligne) pour confirmer que le test peut échouer.
Configuration
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
import { afterAll, afterEach, beforeAll } from "vitest";
import { cleanup } from "@testing-library/react";
import { server } from "./src/test/server";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => { server.resetHandlers(); cleanup(); });
afterAll(() => server.close());
onUnhandledRequest: "error" fait échouer un handler manquant de manière visible au lieu de bloquer.
Exemple
import { http, HttpResponse } from "msw";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { server } from "../test/server";
import { AlertList } from "./AlertList";
test("liste les alertes et filtre par sévérité", async () => {
server.use(http.get("/api/alerts", () => HttpResponse.json([
{ id: "a1", title: "Brute force sur le VPN", severity: "high" },
{ id: "a2", title: "Nouvelle connexion admin", severity: "low" },
])));
const user = userEvent.setup();
render(<AlertList />);
expect(await screen.findByText("Brute force sur le VPN")).toBeInTheDocument();
await user.selectOptions(screen.getByRole("combobox", { name: "Sévérité" }), "high");
expect(screen.queryByText("Nouvelle connexion admin")).not.toBeInTheDocument();
});
test("affiche un bouton de réessai en cas d'échec du chargement", async () => {
server.use(http.get("/api/alerts", () => new HttpResponse(null, { status: 500 })));
render(<AlertList />);
expect(await screen.findByRole("button", { name: /réessayer/i })).toBeInTheDocument();
});
Hooks et temps
- Testez un hook via un petit composant qui l'utilise, ou avec
renderHookquand il n'a pas d'UI. - Temporisateurs fictifs :
vi.useFakeTimers()etuserEvent.setup({ advanceTimers: vi.advanceTimersByTime }); restaurez avecvi.useRealTimers()dansafterEach. - Figez les dates avec
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"))pour que les snapshots et les labels soient stables.
Liste de vérification
- Les requêtes sont basées sur les rôles ou les labels.
- Tous les appels
userEventsont attendus. - Réseau mocké avec MSW ; les requêtes non traitées échouent.
- États de chargement, d'erreur et vide couverts.
- Pas d'assertion sur les internes ou les noms de classes.
- Les tests passent seuls et dans n'importe quel ordre.
- Pas d'avertissement
act(...)dans la sortie.
Erreurs courantes
| Erreur | Correction |
|---|---|
getByTestId partout | requêtes par rôle/label ; ajoutez des labels au composant si manquants |
await waitFor(() => getByText("x")) | await findByText("x") |
fireEvent.change pour la saisie | await user.type(input, "texte") |
Moquer directement le hook useAlerts | mockez l'endpoint HTTP |
| Grands snapshots de pages entières | assertions explicites sur ce qui compte |
| Client de requête avec réessais activés, tests d'erreur lents | retry: false dans le fournisseur de test |
Avertissements act ignorés | attendez la mise à jour asynchrone de l'UI avec findBy* |