Vitest : composants et DOM
Pour tester l’interface, Vitest exécute le code dans un faux navigateur (jsdom ou happy-dom) et on pilote les composants avec Testing Library, qui interroge la page comme le ferait un utilisateur.
Installer
Section intitulée « Installer »npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event| Paquet | Rôle |
|---|---|
jsdom |
Environnement DOM simulé (happy-dom est une alternative plus rapide, moins complète) |
@testing-library/react |
Rendu et requêtes pour React |
@testing-library/jest-dom |
Assertions DOM (toBeInTheDocument…) |
@testing-library/user-event |
Simulation réaliste des actions de l’utilisateur |
Configurer
Section intitulée « Configurer »vitest.config.ts :
import react from '@vitejs/plugin-react';import { defineConfig } from 'vitest/config';
export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', setupFiles: ['./tests/setup.ts'], globals: true, css: false, },});tests/setup.ts :
import '@testing-library/jest-dom/vitest';import { cleanup } from '@testing-library/react';import { afterEach } from 'vitest';
afterEach(() => cleanup());Avec globals: true, Testing Library nettoie le DOM automatiquement ; sinon, appeler cleanup() comme ci-dessus. Dans tsconfig.json, ajouter "types": ["vitest/globals", "@testing-library/jest-dom"].
Pour choisir l’environnement par fichier :
// @vitest-environment jsdomen première ligne du fichier de test. Les tests de logique pure restent en environnement node (plus rapide).
Un premier test de composant
Section intitulée « Un premier test de composant »src/Compteur.tsx :
import { useState } from 'react';
export function Compteur({ depart = 0 }: { depart?: number }) { const [n, setN] = useState(depart); return ( <section> <p>Valeur : {n}</p> <button onClick={() => setN(n + 1)}>Incrémenter</button> </section> );}src/Compteur.test.tsx :
import { render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';import { describe, expect, it } from 'vitest';import { Compteur } from './Compteur';
describe('Compteur', () => { it('affiche la valeur de départ', () => { render(<Compteur depart={5} />); expect(screen.getByText('Valeur : 5')).toBeInTheDocument(); });
it('incrémente au clic', async () => { const utilisateur = userEvent.setup(); render(<Compteur />); await utilisateur.click( screen.getByRole('button', { name: 'Incrémenter' }), ); expect(screen.getByText('Valeur : 1')).toBeInTheDocument(); });});Les requêtes : par priorité
Section intitulée « Les requêtes : par priorité »Chercher les éléments comme un utilisateur (et un lecteur d’écran) :
| Requête | Usage |
|---|---|
getByRole('button', { name: 'Envoyer' }) |
Première intention : rôle et nom accessibles |
getByLabelText('E-mail') |
Champs de formulaire |
getByPlaceholderText, getByText |
Texte visible |
getByAltText, getByTitle |
Images, titres |
getByTestId('id') |
Dernier recours (data-testid) |
Variantes selon le besoin :
| Préfixe | Comportement |
|---|---|
getBy… |
Doit exister, sinon erreur |
queryBy… |
Renvoie null si absent (pour vérifier l’absence) |
findBy… |
Asynchrone : attend l’apparition (pour les mises à jour différées) |
getAllBy…, queryAllBy…, findAllBy… |
Plusieurs éléments |
expect(screen.queryByText('Erreur')).not.toBeInTheDocument();expect(await screen.findByText('Enregistré')).toBeInTheDocument();Les requêtes par rôle encouragent aussi l’accessibilité : si un élément est introuvable par son rôle, il est probablement mal balisé.
Assertions DOM (jest-dom)
Section intitulée « Assertions DOM (jest-dom) »expect(element).toBeInTheDocument();expect(element).toBeVisible();expect(element).toBeDisabled();expect(element).toHaveTextContent('Bonjour');expect(champ).toHaveValue('alice@exemple.com');expect(case).toBeChecked();expect(lien).toHaveAttribute('href', '/docs/');expect(element).toHaveClass('actif');expect(champ).toBeRequired();expect(champ).toHaveAccessibleName('E-mail');Formulaires
Section intitulée « Formulaires »it('envoie le formulaire', async () => { const onEnvoyer = vi.fn(); const utilisateur = userEvent.setup(); render(<Inscription onEnvoyer={onEnvoyer} />);
await utilisateur.type( screen.getByLabelText('Adresse e-mail'), 'alice@exemple.com', ); await utilisateur.click( screen.getByRole('checkbox', { name: /conditions/i }), ); await utilisateur.click(screen.getByRole('button', { name: "S'inscrire" }));
expect(onEnvoyer).toHaveBeenCalledWith({ email: 'alice@exemple.com', accord: true, });});userEvent reproduit une vraie saisie (focus, frappe, événements) : préférable à fireEvent. Appeler userEvent.setup() avant render.
Composants asynchrones
Section intitulée « Composants asynchrones »it('affiche les clients chargés', async () => { vi.stubGlobal( 'fetch', vi .fn() .mockResolvedValue( new Response(JSON.stringify([{ id: 1, nom: 'Alice' }])), ), );
render(<ListeClients />);
expect(screen.getByText('Chargement…')).toBeInTheDocument(); expect(await screen.findByText('Alice')).toBeInTheDocument();});findBy… attend l’apparition (par défaut 1 seconde) ; pour une condition plus complexe : await waitFor(() => expect(...)). Ne pas utiliser de setTimeout pour « attendre » dans un test.
Fournisseurs et routage
Section intitulée « Fournisseurs et routage »import { MemoryRouter } from 'react-router';
function afficher(ui: React.ReactElement, route = '/') { return render(<MemoryRouter initialEntries={[route]}>{ui}</MemoryRouter>);}Les composants qui dépendent d’un contexte (thème, requêtes TanStack Query, routeur) se rendent dans leur fournisseur ; encapsuler ce rendu dans une fonction utilitaire de test (voir React : données et routage).
Hooks personnalisés
Section intitulée « Hooks personnalisés »import { act, renderHook } from '@testing-library/react';
it('mémorise la valeur', () => { const { result } = renderHook(() => useStockageLocal('cle', 1)); act(() => result.current[1](2)); expect(result.current[0]).toBe(2);});jsdom fournit un localStorage fonctionnel : vider entre les tests (localStorage.clear() dans beforeEach).
Ce que le faux navigateur ne fait pas
Section intitulée « Ce que le faux navigateur ne fait pas »jsdom n’implémente ni la mise en page ni le rendu visuel : getBoundingClientRect renvoie des zéros, pas de CSS réel ni de IntersectionObserver natif. Pour ces cas : simuler l’API manquante, ou utiliser des tests dans un vrai navigateur (mode navigateur de Vitest, ou Playwright).
Composants Astro
Section intitulée « Composants Astro »Les composants .astro ne se testent pas avec Testing Library ; Astro propose une API de rendu en conteneur (experimental_AstroContainer) ou des tests de bout en bout. Tester la logique (fonctions, utilitaires) à part, en tests unitaires.
Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
document is not defined |
Environnement node : environment: 'jsdom' |
toBeInTheDocument is not a function |
@testing-library/jest-dom/vitest non importé dans le fichier de setup |
Unable to find an accessible element with the role |
Mauvais nom ou rôle : screen.debug() ou logRoles(container) |
| Éléments du test précédent encore présents | cleanup non appelé (globales désactivées) |
act(...) avertissement |
Mise à jour d’état hors await : attendre avec findBy… ou waitFor |
Not wrapped in act avec userEvent |
userEvent.setup() après render, ou await manquant |
| Import CSS ou image qui échoue | css: false, ou assetsInclude ; Vite gère déjà la plupart des cas |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Aucune. Pour des tests dans un vrai navigateur sur serveur sans écran, installer les navigateurs de Playwright (npx playwright install --with-deps) ; sous Ubuntu, cette commande installe les bibliothèques système nécessaires (apt).