Vous avez rendu vos composants accessibles. Mais une accessibilité qu’on ne mesure pas se dégrade : un développeur retire une étiquette, un autre ajoute un contraste insuffisant, et trois sprints plus tard l’interface est de nouveau inutilisable au clavier. La parade n’est pas un audit annuel, c’est un filet automatique qui tourne à chaque modification. Ce tutoriel met en place trois niveaux de tests d’accessibilité et les branche dans l’intégration continue pour qu’une régression bloque la fusion.
🎯 Ce que vous allez apprendre
- Comprendre les trois niveaux de test automatisé et ce que chacun couvre — et ne couvre pas.
- Configurer ESLint avec jsx-a11y pour repérer les défauts dès l’écriture du code.
- Tester un composant React avec vitest-axe et le matcher
toHaveNoViolations. - Auditer une page rendue avec @axe-core/playwright et cibler les règles WCAG 2.2.
- Faire échouer le pipeline d’intégration continue dès qu’une régression apparaît.
🛠️ Ce que vous allez construire
Une chaîne à trois étages — analyse statique, test de composant, test de page — qui s’exécute à chaque pull request et empêche de fusionner un code qui régresse l’accessibilité. Une fois en place, le filet travaille sans intervention.
Prérequis
- Un projet React avec ESLint, et un lanceur de tests (Vitest dans nos exemples ; Jest fonctionne pareil).
- Playwright installé pour les tests de page (
npm init playwright@latest). - Un dépôt avec intégration continue (les exemples utilisent un workflow standard).
- ⏱️ Temps estimé : ~45 minutes.
Étape 1 — Comprendre les trois niveaux (et leur limite)
Avant de configurer quoi que ce soit, fixons les attentes. Les tests automatisés d’accessibilité ne couvrent qu’une partie des problèmes — de l’ordre de 30 à 40 % des critères selon le référentiel britannique GDS (le meilleur outil y détectait environ 40 % des barrières d’une page de test), une part qui grimpe si on compte en volume d’erreurs, le contraste de couleur (très fréquent et automatisable) gonflant le ratio. Ils repèrent l’absence d’étiquette, un contraste insuffisant, un attribut ARIA invalide, mais ils ne jugent ni la pertinence d’un texte alternatif, ni la logique d’un parcours clavier. Ils complètent les tests manuels du tutoriel WCAG 2.2, ils ne les remplacent pas. Cela dit, ces 30 à 40 % sont précisément les régressions répétitives et bêtes que l’humain laisse passer par lassitude — exactement ce qu’une machine doit surveiller.
Les trois niveaux se distinguent par ce qu’ils voient. L’analyse statique (ESLint) lit le code source sans l’exécuter : rapide, mais aveugle au rendu. Le test de composant exécute un composant isolé et l’inspecte : il voit le DOM réel mais pas la page entière. Le test de page charge l’interface complète dans un vrai navigateur : le plus proche de l’utilisateur, mais le plus lent. On les superpose, du plus rapide au plus lent, pour attraper chaque défaut au niveau le moins coûteux.
Étape 2 — Niveau 1 : l’analyse statique avec jsx-a11y
Le plugin eslint-plugin-jsx-a11y signale, pendant que vous tapez, les fautes d’accessibilité décelables dans le JSX : une image sans alt, un onClick sur une div non focusable, un label non relié. On l’active via sa configuration recommandée, au format « flat config » des versions récentes d’ESLint :
// eslint.config.js
import jsxA11y from 'eslint-plugin-jsx-a11y';
export default [
jsxA11y.flatConfigs.recommended,
// ... vos autres configurations
];
Lancez npx eslint . : les violations apparaissent comme n’importe quelle erreur de lint, avec le fichier et la ligne. C’est le niveau le moins cher — aucune exécution, retour immédiat dans l’éditeur. Traitez ces avertissements comme des erreurs bloquantes : ils interceptent les fautes les plus communes avant même le premier rendu.
✅ Point d’étape —
npx eslint .remonte les problèmes d’accessibilité du JSX. Pour tester, retirez lealtd’une image : le lint doit protester. S’il reste muet, vérifiez que la config recommandée est bien chargée.
Étape 3 — Niveau 2 : tester un composant avec vitest-axe
L’analyse statique ne voit pas le DOM produit. Pour cela, on rend le composant en test et on le passe au moteur axe-core via vitest-axe (ou jest-axe avec Jest), qui expose le matcher toHaveNoViolations. On étend d’abord expect dans le fichier d’amorçage des tests, puis on écrit l’assertion :
import { render } from '@testing-library/react';
import { axe } from 'vitest-axe';
import { expect, test } from 'vitest';
import { Modal } from './Modal';
test('la modale ne présente aucune violation axe', async () => {
const { container } = render(<Modal open title="Détails" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Le test rend le composant, soumet son DOM à axe-core, et échoue avec un rapport détaillé si une règle est violée — règle concernée, élément fautif, lien d’explication. Une précision importante : la vérification de contraste de couleur ne fonctionne pas dans l’environnement de test simulé (JSDOM) et y est désactivée ; ce contrôle-là se fera au niveau page, à l’étape suivante.
Étape 4 — Niveau 3 : auditer une page avec @axe-core/playwright
Le test ultime charge la vraie page dans un vrai navigateur. L’intégration officielle @axe-core/playwright fournit AxeBuilder, qui pilote Playwright et renvoie les violations. On peut cibler précisément les règles voulues — par exemple les niveaux A et AA de WCAG, y compris les ajouts de 2.2 :
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('la page de connexion est conforme', async ({ page }) => {
await page.goto('/login');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
La méthode .withTags restreint l’analyse aux règles correspondant aux critères ciblés, et .analyze() exécute l’audit sur la page chargée. Comme c’est un vrai navigateur, le contraste de couleur est cette fois évalué. On peut affiner avec .include(), .exclude() ou .disableRules() pour cibler une zone ou écarter une règle non pertinente. L’assertion sur un tableau de violations vide rend l’échec parlant : la trace liste chaque problème.
Étape 5 — Faire échouer le pipeline
Un test qui ne bloque rien ne sert à rien. L’intérêt des assertions toHaveNoViolations et toEqual([]) est qu’elles font sortir le lanceur de tests en erreur, donc le job d’intégration continue en échec. Combiné au lint traité comme bloquant, vous obtenez trois portes : si l’une se ferme, la fusion est refusée. C’est ce passage du « rapport informatif » au « blocage effectif » qui change tout — sans lui, les rapports d’accessibilité finissent ignorés comme tant d’autres avertissements.
Étape 6 — Brancher le tout en intégration continue
On rassemble les trois niveaux dans un job unique, déclenché à chaque pull request. Le job installe les dépendances, lance le lint, les tests de composant, puis les tests de page :
# .github/workflows/a11y.yml
name: Accessibilité
on: [pull_request]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx eslint .
- run: npx vitest run
- run: npx playwright install --with-deps
- run: npx playwright test
Chaque run qui échoue arrête le job et marque la pull request en échec. L’ordre — lint, puis composant, puis page — place les contrôles rapides en premier : inutile de lancer un navigateur complet si le lint a déjà trouvé une faute évidente. C’est le filet qui tournera désormais sans que personne n’y pense.
✅ Point d’étape — Ouvrez une pull request introduisant une faute volontaire (une image sans
alt) : le job doit passer au rouge. Si tout reste vert, vérifiez que les commandes sortent bien en erreur (essayez-les en local d’abord).
Étape 7 — Gérer le bruit sans tricher
Un piège guette : face à une règle jugée gênante, on la désactive globalement, et le filet se troue. La discipline consiste à n’écarter une règle que localement et avec justification. Si un composant tiers déclenche une violation que vous ne pouvez pas corriger, ciblez-le avec .exclude() plutôt que de couper la règle partout. Si une règle est réellement inadaptée à votre contexte, documentez la raison dans la configuration. L’objectif n’est jamais « zéro alerte » obtenu en aveuglant l’outil, mais « zéro violation réelle » sur ce que vous maîtrisez. Une exclusion non documentée est une dette qui se paiera au prochain audit humain.
Couvrir les 60 % que l’automatisation ne voit pas
Si les outils ne couvrent qu’une fraction des problèmes — de l’ordre de 30 à 40 % des critères —, la question honnête est : que fait-on du reste ? L’erreur serait de conclure que l’automatisation ne sert à rien. Au contraire — en éliminant les fautes mécaniques, elle libère du temps humain pour les défauts que seul un jugement repère. Encore faut-il organiser ce temps, sinon le « reste » n’est jamais testé.
La première catégorie d’angles morts concerne la pertinence du contenu. Un outil vérifie qu’une image a un attribut alt ; il ne dit pas si « image123.png » décrit réellement le graphique. Il voit qu’un bouton a un nom accessible ; il ignore que ce nom, « Cliquez ici », ne veut rien dire hors contexte. Ces jugements de qualité se font à la relecture, idéalement dans la revue de code : exigez que chaque texte alternatif et chaque libellé soit lisible isolément.
La deuxième catégorie est la logique d’interaction. L’ordre de tabulation suit-il le sens de lecture ? Le focus part-il au bon endroit après la soumission d’un formulaire ? Un message d’erreur est-il annoncé au lecteur d’écran, ou apparaît-il silencieusement ? Aucun outil ne tranche ces questions, qui exigent de parcourir le scénario. C’est le rôle du protocole manuel de cinq minutes décrit dans le tutoriel WCAG 2.2 : à inscrire dans la définition de « terminé » d’une fonctionnalité, pas à reléguer à la fin du projet.
La troisième catégorie, la plus précieuse, est le test avec de vrais utilisateurs de technologies d’assistance. Une demi-heure d’observation d’une personne naviguant au lecteur d’écran révèle plus de défauts réels qu’une semaine d’audit théorique, parce qu’elle expose les frictions que personne dans l’équipe ne soupçonnait. Quand c’est possible, c’est le contrôle qui a le plus de valeur.
La bonne manière de voir l’ensemble : l’automatisation est le socle qui empêche de régresser sur l’évident, la relecture et le test manuel forment l’étage du jugement, et le test utilisateur est le sommet qui valide l’usage réel. Chaque étage suppose le précédent. Construire le sommet sans le socle, c’est gaspiller un temps humain rare à chasser des fautes qu’une machine aurait signalées gratuitement. Mettre en place le filet de ce tutoriel n’est donc pas la fin du travail d’accessibilité — c’en est la fondation, celle qui rend tout le reste soutenable.
🐞 Pièges fréquents
| Symptôme | Cause probable | Correctif |
|---|---|---|
| Le job reste vert malgré une faute | La commande ne sort pas en erreur | Vérifier les assertions et le code de sortie |
| Contraste non détecté en test composant | Désactivé sous JSDOM | Le vérifier au niveau page (Playwright) |
toHaveNoViolations is not a function |
Matcher non étendu | Importer l’extension dans l’amorçage des tests |
| Trop d’alertes ingérables | Règles désactivées en masse plus tôt | Réactiver, exclure localement le cas précis |
| Tests de page lents en CI | Navigateur relancé par test | Grouper les audits, réutiliser le contexte |
✅ Récapitulatif
Vous disposez d’un filet à trois étages : jsx-a11y attrape les fautes à l’écriture, vitest-axe valide chaque composant, @axe-core/playwright audite la page entière contrastes compris, et l’intégration continue bloque toute régression avant fusion. Vous savez aussi ce que ce filet ne voit pas — environ deux tiers des problèmes — et pourquoi il reste indispensable malgré tout : il élimine les régressions répétitives pour que vos tests manuels se concentrent sur le jugement.
🧾 Aide-mémoire
| Niveau | Outil | Ce qu’il voit |
|---|---|---|
| Statique | eslint-plugin-jsx-a11y |
Fautes dans le JSX source |
| Composant | vitest-axe / jest-axe |
DOM d’un composant isolé |
| Page | @axe-core/playwright |
Page rendue, contraste compris |
| Blocage | toHaveNoViolations / toEqual([]) |
Échec du job d’intégration |
💪 À vous de jouer
Ajoutez un test de page qui ouvre la modale du tutoriel précédent avant de lancer l’audit, afin de vérifier l’accessibilité de l’état ouvert (et pas seulement de la page au repos). Cliquez le déclencheur avec Playwright, attendez l’apparition du dialogue, puis lancez analyze().
Voir une piste de solution
Avant l’audit : await page.getByRole('button', { name: 'Ouvrir les détails' }).click(); puis await page.getByRole('dialog').waitFor();. L’AxeBuilder analysera alors le DOM avec la modale ouverte. On peut restreindre l’analyse au dialogue avec .include('[role=dialog]').
Tutoriels de la série
- Construire des composants React accessibles avec ARIA — les composants que ces tests protègent.
- Implémenter les critères WCAG 2.2 en code — ce que les tests automatisés ne voient pas.
Pour aller plus loin
- 🔝 Retour au guide principal : Du design au code : Figma, tokens et WCAG 2.2.
- Audit au niveau page : Lighthouse + Pa11y : automatiser l’audit d’accessibilité.
- Référence officielle : @axe-core/playwright (Deque).
FAQ
Jest ou Vitest ?
Les deux conviennent : jest-axe pour Jest, vitest-axe (un portage de jest-axe) pour Vitest. Le matcher et l’usage sont identiques ; choisissez selon le lanceur déjà en place.
Pourquoi ne pas se contenter de Lighthouse ?
Lighthouse audite une page entière, utile en complément, mais ne teste pas un composant isolé ni n’intègre aussi finement le clavier dans un scénario. Les trois niveaux décrits ici se placent plus tôt et plus précisément dans le cycle de développement.
Ces tests garantissent-ils la conformité ?
Non. Ils établissent une base et préviennent les régressions, mais la conformité exige aussi des tests manuels et, idéalement, des tests avec de vrais utilisateurs de technologies d’assistance.
Faut-il viser zéro violation dès le départ ?
Sur un projet existant, on peut figer une référence (les violations connues) et interdire toute nouvelle violation, puis résorber le passif progressivement. Sur un nouveau projet, zéro dès le départ est l’objectif réaliste.