Aller au contenu

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.

Fenêtre de terminal
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

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 jsdom

en première ligne du fichier de test. Les tests de logique pure restent en environnement node (plus rapide).

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();
});
});

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é.

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');
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.

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.

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).

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).

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).

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.

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).