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éesAmenez 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 texteAnalysez 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 clavierTestez-la au clavier. Aucune analyse ne la couvre.
Un résultat est cantTellSuivez 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 responsiveAnalysez à chaque largeur qui vous importe.
Deux exécutions sur la même page diffèrentComparez 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 échecCe 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 vrai getBoundingClientRect()), renvoient notApplicable sous jsdom seul plutôt que de deviner. Exécutez l’analyse dans un vrai navigateur (Puppeteer/Playwright, voir le Pattern 2 de INTEGRATION.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-spacing considè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 renvoie cantTell à 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.toLowerCase et d’autres) arrête l’analyse avec une erreur de code PAGE_BUILTINS_BROKEN qui 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 ajoute then à Object.prototype est 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 que empty-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’un shadowrootmode="closed" déclaratif : le moteur ne peut donc pas voir ce qu’un navigateur en tire comme nom. button-name-present et link-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 ElementInternals ne 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 et aria-required-attr posent 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, ou internalsHookScript() de core pour une page à vous (voir INTEGRATION.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 : un role="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 de widget-keyboard-focusable), un gestionnaire de clavier ajouté par script ne peut pas justifier un onclick en ligne, et password-paste-enabled ne peut pas savoir si un gestionnaire de collage ajouté par script remet le texte qu’un onpaste en 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-announcements ne juge que ce qui est là : une région live masquée aux technologies d’assistance par aria-hidden ou inert, ou derrière un <dialog> modal ouvert, ou laissée en aria-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, et getAnnouncements() 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 avec showModal() 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’attribut open ou show() n’est pas modale, même avec aria-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) renvoient notApplicable, 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é avec role="dialog" ou role="alertdialog" et aria-modal="true", rend aussi ces règles de structure notApplicable ; aria-modal ne 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 sans aria-modal="true" n’est traitée qu’à travers le inert ou le aria-hidden que 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 attribut open, display: none selon la spécification), ainsi que tout autre sous-arbre masqué via display: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éfinissez engineOptions.includeHiddenElements: true pour é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. Voir ENGINE_OPTIONS.md pour l’option et la liste exacte des mécanismes de masquage qu’elle couvre.
  • Un aria-details qui désigne un contenu masqué n’est pas signalé. aria-valid-attr-value vé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 pas showPopover(), et lit :popover-open de façon incohérente. Là où le DOM n’a pas de showPopover, 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 content CSS ne nomme rien. jsdom ne calcule aucun style pour ::before et ::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-present et 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-large vaut 32px) ainsi que les calc() de longueurs et de nombres, si bien que le texte de grande taille est distingué comme dans un navigateur. larger et smaller, 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 ses adoptedStyleSheets n’atteignent le style calculé de ses éléments, si bien que sous jsdom les couleurs, les tailles, et un display: none ou un visibility: hidden qui 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-shadow calculé 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 de text-shadow sur le même élément (par n’importe quel accesseur, depuis n’importe quelle CSSStyleDeclaration nouvellement 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 le text-shadow de chaque élément une seule fois par exécution et en mettant le résultat en cache (voir __textShadowInfoEl dans src/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ême getComputedStyle(el).textShadow plus 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} et style="background-image:url(hero.png)", la première lecture donne l’image et toutes les suivantes none. 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/testing le 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’en getComputedStyle, 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 concerne runDomRulesInPage et 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 le a { 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, un var(), une règle sous @supports, @layer, @container ou 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-size calculé. Dans un navigateur, les règles de contraste et p-as-heading jugent le texte à la taille où il est dessiné, zoom CSS, transformation SVG et font-size-adjust compris, ce dernier d’après les métriques de la police (font-size: 24px; font-size-adjust: 0.3 dessine Arial à environ 14px, ce qui n’est pas du texte de grande taille). jsdom ne met rien en page et ne lit aucune police : le font-size y 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-focus donne le focus aux éléments comme le ferait le clavier, et désactive les transitions sur l’élément qu’il mesure ; aria-hidden-focus donne le focus à un élément situé dans un sous-arbre aria-hidden pour voir si la page déplace aussitôt le focus ailleurs (une sentinelle de piège à focus) ; text-spacing-content-loss ajoute 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 gestionnaires focus et focusin : un carrousel peut passer à la diapositive focalisée, en déplaçant aria-hidden avec 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-focus lit 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-lock pose donc la question (STYLESHEETS_NOT_READABLE), et css-hidden-focus et css-focus-indicator-suppressed ne peuvent pas y voir une règle de focus. Passez son texte dans engineOptions.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 (voir INTEGRATION.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é native checked en 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 pourquoi aria-checked-state-mismatch est plafonnée à manual/cantTell plutôt qu’à un fail strict. 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-minimum et skip-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 dans engine.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 (voir INTEGRATION.md, qui montre comment l’appeler avant une analyse), et le résultat enregistre dans engine.environment si 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, reasonCode BACKGROUND_OVERLAP) au lieu d’être mesuré. Un texte qu’une boîte en overflow: hidden garde 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, ou forced-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 query forced-colors, ou une couleur système comme GrayText), ce que l’analyse signale dans engine.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 dans LAYOUT_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 Canvas du schéma de couleurs dans lequel la page est dessinée, que décide le color-scheme de 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 de contrast-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: dark sur 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 en light-dark(), CanvasText, LinkText ou Canvas est 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 valeur light-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-line ou un pseudo-élément, ni le texte mesuré sur une autre peinture. data.details.colorSchemesChecked liste 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 content d’un ::before ou d’un ::after, et le ::marker d’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-style propre à la page), et un ::before ou un ::after positionné 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 donne cantTell (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 donne cantTell quand on ne peut pas le savoir. Sous jsdom, les placeholders ne sont pas mesurés du tout. Détails : LAYOUT_MEASUREMENT.md.
  • target-size-minimum calcule 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 de clip-path et 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 renvoie notApplicable. Détails : LAYOUT_MEASUREMENT.md.
  • Le contraste est calculé en sRGB. Les couleurs exprimées en oklch(), oklab(), lab(), lch() ou color() (y compris la forme calculée de color-mix() et des couleurs relatives) sont converties en sRGB, comme un écran standard les affiche ; une couleur hors de sRGB, comme un rouge display-p3 saturé, 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, un color() dans un espace qu’il ne connaît pas, n’est pas traitée comme transparente : contrast-computable pose la question pour ce texte avec le reasonCode BACKGROUND_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-quality et form-control-label-quality signalent 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 fait link-name-quality. Ce sont toutes deux des règles manual plafonné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-text ne 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.