Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions GLOSSAIRE.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ pour la justification des décisions de conception, voir les [ADR](docs/adr/READ
- **ADR (Architecture Decision Record)** — Un document court, numéroté et en
ajout seul qui capture une décision de conception et ses compromis, au
format MADR.
- **axe-core** — Le moteur open source de règles d'accessibilité derrière la
plupart des audits automatisés (utilisé par Lighthouse et des extensions
de navigateur). Exécuté ici sur les pages rendues avec le jeu de règles
WCAG 2.1 A/AA ; résultats dans [docs/rgaa-audit.md](docs/rgaa-audit.md).
- **Bean Validation** — Le standard Jakarta de déclaration de contraintes
(`@NotBlank`, `@Email`, `@Size`) sur les champs de formulaires/DTO,
appliquées avec `@Valid`.
Expand Down Expand Up @@ -181,6 +185,10 @@ pour la justification des décisions de conception, voir les [ADR](docs/adr/READ
gestionnaires d'événements, cadres) du HTML des leçons rendues : une
défense XSS qui ne repose pas sur la confiance envers les auteurs (voir
[ADR-0013](docs/adr/0013-render-lesson-markdown-with-commonmark-java.md)).
- **Lighthouse** — L'outil d'audit de pages de Google (livré avec Chrome).
Sa catégorie accessibilité note une page rendue selon un sous-ensemble
des règles axe-core ; chaque page de learn-dev obtient 100 dans
[docs/rgaa-audit.md](docs/rgaa-audit.md).
- **Linter** — Un outil qui signale les problèmes de style et de qualité dans
le code source sans l'exécuter (analyse statique). Le linter du projet est
Checkstyle.
Expand Down
8 changes: 8 additions & 0 deletions GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ rationale behind design decisions, see the [ADRs](docs/adr/README.md).

- **ADR (Architecture Decision Record)** — A short, numbered, append-only document
capturing one design decision and its trade-offs, in MADR format.
- **axe-core** — The open-source accessibility rules engine behind most
automated audits (used by Lighthouse and browser extensions). Run here
against the rendered pages with the WCAG 2.1 A/AA rule set; results in
[docs/rgaa-audit.md](docs/rgaa-audit.md).
- **Bean Validation** — The Jakarta standard for declaring constraints
(`@NotBlank`, `@Email`, `@Size`) on form/DTO fields, enforced with `@Valid`.
- **Caffeine** — A high-performance in-memory cache library for Java. Used
Expand Down Expand Up @@ -151,6 +155,10 @@ rationale behind design decisions, see the [ADRs](docs/adr/README.md).
strips dangerous markup (scripts, event handlers, frames) from the rendered
lesson HTML: XSS defense that does not depend on trusting authors (see
[ADR-0013](docs/adr/0013-render-lesson-markdown-with-commonmark-java.md)).
- **Lighthouse** — Google's page auditing tool (bundled with Chrome). Its
accessibility category scores a rendered page against a subset of
axe-core rules; every learn-dev page scores 100 in
[docs/rgaa-audit.md](docs/rgaa-audit.md).
- **Linter** — A tool that flags style and quality issues in source code without
running it (static analysis). The project's linter is Checkstyle.
- **Lombok** — A library that generates boilerplate (getters, constructors) from
Expand Down
173 changes: 173 additions & 0 deletions docs/rgaa-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Auto-audit d'accessibilité RGAA

Rapport d'auto-audit d'accessibilité de **learn-dev**, destiné à figurer en
annexe du dossier projet de la certification DWWM. Il complète
[docs/rgaa.md](rgaa.md) (English), qui explique chaque critère et où il est
implémenté ; le présent document apporte la **preuve outillée** : mesures
Lighthouse et axe-core sur chaque page, et parcours clavier manuel.

- **Date de l'audit** : 12 juillet 2026
- **État audité** : branche `dev`, commit `8c82a93` (application démarrée
localement, profil `dev`)
- **Périmètre** : les 7 pages de l'application, y compris la page
authentifiée, plus l'état d'erreur du formulaire d'inscription
- **Référentiel** : RGAA 4.x (déclinaison française de WCAG 2.1 niveau AA)

## Méthode et outils

| Outil | Version | Rôle |
|-------|---------|------|
| Lighthouse | 13.4.0 | Score d'accessibilité par page (Chrome headless) |
| axe-core | 4.12.1 | Analyse statique du DOM rendu, jeu de règles WCAG 2.1 A/AA + bonnes pratiques, injecté dans le navigateur sur la page réelle |
| Parcours clavier | manuel | Ordre de tabulation, visibilité du focus, lien d'évitement, connexion au clavier seul |

Trois angles indépendants : Lighthouse note la page, axe-core détaille les
règles (y compris celles que Lighthouse ne couvre pas), et le parcours
clavier vérifie ce qu'aucun outil automatique ne sait prouver
(l'utilisabilité réelle au clavier). La page authentifiée `/dashboard` a été
auditée avec une vraie session (compte créé pour l'audit), pas une copie
statique.

**Exemple : reproduire la mesure Lighthouse d'une page**

```shell
npx lighthouse "http://localhost:8082/auth/login" \
--only-categories=accessibility \
--output=json --output-path=lh-login.json \
--chrome-flags="--headless=new"
```

Où :

- `--only-categories=accessibility` limite l'audit à la catégorie
accessibilité (les autres catégories ne concernent pas ce rapport) ;
- `--chrome-flags="--headless=new"` exécute Chrome sans interface ;
- le score se lit dans `categories.accessibility.score` du JSON (1 = 100 %).

**Exemple : reproduire l'analyse axe-core dans le navigateur**

```js
// Dans la console du navigateur, sur la page à auditer,
// après avoir chargé axe.min.js (npm install axe-core) :
const r = await axe.run(document, {
runOnly: { type: 'tag',
values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'] }
});
console.log(r.violations); // [] attendu
```

## Résultats : mesures automatiques

| Page | État | Lighthouse | axe : violations | axe : règles passées |
|------|------|-----------:|-----------------:|---------------------:|
| `/` (accueil) | anonyme | **100** | **0** | 32 |
| `/auth/login` | anonyme | **100** | **0** | 37 |
| `/auth/register` | anonyme | **100** | **0** | 38 |
| `/auth/register` | erreurs de saisie | n/a (voir ci-dessous) | n/a | markup vérifié |
| `/auth/forgot-password` | anonyme | **100** | **0** | 38 |
| `/auth/reset-password` | lien invalide | **100** | **0** | 35 |
| `/privacy` (`lang="fr"`) | anonyme | **100** | **0** | 37 |
| `/dashboard` | **authentifié** | **100** | **0** | 33 |

**État d'erreur du formulaire d'inscription** : une soumission vide (POST
anonyme avec jeton CSRF valide) re-rend la page (HTTP 200) avec 3 champs
marqués `aria-invalid="true"`, chacun relié à son message par
`aria-describedby` (par exemple `aria-describedby="username-hint
username-error"`, message « must not be blank »). Ce câblage est aussi
verrouillé par un test de non-régression MockMvc
(`AuthFlowTest.register_form_renders_and_shows_field_errors`).

### Points « incomplete » d'axe-core

Sur `/auth/login`, axe-core classe 8 nœuds en `incomplete` (ni réussite ni
échec) pour la règle `color-contrast` : le **lien d'évitement**, positionné
hors écran au-dessus de l'en-tête tant qu'il n'a pas le focus, chevauche
géométriquement d'autres éléments, et l'outil renonce à déterminer leur
couleur de fond. Vérification manuelle : tous les couples texte/fond de la
charte sont calculés par script avec la formule de luminance relative WCAG
(méthode et valeurs dans
[design/theme-exploration.md](design/theme-exploration.md)) ; le pire couple
livré est à **4,73:1**, au-dessus du seuil AA de 4,5:1. Aucune action requise.

## Résultats : parcours clavier

Réalisé sur `/auth/login` en mode sombre (`prefers-color-scheme: dark`),
puis étendu par sondage aux autres pages (même gabarit `layout.html`, mêmes
styles de focus globaux).

### Ordre de tabulation constaté

| Tab | Élément | Focus visible |
|----:|---------|---------------|
| 1 | Lien d'évitement « Skip to main content » | contour 2 px |
| 2 | Marque « learn-dev » (retour accueil) | contour 2 px |
| 3 | Navigation : « Home » | contour 2 px |
| 4 | Navigation : « Log in » | contour 2 px |
| 5 | Navigation : « Sign up » | contour 2 px |
| 6 | Champ « Username » | contour 2 px |
| 7 | Champ « Password » | contour 2 px |
| 8 | Bouton « Log in » | contour 2 px |
| 9 | Lien « Create one » | contour 2 px |
| 10 | Lien « Forgot your password? » | contour 2 px |
| 11 | Pied de page : « Privacy policy (RGPD) » | contour 2 px |
| 12 | Pied de page : « Source on GitHub » | contour 2 px |

L'ordre suit la lecture visuelle, aucun piège au clavier, et **chacun des 12
arrêts** affiche l'indicateur de focus global (`outline: 2px solid`, couleur
du jeton bleu, mesuré `rgb(137, 180, 250)` en mode sombre). Aucun
`outline: none` non compensé.

### Lien d'évitement (RGAA 12.7)

- Premier élément focusable de chaque page ; visible dès qu'il a le focus.
- Cible `href="#main"` présente (`<main id="main">`).
- **Preuve d'utilité** : après activation du lien, la tabulation suivante
atterrit directement sur le champ « Username », en sautant les 4 liens
d'en-tête.

### Connexion au clavier seul

Le parcours complet de connexion a été réalisé sans souris : lien
d'évitement, saisie de l'identifiant, tabulation, saisie du mot de passe,
activation du bouton de soumission, arrivée sur `/dashboard`. Le formulaire
est un `<form method="post">` avec un `<button type="submit">`, ce qui
garantit aussi la soumission implicite par la touche Entrée dans un champ.

## Correspondance avec la grille de critères

Les mesures ci-dessus apportent la preuve outillée des critères suivis dans
[docs/rgaa.md](rgaa.md#criteria-map) ; en particulier :

| Critère RGAA | Preuve dans cet audit |
|--------------|------------------------|
| 3.2 (contrastes) | Lighthouse et axe sans violation ; ratios calculés, pire couple 4,73:1 |
| 8.3/8.4 (langue) | `lang` vérifié par axe sur chaque page, `lang="fr"` sur `/privacy` |
| 8.5/8.6 (titre de page) | Titres uniques constatés sur les 7 pages |
| 9.1/9.2 (titres, structure) | Règles axe heading-order et landmarks passées partout |
| 10.7 (focus visible) | 12/12 arrêts avec contour visible au parcours clavier |
| 11.1 (étiquettes) | Règle axe `label` passée sur les 4 formulaires |
| 11.11 (erreurs identifiées) | État d'erreur : `aria-invalid` + `aria-describedby` + message explicite |
| 12.7 (lien d'évitement) | Premier focusable, cible valide, utilité prouvée |

## Constats et recommandations

1. **Aucune violation** détectée par les deux outils sur les 7 pages, y
compris la page authentifiée et l'état « lien invalide » de la
réinitialisation ; score Lighthouse 100/100 partout.
2. **Recommandation (robustesse), implémentée** : ajouter `tabindex="-1"` sur
`<main id="main">`. Avant, l'activation du lien d'évitement
reposait sur le « sequential focus navigation starting point » du
navigateur (comportement correct des navigateurs modernes) ; avec
`tabindex="-1"`, le focus est déplacé explicitement sur `main`, ce
qui est plus robuste avec des technologies d'assistance anciennes.
Mise en oeuvre : `tabindex="-1"` sur le `<main id="main">` des 7
gabarits, plus la règle `main:focus { outline: none; }` dans
[base.css](../src/main/resources/static/css/base.css), car le contour
global `:focus-visible` apparaissait sinon autour de toute la région de
contenu après activation au clavier (vérifié en navigateur ; `main`
n'étant pas un élément interactif, WCAG 2.4.7 ne s'applique pas).
3. **Reste à faire** (suivi dans
[#84](https://github.com/ebouchut/learn-dev/issues/84)) : test de fumée
avec un lecteur d'écran (VoiceOver), et re-passage de cet audit après
tout changement de palette ou ajout de page (les commandes de la section
« Méthode et outils » rendent le re-passage reproductible).
10 changes: 7 additions & 3 deletions docs/rgaa.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ how learn-dev fulfills it, and where.
the computed `aria-describedby` associations.
- **Responsive**: screenshots at 375, 768, and 1440 px; no horizontal page
scroll (`scrollWidth == viewport`).
- **Self-audit report** (dossier appendix, French): Lighthouse and
axe-core on every page (100/100, zero violations) plus the keyboard
walkthrough, with the commands to reproduce it, in
[rgaa-audit.md](rgaa-audit.md).
- **Remaining manual passes** (tracked in
[#84](https://github.com/ebouchut/learn-dev/issues/84)): keyboard
walkthrough, Lighthouse/axe on each page, a VoiceOver smoke test, and a
contrast re-run after any palette change.
[#84](https://github.com/ebouchut/learn-dev/issues/84)): a VoiceOver
smoke test, and a re-run of [rgaa-audit.md](rgaa-audit.md) after any
palette change or new page.
6 changes: 6 additions & 0 deletions src/main/resources/static/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ code {
top: 0;
}

/* The skip link moves focus onto main (tabindex="-1"); no ring on a
non-interactive landmark (WCAG 2.4.7 applies to interactive controls) */
main:focus {
outline: none;
}

/* ---------- Site header ---------- */

.site-header {
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<body>
<th:block th:replace="~{fragments/layout :: header('dashboard')}"></th:block>

<main class="site-main" id="main">
<main class="site-main" id="main" tabindex="-1">
<h1 class="page-title">
Welcome back, <strong sec:authentication="name">user</strong> 👋
</h1>
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/forgot-password.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<body>
<th:block th:replace="~{fragments/layout :: header('login')}"></th:block>

<main class="site-main site-main--narrow" id="main" th:object="${form}">
<main class="site-main site-main--narrow" id="main" tabindex="-1" th:object="${form}">
<!-- Always the same message, whatever the email (enumeration-safe). -->
<p class="alert alert--success" role="status" th:if="${param.sent}">
If an account exists for this address, a reset link is on its way.
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<body>
<th:block th:replace="~{fragments/layout :: header('home')}"></th:block>

<main class="site-main" id="main">
<main class="site-main" id="main" tabindex="-1">
<section class="hero" aria-labelledby="hero-title">
<div>
<h1 class="hero__title reveal" id="hero-title">
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<body>
<th:block th:replace="~{fragments/layout :: header('login')}"></th:block>

<main class="site-main site-main--narrow" id="main">
<main class="site-main site-main--narrow" id="main" tabindex="-1">
<p class="alert alert--success" role="status" th:if="${param.registered}">
Account created, please log in.
</p>
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/privacy.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<body>
<th:block th:replace="~{fragments/layout :: header(null)}"></th:block>

<main class="site-main" id="main">
<main class="site-main" id="main" tabindex="-1">
<article>
<header>
<h1>Politique de confidentialité</h1>
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<!-- th:object on main puts the form-backing bean in scope for the whole
page, so #fields works in the alert ABOVE the form too. -->
<main class="site-main site-main--narrow" id="main" th:object="${form}">
<main class="site-main site-main--narrow" id="main" tabindex="-1" th:object="${form}">
<p class="alert alert--error" role="alert" th:if="${#fields.hasAnyErrors()}">
Your registration could not be completed. Check the highlighted fields below.
</p>
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/reset-password.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<body>
<th:block th:replace="~{fragments/layout :: header(null)}"></th:block>

<main class="site-main site-main--narrow" id="main" th:object="${form}">
<main class="site-main site-main--narrow" id="main" tabindex="-1" th:object="${form}">

<!-- Invalid, expired, or already used link -->
<th:block th:if="${!tokenValid}">
Expand Down