Rapport JUnit
@surea11y/core/junit produit un résultat d’analyse au format JUnit XML, le format de rapport de tests que les tableaux de bord de CI lisent nativement : le widget de tests des merge requests de GitLab, l’onglet Tests d’Azure DevOps, Jenkins et CircleCI.
const { renderJunitReport } = require('@surea11y/core/junit');const { runDomRulesInPage } = require('@surea11y/core');const result = runDomRulesInPage(url, null, { profile: 'wcag22-aa' }, null);require('fs').writeFileSync('surea11y.junit.xml', renderJunitReport(result));
renderJunitReport(result, options) est une fonction pure : elle renvoie le XML sous forme de chaîne et ne touche jamais au système de fichiers. Elle fonctionne aussi bien sur un résultat enregistré auparavant en JSON, comme la sortie de surea11y scan <target> --json de @surea11y/cli. En ligne de commande, inutile de l’appeler : surea11y scan <target> --junit a11y.junit.xml (CLI 1.1.0 ou ultérieure) écrit le rapport lui-même, avec les options par défaut plus les baselineEntries de son fichier --baseline. Voir Intégration CI pour des recettes GitLab et Azure DevOps.
Structure
Un <testsuite> par critère de succès WCAG, un <testcase> par règle qui y est associée. En analysant cette page avec l’appel ci-dessus :
<!doctype html><html lang="en"><head><title>Home</title></head><body><main><h1>Welcome</h1><img src="a.png"></main></body></html>
on obtient :
<?xml version="1.0" encoding="UTF-8"?><testsuites name="surea11y" tests="14" failures="1" errors="0" skipped="4" time="0"><testsuite name="WCAG 1.1.1" tests="1" failures="1" errors="0" skipped="0" time="0"><properties><property name="wcagCriterion" value="1.1.1"/><property name="title" value="Non-text content: text alternatives"/><property name="wcagLevel" value="A"/><property name="criterionOutcome" value="fail"/><property name="engine" value="a11ycore"/><property name="schemaVersion" value="1.0.0"/><property name="wcagVersion" value="2.2"/><property name="profile" value="wcag22-aa"/><property name="layout" value="false"/><property name="locale" value="en"/><property name="url" value="https://example.test/"/></properties><testcase classname="wcag-1.1.1" name="img-alt-present" time="0"><failure type="fail" message="1 failing occurrence: This <img> has no alt attribute.">- This <img> has no alt attribute. Add an alt attribute that says what the image shows, or alt="" if the image is purely decorative.selector: html > body > main > imghtml: <img src="a.png">help: https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html</failure></testcase></testsuite><!-- 10 more suites trimmed: 13 tests, 4 of them skipped (manual review) --></testsuites>
- Une suite verte nommée d’après un critère signifie que les règles exécutées pour lui n’ont signalé aucun échec. Cela ne signifie pas que le critère a été entièrement vérifié, ni qu’il est satisfait : la plupart des critères ont des parties qu’aucune règle ne couvre (voir Correspondance de conformité WCAG). Sa propriété
criterionOutcomele dit :cantTelllorsque les règles ont réussi mais ne vérifient qu’une partie du critère, ce qui est aujourd’hui le cas de chaque critère. - Les suites suivent le critère, parce que c’est ce que l’on suit, et les testcases suivent la règle plutôt que l’occurrence : un défaut répété quarante fois sur une page est un seul test en échec dont le corps liste les quarante, et le nombre de tests reste stable d’une exécution à l’autre.
- Les noms sont les mêmes dans toutes les langues. Une suite est nommée
WCAG <criterion>(WCAG 1.4.3), la suite des règles associées à aucun critèreOther checks, un testcase d’après son id de règle, et sonclassnamevautwcag-<criterion>ouother, quelle que soit laengineOptions.localede l’analyse. L’historique des tests en CI (GitLab, Jenkins, les rapporteurs de tests de GitHub) repose sur ces noms : une analyse lue en français donne donc les mêmes tests qu’une analyse lue en anglais. Le titre du critère, qui est traduit, est la propriététitlede la suite (Contrast: minimum,Contraste (minimum)). Le texte des constats eux-mêmes (le message et l’indication de chaque occurrence) est dans la langue de l’analyse ; les mots que le rapport ajoute autour sont en anglais dans toutes les langues : le compte qui ouvre unmessage(1 failing occurrence: Cet élément <img> n’a pas d’attribut alt.), les libellésselector:,html:ethelp:, et les messages<skipped>. Avant core 1.12, le nom d’une suite portait le titre du critère (WCAG 1.1.1 Non-text content: text alternatives) : le premier rapport après la mise à jour affiche donc chaque test une fois comme nouveau dans un tableau de bord qui suivait les anciens noms, et leur historique repart de là. - Les suites proviennent des correspondances WCAG de chaque règle, pas des composites : chaque règle exécutée est donc signalée même si les composites ont été exclus. Le composite, s’il s’est exécuté, fournit la propriété
titlede la suite et la propriétécriterionOutcome, qui compte les règles personnalisées associées au critère comme il compte les règles intégrées. Un critère vérifié par plusieurs composites (4.1.2 : son nom accessible et sa validité ARIA) prend le pire de leurs résultats, puisqu’une partie en échec fait échouer le critère, et le titre que partagent leurs titres (Name, role, value). Une règle associée à deux critères apparaît dans les deux suites. Une règle sans critère va dans une suite finaleOther checksavecclassname="other". - Les suites suivent le niveau et la version WCAG de l’analyse. Un critère que l’analyse a écarté de
rulesResultsà cause d’eux n’a pas de suite : un critère AAA sousengineOptions.wcagLevel: 'AA'(engine.wcagLevel), ou 4.1.1 Analyse syntaxique quandincludeRuleIds: ['duplicate-id']s’est exécuté sous WCAG 2.2 (engine.wcagVersion), qui l’a supprimé. Un critère que l’analyse signale est conservé, comme une agrégation nommée par son id au-dessus du niveau, un critère partiellement vérifié, ou 4.1.1 dans une analyse 2.2 qui l’a sélectionné. Une règle associée seulement à des critères écartés va dansOther checks, et ses échecs sont<skipped message="Not counted: WCAG 1.4.6 is outside the scan's target, WCAG 2.2 AA; 1 failing occurrence">, avec les occurrences dans<system-out>: la règle s’est exécutée et a échoué, ce que le rapport montre, mais un build AA n’échoue pas sur un critère AAA. Une règle qui ne s’est pas terminée reste une<error>. Un niveau que donne la sélection (runOnly.wcag.level, ou des tags de niveau) est lui aussi enregistré commeengine.wcagLevel, et filtre les suites de la même façon. Un résultat qui n’enregistre niengine.wcagLevelniengine.wcagVersion, comme celui d’un moteur plus ancien, garde tous les critères. engine,schemaVersion,wcagVersion,profile,optInRules, les conditions de rendu issues deengine.environment(layoutet, dans un navigateur,viewportsous la forme1280x720,devicePixelRatio,colorScheme,forcedColorsetfonts),localeeturlde l’exécution sont répétés comme propriétés sur chaque suite, chacun seulement quand le résultat le contient. Un échec qui apparaît ou disparaît selon la largeur du viewport peut ainsi être rattaché à la largeur utilisée par l’exécution. L’exemple ci-dessus est une analyse jsdom, qui n’a pas de mise en page.- Une page analysée à plusieurs largeurs de viewport donne les mêmes noms de suites et de tests à chaque largeur, et la vue des tests d’une CI peut les fusionner. Écrivez un fichier par largeur, et nommez ou regroupez les fichiers par largeur dans les réglages de rapport de la CI ; la propriété
viewportenregistre la largeur que décrit un fichier. - Les suites sont triées par critère, numériquement (1.4.3 avant 1.4.10), et les testcases par id de règle.
- Les propriétés
en301549nomment la clause d’EN 301 549 qui reprend le critère, quand il y en a une et que l’analyse a demandé les clauses d’EN 301 549 (voir EN 301 549). Chaque autre norme listée dans le champstandardsdu résultat ajoute des propriétés nommées d’après sa clé, une par exigence de cette norme à laquelle les règles de la suite sont associées : un point de la liste de contrôle d’un pack qui regroupe la règle est listé par son id,<property name="acme" value="acme-images"/>. Une norme que seule une règle personnalisée nomme n’ajoute aucune propriété.
Un résultat compact (engineOptions.output.detail: 'findings') est lu comme le résultat complet qu’il représente ; il est donc rapporté de la même façon.
Résultats multi-frames
Un résultat multi-frames (voir Schéma de sortie) donne d’abord les suites de la page, puis celles de chaque frame, nommées d’après le chemin du frame (Frame #checkout: WCAG 1.1.1, le même dans toutes les langues) et avec une propriété frame qui le contient, et le classname de leurs testcases commence par lui (frame #checkout wcag-1.1.1), si bien qu’un échec dans un frame n’est pas fusionné avec ceux de la page par une CI qui identifie les tests par nom de suite ou par classname et name. Un frame qui n’a pas répondu est une suite Frame <path> avec un seul testcase ignoré, Not scanned: <error> : le frame n’a pas été vérifié, ce qui n’est pas une réussite. Les totaux de <testsuites> comptent chaque frame.
Résultats
| Résultat de la règle | JUnit | Pourquoi |
|---|---|---|
fail | <failure type="fail">, une ligne par occurrence en échec (message, indication, sélecteur, balisage), puis help: <url> quand la règle a un lien d’aide http(s) (meta.helpUrl), sinon le document Understanding de son premier critère WCAG | Le cas déterministe, celui qui bloque. |
cantTell | <skipped> indiquant combien d’occurrences demandent une revue manuelle | JUnit n’a pas de « impossible à déterminer ». « Ignoré » le fait apparaître sans faire passer le build au rouge, la même ligne que trace SARIF avec warning. |
pass | un simple <testcase> | |
notApplicable | omis | Une page en compte des centaines, et aucun ne dit rien. includeNotApplicable: true les ajoute sous forme de <skipped message="Not applicable">. |
une règle qui ne s’est pas terminée (cantTell sans occurrence et avec une error : elle a levé une exception) | <error type="ruleError"> avec l’erreur, comptée dans errors | L’élément JUnit pour un test qui n’a pas pu s’exécuter. La page n’a pas été vérifiée au regard de la règle, ce qui n’est ni une réussite ni un point à examiner. |
Une règle fail qui a aussi des occurrences cantTell signale les échecs dans <failure> et les cas indécis dans <system-out>, où les tableaux de bord affichent la sortie des tests. Une règle cantTell ignorée fait de même.
La ligne selector: d’un élément situé dans un arbre shadow nomme d’abord les shadow hosts, puis l’élément, joints par >>> : html > body > main > x-card >>> button. C’est le même emplacement que donnent SARIF et le rapport HTML.
Options
| Option | Par défaut | Effet |
|---|---|---|
cantTellAs | 'skipped' | 'failure' signale les règles cantTell sous forme de <failure type="cantTell">, pour un pipeline qui ne doit pas réussir tant que quelque chose reste indécis. Lue quelle que soit la casse ; toute autre valeur lève une TypeError, car une valeur mal orthographiée ne bloquerait rien, sans le dire. |
leanFailAs | 'skipped' | 'failure' signale une règle dont les constats non tranchés en comptent un qui penche vers l’échec (son lean) sous forme de <failure type="leanFail">, en listant ces constats, chacun avec la raison pour laquelle il échoue probablement (likely fails:), la mise en garde de la tendance quand elle en a une, et la façon dont une personne le confirme (how to check:, les étapes howToCheck du constat, puis fails when:), que le rapport ajoute dans Node à un résultat qui arrive sans elles ; ses autres constats non tranchés restent tels que le dit cantTellAs. Pour un pipeline qui bloque sur les échecs probables sans bloquer sur chaque question. Y compris pour une règle de bonne pratique : une telle règle, associée à aucun critère WCAG, n’échoue jamais, et les faits qu’elle constate penchent vers l’échec sans condition, si bien qu’un niveau de titre sauté (heading-order, un <h1> puis un <h3>) devient un <failure type="leanFail"> dans Other checks. Une règle dont chaque critère est hors de la cible de l’analyse ne bloque pas, comme pour un échec. Lue quelle que soit la casse ; toute autre valeur lève une TypeError. leaningFailures(result, { wcagOnly }), de @surea11y/core, liste les constats sur lesquels elle bloque, avant la baseline. |
leanFailWcagOnly | false | Avec leanFailAs: 'failure', true ne bloque que sur les constats des règles associées à un critère WCAG de la cible de l’analyse, en laissant de côté les tendances des règles de bonne pratique. Une valeur autre que true ou false lève une TypeError. |
includeNotApplicable | false | Inclut les règles notApplicable comme tests ignorés. |
baselineEntries | aucune | Entrées issues de buildBaselineEntries() de la baseline. Les occurrences en échec qui y sont enregistrées sont retirées, exactement comme dans SARIF. L’option accepte le fichier de baseline tel qu’il est enregistré (son JSON.parse) ou sa liste entries. Une règle dont tous les échecs sont déjà connus devient <skipped message="N known failures recorded in the baseline">, et non un test réussi : elle n’a pas réussi. Une entrée écrite par 1.13 sous un code de raison qui a changé en 1.14 correspond toujours au constat sous son nouveau code. Avec leanFailAs: 'failure', un constat qui penche vers l’échec et a une entrée marquée lean: 'fail' (buildBaselineEntries(result, { leanFail: true })) ne bloque pas non plus, et le message <skipped> le compte parmi les known likely failures recorded in the baseline. |
packs | aucune | Les packs que l’analyse a exécutés (engineOptions.packs) : avec leanFailAs: 'failure', les échecs probables d’une règle de pack donnent les étapes de son propre meta.review, comme dans le rapport HTML. |
name | 'surea11y' | Le name du <testsuites> racine, pour distinguer les rapports de plusieurs pages dans un même tableau de bord. |
Déterminisme
Le moteur n’a pas d’horloge, et ce rapport n’en invente pas : chaque time vaut "0", et un attribut timestamp n’apparaît sur chaque suite que si le résultat en porte un (engineOptions.timestamp). La même analyse produit toujours un XML identique octet pour octet : un rapport peut donc être versionné ou comparé.
Le texte est échappé pour XML, et les caractères que XML 1.0 interdit même échappés (la plupart des caractères de contrôle, les surrogates isolés) sont supprimés, car le balisage capturé dans une page peut en contenir.