Limites connues
Ce que le moteur n’évalue pas, ce qui dépend de l’endroit où une analyse s’exécute (jsdom, un navigateur, ou les scripts de la page elle-même), et ce qui demande une revue par une personne. Certains points sont des choix de conception, chacun avec sa justification ; chacun indique ce qu’une analyse ne peut pas établir et, quand il existe, ce qu’il faut faire. Lisez ceci avant de vous fier à un résultat sans constat.
Que faire
| Si… | Alors… |
|---|---|
| Un contenu n’apparaît qu’après un clic, un survol ou un chargement de données | Amenez vous-même la page dans cet état, puis analysez-la. Analysez chaque état qui compte. |
| Une vérification a besoin de la mise en page : taille des cibles, espacement du texte, contraste par rapport à ce qui est peint sous le texte | Analysez dans un vrai navigateur. jsdom n’a pas de mise en page (la propriété engine.environment.layout du résultat vaut false) : ces règles y mesurent donc moins, ou renvoient notApplicable, comme l’indiquent les sections ci-dessous. |
| Une exigence porte sur l’utilisation au clavier, comme un piège au clavier | Testez-la au clavier. Aucune analyse ne la couvre. |
Un résultat est cantTell | Suivez les étapes de howToCheck de l’occurrence, et tranchez à la main. uncertainty dit ce qui manquait, et lean vers où pointent les indices de la page (voir le schéma de sortie). |
| La page est responsive | Analysez à chaque largeur qui vous importe. |
| Deux exécutions sur la même page diffèrent | Comparez engine.version et engine.environment (viewport, polices et images encore en cours de chargement), et rechargez la page entre les analyses. |
| Une analyse ne rapporte aucun échec | Ce n’est pas une déclaration de conformité. Voir Correspondance de conformité WCAG. |
Les sections ci-dessous en donnent les raisons.
Hors de ce qu’une analyse peut voir
Une analyse lit la page à un moment donné, dans un seul état, à un seul viewport : le DOM, ses styles calculés et, dans un vrai navigateur, sa mise en page. Elle n’utilise pas la page comme le fait une personne. Elle ne la parcourt pas au clavier, n’ouvre pas ses menus, n’attend pas ses données et ne la redimensionne pas. (Dans un navigateur, quelques règles donnent le focus à un élément ou ajoutent une feuille de style le temps de leur vérification, puis rétablissent la page ; voir la section suivante.) Les points ci-dessous demandent ce type d’interaction dans la durée : une règle de plus ne peut donc pas les couvrir. Ils demandent un test d’interaction ou une personne :
- Détection des pièges au clavier (WCAG 2.1.2) : il faut déplacer le focus dans la page et observer où il arrive, ce qu’aucune lecture ponctuelle du DOM ne permet. Une technique viable a été étudiée (voir la section sur les pièges au clavier de
ACT_RULE_MAPPING.md), mais elle n’est pas implémentée : une analyse n’explore pas les interactions des utilisateurs ni les états auxquels elles mènent, et aucune des vérifications qui modifient brièvement la page ne lui fait parcourir le focus comme le ferait la détection des pièges. En attendant, rien dans le balisage statique ne peut la remplacer. - Redistribution / rognage au zoom (WCAG 1.4.10) : il faut une mesure réelle de la mise en page (
clientWidth/scrollWidth) dans un viewport simulé équivalent à 320 px. Contrairement à certaines heuristiques fondées sur les déclarations CSS ailleurs dans ce moteur, il n’existe aucun indicateur dans le balisage statique pour « le contenu est-il rogné à un zoom de 400 % ». - État dynamique / après interaction : tout ce qui n’existe qu’après un clic, un survol ou un chargement de données asynchrone (le contenu d’une modale, les options d’une liste déroulante, les messages d’erreur de validation d’un formulaire) est invisible pour une analyse du DOM actuel de la page. Si votre framework le rend d’emblée (même hors écran ou
hidden), il peut être analysé ; s’il n’existe qu’après une interaction, il ne le peut pas, à moins que vous ne déclenchiez vous-même cette interaction avant l’analyse (par exemple, cliquer sur le bouton, puis analyser). Analyser l’état ouvert vérifie ce qu’il contient, pas si une personne qui utilise le clavier ou un lecteur d’écran peut l’ouvrir, l’utiliser et le refermer.
Dépendantes de l’environnement : selon la façon dont vous l’exécutez
- jsdom (Node, sans vrai navigateur) n’a pas de moteur de mise en page CSS. Les règles qui ont besoin d’une géométrie réelle, notamment
target-size-minimum(WCAG 2.5.8, qui nécessite un vraigetBoundingClientRect()), renvoientnotApplicablesous jsdom seul plutôt que de deviner. Exécutez l’analyse dans un vrai navigateur (Puppeteer/Playwright, voir le Pattern 2 deINTEGRATION.md) pour obtenir de vrais résultats de ces règles. - Savoir si un texte passe à la ligne nécessite la mise en page ; les résultats d’espacement du texte sur un texte qui ne passe pas à la ligne sont donc signalés pour revue. Le WCAG 1.4.12 et les règles ACT associées ne s’appliquent qu’à un texte contenant une possibilité de retour à la ligne, ce qu’une analyse statique ne peut pas établir.
avoid-inline-spacingconsidère par défaut que le texte passe à la ligne : une valeur forcée ordinaire inférieure au seuil échoue donc toujours ; lorsqu’aucun retour à la ligne n’est possible pour une raison qui est visible sans mise en page (texte non autorisé à passer à la ligne, ou élément à largeur fixe dans un ancêtre à défilement horizontal), elle renvoiecantTellà la place. Un texte qui ne passe jamais à la ligne pour une autre raison est toujours signalé comme un échec. - Le moteur s’exécute sur les objets natifs JavaScript de la page elle-même. Une page dont les scripts cassent l’un de ceux sur lesquels il s’appuie (
Array.prototype.filter,Map,JSON.stringify,String.prototype.toLowerCaseet d’autres) arrête l’analyse avec une erreur de codePAGE_BUILTINS_BROKENqui nomme chacun d’eux, plutôt que de donner un résultat qui paraît correct et qui est faux ; un objet cassé avant l’injection du bundle (Object.keys) empêche le chargement du bundle. Une page qui ajoutethenàObject.prototypeest analysée comme n’importe quelle autre. Les méthodes du DOM qu’une page remplace (getComputedStyle,querySelectorAll,getBoundingClientRect) ne sont pas vérifiées : les règles lisent ce qu’elles renvoient. - Un nom n’est pas calculé à partir d’un contenu imbriqué sur plus de 256 niveaux. Le parcours s’arrête là (Chrome ne donne aucun nom à partir d’un contenu situé 100 niveaux plus bas) : pour un élément dont le seul nom se trouve plus profond,
button-name-present,link-name-present, les règles de nom des éléments de menu, options, éléments summary, onglets, infobulles et éléments d’arbre, ainsi queempty-heading, posent la question (name_contentTooDeep) ; les autres règles qui calculent un nom à partir du contenu lisent un tel contenu comme vide. - Le contenu d’une shadow root fermée ne peut pas être lu. Aucun script n’accède à l’intérieur d’un
attachShadow({ mode: 'closed' })ou d’unshadowrootmode="closed"déclaratif : le moteur ne peut donc pas voir ce qu’un navigateur en tire comme nom.button-name-presentetlink-name-present, ainsi que les règles de nom des éléments de menu, options, onglets, éléments d’arbre, éléments summary et infobulles, posent la question pour un élément dont le nom pourrait provenir d’un élément personnalisé qui s’affiche sans montrer de contenu lisible, au lieu de le faire échouer. Une racine fermée sur un élément intégré (un<span>) ressemble à une racine vide, comme une icône dessinée en CSS : un bouton qui en contient une échoue donc toujours. Vérifiez à la main ce qu’expose un tel composant, par exemple avec un lecteur d’écran. - Un nom ou un état qu’un élément personnalisé définit par
ElementInternalsne peut pas être lu.this.attachInternals().ariaLabel = 'Save'nomme l’élément dans tous les navigateurs, mais aucun script ne peut le relire. Quand un composant conserve ses internals dans une propriété (_internals,internals,internals_,__internals), le moteur les lit par elle, et fait réussir ou échouer l’élément selon ce qu’ils indiquent. Pour tout autre élément personnalisé défini sans nom (ou sans état requis), les règles de nom etaria-required-attrposent la question (name_internals,state_internals) au lieu de le faire échouer. Vérifiez ces éléments dans l’arbre d’accessibilité du navigateur, ou installez le hook optionnel des internals avant l’exécution des scripts de la page :installInternalsHook()dans chaque binding (voir Premiers pas) et dans test-matchers, ouinternalsHookScript()de core pour une page à vous (voirINTEGRATION.md, qui montre où l’installer). Le hook enregistre les internals de chaque élément pour l’analyse : il signale donc aussi un élément qui n’en a jamais attaché. Un rôle défini uniquement par les internals (internals.role) est lu là où se trouvent les internals, par une propriété ou par le hook, comme rôle par défaut de l’élément ; conservé dans un champ privé sans le hook, il n’est pas vu, et l’élément ne fait pas l’objet d’une question, puisque presque tous les composants pourraient en avoir un. Les éléments intégrés, et les éléments personnalisés non définis, sont jugés comme avant. - Les gestionnaires d’événements ajoutés par script ne peuvent pas être listés. Un gestionnaire ajouté avec
addEventListener, ou défini comme propriété (el.onclick = fn), ne laisse rien dans le balisage et aucun script ne peut le relire : les règles de clavier (click-handler-keyboard-operable,mouse-only-event-handlers) ne jugent donc que les attributs de gestionnaire en ligne : unrole="button"focalisable dont le gestionnaire de clic est ajouté par script n’est pas signalé (un élément qui ne peut pas recevoir le focus fait tout de même l’objet d’une question dewidget-keyboard-focusable), un gestionnaire de clavier ajouté par script ne peut pas justifier unonclicken ligne, etpassword-paste-enabledne peut pas savoir si un gestionnaire de collage ajouté par script remet le texte qu’unonpasteen ligne annule. Installez le hook facultatif des gestionnaires (listenerHookScript()) avant l’exécution des scripts de la page : les règles jugent alors aussi les gestionnaires qu’il enregistre et, là où il n’a vu aucun gestionnaire de clavier, font échouer un contrôle qu’Entrée et Espace ne peuvent pas actionner. Un gestionnaire de clic qu’un framework ajoute à son élément racine sert des éléments que rien ne désigne : même avec le hook, ceux-là sont jugés sur leur balisage. Où l’installer :INTEGRATION.md, sur GitHub. - La façon dont une région live a été remplie ne se voit pas après coup. Un message d’état est annoncé quand une région live change alors que les technologies d’assistance peuvent l’atteindre (4.1.3), mais une analyse voit la page à un instant donné. Sans aide,
live-region-announcementsne juge que ce qui est là : une région live masquée aux technologies d’assistance pararia-hiddenouinert, ou derrière un<dialog>modal ouvert, ou laissée enaria-busy. Avec le hook facultatif des régions live (liveRegionHookScript()), elle pose aussi la question pour une région que la page a ajoutée avec son texte déjà dedans, ou affichée et remplie d’un coup, etgetAnnouncements()liste ce que la page a rendu disponible à l’annonce. Aucun des deux ne dit ce qu’un lecteur d’écran a prononcé : cela dépend du lecteur d’écran. Le hook ne voit pas à l’intérieur des shadow roots. Détails :INTEGRATION.md, sur GitHub. - Une analyse faite pendant qu’une boîte de dialogue modale est ouverte voit la boîte de dialogue, pas la page. Un
<dialog>ouvert avecshowModal()rend inerte le reste du document, et les navigateurs ne présentent que la boîte de dialogue aux technologies d’assistance. Une boîte de dialogue ouverte avec l’attributopenoushow()n’est pas modale, même avecaria-modal="true": la page derrière reste atteignable, et le moteur la juge. Sous jsdom, qui n’a pas de top layer, un<dialog aria-modal="true">ouvert est au contraire considéré comme modal, ce que le moteur peut établir de plus proche là. Le moteur suit ce comportement : le contenu derrière la boîte de dialogue est écarté de toutes les règles, comme tout autre contenu masqué, et les règles portant sur la structure de la page elle-même (page-has-heading-one,landmark-one-main,bypass-blocks-present,region) renvoientnotApplicable, puisque l’analyse ne peut pas voir cette structure. Analysez à nouveau la page avec la boîte de dialogue fermée pour juger ce qu’il y a derrière. Une modale personnalisée, un élément affiché avecrole="dialog"ourole="alertdialog"etaria-modal="true", rend aussi ces règles de structurenotApplicable;aria-modalne rend rien inerte, donc toutes les autres règles jugent toujours la page derrière. Une boîte de dialogue modale dans un shadow root ouvert compte comme une boîte de dialogue du document ; une boîte de dialogue dans un shadow root fermé ne peut pas être trouvée. Une modale personnalisée sansaria-modal="true"n’est traitée qu’à travers leinertou learia-hiddenque la page met sur le reste, qui masquent la page sans indiquer aux règles de structure qu’une boîte de dialogue est ouverte. <dialog>et les autres éléments masqués par la feuille de style par défaut du navigateur (sans attributopen,display: noneselon la spécification), ainsi que tout autre sous-arbre masqué viadisplay:none,visibility:hidden,[hidden]ou un<details>fermé, et un sous-arbre[inert](que les règles portant sur l’apparence du contenu, comme le contraste et l’espacement du texte, jugent quand même, puisqu’il est dessiné), sont exclus de l’évaluation des règles par défaut. C’est un choix par défaut délibéré (engineOptions.includeHiddenElements: false), pas un oubli : un contenu masqué n’est pas accessible aux technologies d’assistance ni au clavier tant qu’il n’est pas affiché, si bien que signaler par défaut un défaut de balisage qu’il contient serait souvent du bruit. DéfinissezengineOptions.includeHiddenElements: truepour évaluer malgré tout les sous-arbres masqués ou repliés, par exemple pour repérer un défaut de balisage (comme une référence d’ID ARIA cassée) avant même l’ouverture d’une boîte de dialogue. VoirENGINE_OPTIONS.mdpour l’option et la liste exacte des mécanismes de masquage qu’elle couvre.- Un
aria-detailsqui désigne un contenu masqué n’est pas signalé.aria-valid-attr-valuevérifie que l’id désigne un élément, pas que l’élément est affiché. Chromium écarte la relation de son arbre d’accessibilité tant que l’élément est masqué et la rétablit dès qu’il est affiché : des détails qui s’ouvrent à la demande sont donc annoncés une fois ouverts, et les WCAG n’en demandent pas plus. Pour vérifier la relation telle que la reçoivent les technologies d’assistance, analysez la page avec les détails affichés. - jsdom n’a pas de popovers. Il intègre la règle HTML qui masque un
[popover]fermé, mais passhowPopover(), et lit:popover-opende façon incohérente. Là où le DOM n’a pas deshowPopover, le moteur traite tout[popover]autre qu’un<dialog>ouvert comme fermé et masqué : son contenu n’est donc pas vérifié ; un popover que le CSS de la page garde visible n’est pas vérifié non plus sous jsdom. Dans un navigateur, le moteur lit ce qui est affiché. Pour vérifier le contenu d’un popover, analysez la page dans un navigateur avec le popover ouvert. - Sous jsdom, le texte d’un
contentCSS ne nomme rien. jsdom ne calcule aucun style pour::beforeet::after: un bouton ou un lien dont le seul texte est généré (content: "Save",content: attr(data-label)) a un nom dans un navigateur et n’en a pas sous jsdom, oùbutton-name-present,link-name-presentet les autres règles de nom le font échouer. Analysez ces pages dans un navigateur. - jsdom conserve une taille de police telle qu’elle est écrite. Un navigateur renvoie une taille de police calculée en px ; jsdom conserve un mot-clé ou un
calc()tel qu’il est écrit. Le moteur lit les mots-clés aux tailles d’un navigateur pour une taille par défaut de 16px (xx-largevaut 32px) ainsi que lescalc()de longueurs et de nombres, si bien que le texte de grande taille est distingué comme dans un navigateur.largeretsmaller, qui dépendent de la taille du parent, ne sont pas résolus sous jsdom : un tel texte est soumis au rapport du texte normal, et son message indique une taille de 0px. Un navigateur résout les deux. - jsdom ignore les feuilles de style propres à un shadow root. Constaté avec jsdom 29.1.1 : ni un
<style>dans un shadow root ni sesadoptedStyleSheetsn’atteignent le style calculé de ses éléments, si bien que sous jsdom les couleurs, les tailles, et undisplay: noneou unvisibility: hiddenqui y sont définis ne sont pas vus. Les règles jugent alors le contenu d’un composant comme s’il n’avait pas de style : le contraste est mesuré sur les couleurs par défaut, et un contenu que son CSS masque est jugé comme affiché. Seule exception : la couleur d’un lien, que les helpers de contraste calculent eux-mêmes à partir des feuilles du shadow root. Analysez dans un navigateur les pages construites avec des composants web stylés. - Le
text-shadowcalculé par jsdom n’est pas fiable à la deuxième lecture d’un même élément. Confirmé avec jsdom 29.1.1 : lire une deuxième fois la valeur calculée detext-shadowsur le même élément (par n’importe quel accesseur, depuis n’importe quelleCSSStyleDeclarationnouvellement obtenue pour cet élément, cache ou non) renvoie silencieusement une valeur différente et fausse, « pas d’ombre », au lieu de la vraie valeur déclarée. La première lecture est toujours correcte. Ce moteur contourne le problème en interne en lisant letext-shadowde chaque élément une seule fois par exécution et en mettant le résultat en cache (voir__textShadowInfoEldanssrc/core/contrast-helpers.js), de sorte qu’une analyse n’est pas affectée. Le problème n’apparaît que si vous lisez vous-mêmegetComputedStyle(el).textShadowplus d’une fois sur le même élément analysé par jsdom ; un vrai navigateur n’a pas ce bug. - Une deuxième analyse du même DOM jsdom peut perdre un style qui surcharge une propriété raccourcie. jsdom conserve le style calculé d’un élément une fois lu, jusqu’à la prochaine modification du DOM, et la copie qu’il renvoie depuis ce cache perd une propriété détaillée qui surcharge une propriété raccourcie venant d’une règle moins spécifique : avec
*{background:#888}etstyle="background-image:url(hero.png)", la première lecture donne l’image et toutes les suivantesnone. Une analyse garde le style lu en premier et n’est donc pas affectée, mais une deuxième analyse du même DOM inchangé peut manquer l’image et valider un texte sur lequel la première posait la question (contrast-computable). Analysez un nouveau parsing, ou modifiez le DOM entre les analyses, ce qui vide le cache de jsdom ;@surea11y/core/testingle fait pour sa propre deuxième analyse. Un vrai navigateur n’a pas ce bug. - Sous jsdom, le temps d’analyse croît avec le carré de la profondeur du DOM, et non avec le nombre d’éléments. jsdom résout le CSS hérité en parcourant la chaîne des ancêtres d’un élément à chaque appel de
getComputedStyle, donc un appel par élément coûte O(éléments x profondeur). Mesuré sur jsdom 29.1.1 sans aucun code du moteur : 4000 éléments dans une seule chaîne prennent 8,3 s rien qu’engetComputedStyle, contre 0,25 s pour les mêmes 4000 éléments placés côte à côte. Les parcours d’ancêtres du moteur lui-même sont plafonnés et restent linéaires : ce coût vient donc de jsdom, pas des règles. Il concernerunDomRulesInPageet tout ce qui repose dessus, y compris@surea11y/test-matchers; un vrai navigateur calcule le style hérité nativement et ne présente pas ce comportement. Les frameworks de composants imbriquent couramment sur 100 à 300 niveaux, ce qui reste largement rapide ; cela devient sensible au-delà d’environ 1000. - Sous jsdom, le temps d’analyse croît aussi avec la taille de la feuille de style. jsdom calcule un style en confrontant chaque règle CSS à l’élément. Le moteur ne modifie pas le DOM pendant une analyse, donc chaque élément n’est calculé qu’une fois, mais cette fois coûte O(règles) : sur une page d’actualités enregistrée (7 916 éléments, 2 683 règles CSS, jsdom 29.1.1), le premier calcul de 5 735 éléments prend environ 224 s d’une analyse de 243 s. Ce coût vient de jsdom ; un vrai navigateur calcule la même page en une fraction de seconde. jsdom laisse aussi les propriétés personnalisées non substituées, si bien qu’une couleur définie avec
var()y est illisible : le texte ou l’arrière-plan fait l’objet d’une question (contrast-computable) au lieu d’être mesuré. Utilisez un vrai navigateur pour les grandes pages de production. - Sous jsdom, la couleur d’un lien est calculée à partir des règles de la page. jsdom classe son propre
:link { color: #0000EE }avec les règles de la page selon la seule spécificité, si bien que lea { color: #fff }d’une page perd face à lui, alors qu’un navigateur applique la règle de la page. Sous jsdom, le moteur calcule la couleur d’un lien, et du texte qu’il contient, à partir des règles de la page comme le fait la cascade d’un navigateur ; un lien qu’aucune règle de la page ne colore garde la couleur de lien du navigateur, et un lien est considéré comme non visité. Là où ce n’est pas possible (une feuille de style illisible, unvar(), une règle sous@supports,@layer,@containerou imbriquée dans une autre), le texte fait l’objet d’une question (contrast-computable,FOREGROUND_UNPARSABLE) au lieu d’être mesuré dans le bleu de jsdom. Dans un navigateur, la couleur calculée est lue telle quelle. - Sous jsdom, le texte est jugé sur son
font-sizecalculé. Dans un navigateur, les règles de contraste etp-as-headingjugent le texte à la taille où il est dessiné,zoomCSS, transformation SVG etfont-size-adjustcompris, ce dernier d’après les métriques de la police (font-size: 24px; font-size-adjust: 0.3dessine Arial à environ 14px, ce qui n’est pas du texte de grande taille). jsdom ne met rien en page et ne lit aucune police : lefont-sizey est pris tel quel. Analysez dans un navigateur une page qui met ainsi son texte à l’échelle. - Dans un vrai navigateur, certaines règles modifient la page le temps de leur vérification, et les scripts de la page peuvent réagir. Pour mesurer ce que dessine le navigateur,
css-hidden-focusdonne le focus aux éléments comme le ferait le clavier, et désactive les transitions sur l’élément qu’il mesure ;aria-hidden-focusdonne le focus à un élément situé dans un sous-arbrearia-hiddenpour voir si la page déplace aussitôt le focus ailleurs (une sentinelle de piège à focus) ;text-spacing-content-lossajoute une feuille de style avec l’espacement de WCAG 1.4.12 ; les règles de contraste ajoutent un frame vide hors de l’écran pour lire les couleurs propres du navigateur pour un placeholder ou un<select>déroulant. L’analyse amène aussi chaque animation en cours à un point fixe pendant l’exécution des règles (une animation finie à sa fin, une animation infinie à son début), si bien qu’un fondu d’apparition est mesuré une fois stabilisé. Tout cela est rétabli avant que la règle ou l’analyse ne se termine : le focus, la position de défilement, les attributs style, les animations, la feuille de style ajoutée et le frame. Ce qui ne peut pas être rétabli, c’est ce que font les scripts de la page en réaction, puisque donner le focus à un élément déclenche ses gestionnairesfocusetfocusin: un carrousel peut passer à la diapositive focalisée, en déplaçantaria-hiddenavec elle, ou activer un bouton. Les résultats de cette analyse sont ceux de la page telle qu’elle a été trouvée :aria-hidden-focuslit quels sous-arbres sont masqués, ainsi que le balisage qu’il rapporte, avant de donner le focus à quoi que ce soit. Mais la page reste dans l’état où ses scripts l’ont mise, si bien qu’une deuxième analyse de la même page peut trouver autre chose : si vous analysez une page que vous continuez d’utiliser, ou analysez deux fois la même page, rechargez-la entre-temps. - Une feuille de style d’une autre origine ne peut pas être lue depuis la page. Le script d’une page ne peut pas lister les règles d’une feuille servie par une autre origine (un CDN, un service de polices) :
css-orientation-lockpose donc la question (STYLESHEETS_NOT_READABLE), etcss-hidden-focusetcss-focus-indicator-suppressedne peuvent pas y voir une règle de focus. Passez son texte dansengineOptions.styleSheetTexts:preloadStyleSheets()récupère celles que le serveur partage (CORS), et un outil qui pilote le navigateur peut récupérer les autres depuis l’extérieur de la page (voirINTEGRATION.md, sur GitHub). - Balisage statique et état du DOM en direct / après hydratation. La logique des règles elle-même ne dépend pas de la provenance du DOM : elle évalue le DOM qu’on lui fournit, qu’il s’agisse de HTML statique analysé par jsdom (Pattern 1) ou d’un onglet de navigateur réel déjà chargé et hydraté (Pattern 2, voir
INTEGRATION.md). En revanche, la CLI (npx @surea11y/cli scan <url>) ne récupère que du HTML statique, sans exécuter de JS ; voir la documentation de la CLI. Pour un widget hydraté par un framework JS dont le balisage rendu côté serveur livre volontairement un état avant que le JS client ne le synchronise (par exemple<input type="checkbox" aria-checked="true">livré avant que le JS client ne définisse la propriété nativecheckeden conséquence lors de l’hydratation, un motif extrêmement courant et tout à fait légitime), une analyse CLI ne voit que le balisage d’avant l’hydratation. Une analyse exécutée dans un véritable onglet de navigateur chargé voit au contraire l’état après hydratation ; les deux peuvent donc diverger précisément sur cette catégorie d’éléments, pour des raisons sans aucun rapport avec l’exactitude des règles. C’est pourquoiaria-checked-state-mismatchest plafonnée àmanual/cantTellplutôt qu’à unfailstrict. Si vous avez besoin de la précision du DOM en direct pour les vérifications sensibles à l’hydratation, exécutez la bibliothèque directement sur une page déjà chargée via le Pattern 2, et non via la CLI qui récupère du HTML statique. - Une mise en page est mesurée à un seul viewport, avec les polices dont la page dispose à ce moment-là. Les règles qui mesurent ce que dessine le navigateur,
text-spacing-content-loss,target-size-minimumetskip-link, valent pour le viewport auquel la page a été analysée : une page responsive peut couper du texte, ou rétrécir une cible, à une largeur et pas à une autre, et un texte mesuré avant le chargement de sa police web n’a pas la même largeur. Le résultat enregistre les deux dansengine.environment, et les constats de ces règles reprennent leur viewport et indiquent ce qui a été mesuré (de combien le texte a dépassé le bord, la taille d’une cible et sa proximité avec sa voisine) : un constat qui diffère d’une exécution à l’autre peut ainsi être rattaché aux conditions. Analysez à chaque largeur qui vous importe, et attendez que la page soit stable avant d’analyser :waitForPageReady()attend l’événement load, les polices web et les images (voirINTEGRATION.md, qui montre comment l’appeler avant une analyse), et le résultat enregistre dansengine.environmentsi des polices ou des images étaient encore en cours de chargement. - Dans un navigateur, le contraste est mesuré par rapport à ce qui est peint sous le texte, tel que la page se présente à ce moment-là. Un texte sur une image, un dégradé, ou un rendu qui n’en couvre qu’une partie fait l’objet d’une question (
contrast-computable,reasonCodeBACKGROUND_OVERLAP) au lieu d’être mesuré. Un texte qu’une boîte enoverflow: hiddengarde entièrement hors de vue, comme une diapositive de carrousel hors de son rail, est écarté tant qu’il n’est pas visible : analysez la page dans cet état pour le vérifier. Les boîtes fixes et sticky au-dessus du texte sont ignorées. Sous jsdom, le texte est mesuré par rapport à ses seuls ancêtres. Analysée en couleurs forcées (contraste élevé de Windows, ouforced-colors: activeémulé), la page est maintenue dans ses propres couleurs pendant la mesure : les résultats sont donc ceux obtenus sans couleurs forcées, sauf si le CSS de la page réagit aux couleurs forcées (une media queryforced-colors, ou une couleur système commeGrayText), ce que l’analyse signale dansengine.environment.forcedColorsCss; là où ce n’est pas possible (une Content Security Policy qui bloque le<style>du moteur dans un navigateur qui ne sait pas adopter une feuille de style), les couleurs mesurées sont en partie celles du navigateur, et chaque occurrence de contraste le signale (details.forcedColors). Le fonctionnement de la mesure, étape par étape, est décrit dansLAYOUT_MEASUREMENT.md, sur GitHub. - Le texte d’une page sans arrière-plan est mesuré sur le canevas du schéma de couleurs de la page, là où ce canevas est connu. Le navigateur peint son canevas derrière une page qui ne peint aucun arrière-plan opaque, dans la couleur
Canvasdu schéma de couleurs dans lequel la page est dessinée, que décide lecolor-schemede la page : blanc pour une page qui n’en déclare aucun, quelle que soit la préférence de l’utilisateur, et le canevas sombre pour une page dessinée en sombre. Les règles de contraste y mesurent le texte dans les deux modes. Non mesurés, et objets d’une question decontrast-computable(BACKGROUND_NOT_OPAQUE_AT_ROOT) : le texte dans un frame, dont le canevas peut laisser voir la page qui l’entoure, et le texte en couleurs forcées, où le canevas est la palette de l’utilisateur. - Sous jsdom, le texte est mesuré dans les couleurs du schéma où la page est dessinée là où elles peuvent être calculées, et fait l’objet d’une question sinon. jsdom calcule les couleurs du schéma clair et n’applique aucune règle
prefers-color-scheme. Sur une page dessinée en sombre (color-scheme: darksur la racine ou dans<meta name="color-scheme">), le texte que colore le schéma prend les couleurs du schéma sombre (celles de Chromium), et une couleur de texte donnée enlight-dark(),CanvasText,LinkTextouCanvasest calculée pour le sombre. Le texte dont les couleurs dans ce schéma ne peuvent pas être calculées fait l’objet d’une question (contrast-computable,COLOR_SCHEME_NOT_RENDERED;link-in-text-block,COLOR_NOT_COMPUTABLE) au lieu d’être mesuré dans les couleurs claires. Dans un navigateur, les couleurs sont celles qu’il dessine. - Sur une page qui prend en charge le clair et le sombre, le schéma que l’analyse ne voit pas n’est mesuré que là où les couleurs du texte ne peuvent pas changer avec lui. Une analyse voit le schéma que préfère le navigateur (jsdom : clair). Le texte sur le canevas est aussi mesuré dans l’autre schéma, sauf là où une règle
prefers-color-scheme, une valeurlight-dark()ou une couleur système que définit la page atteint la couleur du texte ou un arrière-plan derrière lui, ou là où une feuille de style est illisible ; ni le texte dessiné par un contour, une ombre,::first-lineou un pseudo-élément, ni le texte mesuré sur une autre peinture.data.details.colorSchemesCheckedliste les schémas mesurés ; analysez à nouveau avec le navigateur dans l’autre schéma pour le vérifier. - Le texte généré n’est mesuré que dans un navigateur, et seulement là où il peut être lu. Le texte qu’écrit le
contentd’un::beforeou d’un::after, et le::markerd’un élément de liste, sont mesurés dans la couleur et la police du pseudo-élément ; un contenu illisible (un compteur dans un@counter-stylepropre à la page), et un::beforeou un::afterpositionné hors du flux sans arrière-plan propre, font l’objet d’une question (contrast-computable) à la place. Les marqueurs ne sont recherchés que sur<li>. Sous jsdom, qui ne calcule aucun style de pseudo-élément, le texte généré est écarté. - Le texte et le placeholder d’un champ de formulaire sont mesurés tels que le champ les dessine, avec des lacunes. Le texte que le navigateur dessine dans un champ de date ou d’heure vide, ainsi que le bouton et le nom de fichier d’un champ de fichier, ne sont pas mesurés. Un
<select>déroulant est mesuré tel que le contrôle fermé le dessine, son option affichée dans les couleurs du select ; une autre de ses options avec ses propres couleurs n’apparaît que dans la liste que la plateforme dessine à l’ouverture, et donnecantTell(SELECT_OPTION_COLOR). Un placeholder sous son rapport échoue (PLACEHOLDER_BELOW_THRESHOLD) quand la page a choisi ses couleurs, respecte le rapport quand elles sont toutes celles du navigateur (technique G148), et donnecantTellquand on ne peut pas le savoir. Sous jsdom, les placeholders ne sont pas mesurés du tout. Détails :LAYOUT_MEASUREMENT.md. target-size-minimumcalcule la zone atteignable à partir de la mise en page plutôt que par un test d’atteinte. Les cibles sous la ligne de flottaison ou dans une boîte défilante sont mesurées en entier, comme si elles étaient défilées jusqu’à être visibles ; les boîtes fixes et sticky au-dessus d’une cible sont ignorées ; les transformations 3D, la plupart des formes declip-pathet le rognage arrondi d’un ancêtre sont mesurés par leur boîte englobante ; et les liens séparés uniquement par des séparateurs (« Edit | Delete ») qui se gênent mutuellement font l’objet d’une question plutôt que d’être exemptés. Sous jsdom, la règle renvoienotApplicable. Détails :LAYOUT_MEASUREMENT.md.- Le contraste est calculé en sRGB. Les couleurs exprimées en
oklch(),oklab(),lab(),lch()oucolor()(y compris la forme calculée decolor-mix()et des couleurs relatives) sont converties en sRGB, comme un écran standard les affiche ; une couleur hors de sRGB, comme un rougedisplay-p3saturé, est ramenée à sa limite, ce que Chromium dessine sur un écran sRGB. Un écran à large gamut peut afficher cette couleur plus saturée que ne le suppose le rapport. Une couleur d’arrière-plan que le moteur ne sait pas convertir, uncolor()dans un espace qu’il ne connaît pas, n’est pas traitée comme transparente :contrast-computablepose la question pour ce texte avec lereasonCodeBACKGROUND_UNPARSABLE, et les autres règles de contraste l’écartent.
Non tentées : des jugements qu’on ne peut pas automatiser sans risque
Il n’existe pour ces cas aucune heuristique aussi sûre au niveau d’exigence de ce moteur (fail doit rester réservé aux violations déterministes, point final). Les implémenter malgré tout reviendrait soit à ne détecter presque rien (trop restrictif pour être utile), soit à risquer de vrais faux positifs (trop large pour être fiable) :
- « Ce texte de titre ou d’étiquette a-t-il du sens ? » Les titres et les étiquettes réels sont extrêmement variés et légitimement courts (« FAQ », « Nom », « Présentation » conviennent tous), donc rien dans le balisage ne permet de décider si un titre décrit la section qu’il introduit ou si une étiquette décrit le champ à côté d’elle. Ce qui est décidable, c’est que certaines chaînes ne peuvent rien décrire :
heading-qualityetform-control-label-qualitysignalent les textes d’exemple oubliés, les emplacements de gabarit numérotés, les noms de fichiers et les URL à partir de listes de correspondances exactes, le même compromis précision plutôt que rappel que faitlink-name-quality. Ce sont toutes deux des règlesmanualplafonnées àcantTell: elles proposent un candidat à la revue, elles n’affirment jamais que le texte est incorrect. - « Ce message d’erreur décrit-il le problème ? » Ce qui déclenche une erreur de validation et son contenu dépendent presque toujours du JS ou d’une bibliothèque de validation, invisibles pour une analyse statique dès le départ ; ce n’est pas seulement un problème de conception d’heuristique.
- Sous-vérifications fines sur les médias temporels (WCAG 1.2.x compte 13 règles ACT sans règle correspondante ici ; voir la section des lacunes de
ACT_RULE_MAPPING.md) : le contenu audio/vidéo lui-même est fondamentalement invérifiable à partir du balisage statique ; les deux cas les plus larges et les plus sûrs sont couverts (media-alternative-transcript-evidence,video-caption), les plus spécifiques ne le sont pas, par choix. - Analyse du contenu des textes sous forme d’image (WCAG 1.4.5/1.4.9) : savoir si une image contient du texte tient à ses pixels, ce qui demanderait une compréhension d’image équivalente à de l’OCR.
image-of-textne pose la question que là où le balisage montre un indice : un<input type="image">, et un<img>affichant une data URL SVG dont le balisage contient<text>. La photo d’une phrase, une bannière au texte incrusté ou une image d’arrière-plan CSS contenant du texte ne donnent aucun indice, et ne font l’objet d’aucune question. Le 1.4.9, qui n’admet aucune exception, n’a pas de règle. - Activation par le mouvement (WCAG 2.5.4) : cas de niche, peu fréquent en pratique ; non prioritaire, mais pas structurellement impossible.
- Les deux cas limites ACT de
img-alt-decorative: un<img>dont la requête réseau n’est pas dans l’état « completely available » (encore en chargement, ou cassée), et un<canvas>entièrement transparent (rien n’y est réellement dessiné) sont tous deux exclus de l’applicabilité d’ACT. Aucun des deux n’est décidable par une analyse statique du DOM (pas de décodage d’image, pas de lecture des pixels du canvas) ; ils restent donc dans le périmètre plutôt que d’être exclus : un faux positif rare, qu’un humain écarte d’un coup d’œil, est plus sûr que de sous-signaler en silence un vrai élément exclu ou non décoratif.
Ce que cela signifie en pratique
L’essentiel de ce qui précède n’est pas propre à surea11y. Tout outil qui juge un seul état du DOM partage les limites décrites dans Hors de ce qu’une analyse peut voir, et la plupart des outils automatisés laissent aussi les jugements à une personne, même si les outils diffèrent par leur façon de s’exécuter, ce qu’ils vérifient et la façon dont ils rapportent ce qu’ils ne peuvent pas trancher. Si nous le disons explicitement ici, c’est pour une raison : un pass de ce moteur (ou de tout outil automatisé) ne remplace jamais la revue manuelle que les WCAG exigent elles-mêmes pour les critères ci-dessus. Voir WCAG_CONFORMANCE.md pour savoir exactement ce que le pass d’une règle affirme, et ce qu’il n’affirme pas, et pourquoi un critère WCAG dont toutes les règles renvoient pass reste cantTell.