Utilisation avec Cypress

Cypress est un framework JavaScript de tests de bout en bout qui exécute vos tests dans le navigateur lui-même, à côté de l’application testée. surea11y s’y intègre via le binding @surea11y/cypress.

$ npm install @surea11y/core @surea11y/cypress cypress

cypress est déclaré dans peerDependencies : il vous faut votre propre installation de Cypress pour que les types et le runtime soient résolus, comme pour n’importe quelle spec Cypress.

// cypress/e2e/my-page.cy.js
const { A11yCoreBuilder } = require('@surea11y/cypress');
 
it('has no accessibility violations', () => {
cy.visit('https://example.com/');
 
new A11yCoreBuilder()
.include('#main') // optional -- call multiple times for multi-region scans
.exclude('.cookie-banner') // optional
.withWcag('2.2', 'AA') // the rules for WCAG 2.2's A and AA criteria
.disableRules(['meta-refresh-no-exceptions'])
.analyze()
.then((results) => {
const fails = results.checksResults.filter((r) => r.outcome === 'fail');
expect(fails).to.have.length(0);
});
});

.withWcag(version, level) sélectionne par cible de conformité : les règles des critères de niveau A et AA de WCAG 2.2, selon le niveau de chaque critère dans cette version. .withBestPractices() ajoute les règles qui ne vérifient aucun critère WCAG, comme region ; seule, elle n’exécute que celles-ci.

Un pack se prépare dans Node, et un test Cypress s’exécute dans le navigateur : dans Cypress, .withPacks() prend donc le nom de module de chaque pack (un nom de paquet, ou un chemin depuis la racine du projet), et le plugin du binding, @surea11y/cypress/plugin, le prépare dans Node : enregistrez-le en renvoyant surea11y(on, config) depuis setupNodeEvents. .options({ profile }) choisit le profil du pack. Un pack qui ne s’exécute pas est signalé comme une lacune d’analyse 'pack-skipped'.

Pas d’argument de constructeur { page }/{ browser }/{ driver }, contrairement aux bindings basés sur un pilote : Cypress n’a pas de tel handle. cy est disponible dans chaque fichier de spec et est le pilote. analyze() renvoie un chainable Cypress, pas une Promise : ne faites pas de await dessus ; appelez .then() comme ci-dessus.

Composants web. Un composant qui garde ses ElementInternals dans un champ privé, et se donne un nom, ses états ou un rôle par ce biais, ne peut être relu par aucun script de la page. Par défaut, l’analyse pose donc une question sur ce composant (cantTell) au lieu de le juger, et un rôle donné uniquement ainsi n’est pas vérifié du tout. Si vous construisez ou testez une bibliothèque de composants, installez le hook d’internals facultatif du binding avant que les scripts de la page elle-même ne s’exécutent, et ces composants sont jugés comme n’importe quel autre contrôle :

// cypress/support/e2e.js
import { installInternalsHook } from '@surea11y/cypress';
 
installInternalsHook();

Appelé une fois dans le fichier de support, il s’exécute à chaque visite, via l’événement window:before:load de Cypress. Pour une seule visite, passez-le à cy.visit() sous la forme { onBeforeLoad: installInternalsHook }. Il s’exécute avec l’eval de la fenêtre de l’application, ce qui fonctionne parce que Cypress retire par défaut la Content Security Policy de l’application ; les frames à l’intérieur de l’application ne le reçoivent pas.

Le hook est destiné aux exécutions de test et d’audit : il garde ce qu’il enregistre là où n’importe quel script de la page peut le lire, c’est pourquoi il est facultatif. Sans lui, l’analyse le signale une fois lorsqu’elle a posé une question sur de tels composants. La section du README sur le hook d’internals donne les détails.

Gestionnaires de clic et régions live. Depuis core 1.14, il existe deux autres hooks facultatifs. La plupart des pages ajoutent leurs gestionnaires de clic depuis un script, où aucun script de la page ne peut les lister ensuite, si bien que l’analyse ne voit pas qu’un <div role="button"> doté d’un écouteur de clic ne fait rien au clavier ; avec le hook d’écouteurs, ce bouton échoue (CLICK_HANDLER_NO_KEY_LISTENER). Le hook de régions live enregistre ce que reçoivent les régions live de la page : l’analyse pose alors une question sur un message d’état ajouté avec son texte déjà dedans, que les lecteurs d’écran n’annoncent souvent pas, et announcements() renvoie ce que la page a mis à disposition pour être annoncé, afin qu’un test vérifie qu’une action a bien informé l’utilisateur. Installez-les avant que les scripts de la page elle-même ne s’exécutent, comme le hook d’internals :

// cypress/support/e2e.js
import { installListenerHook, installLiveRegionHook } from '@surea11y/cypress';
 
installListenerHook();
installLiveRegionHook();
 
// in a spec
cy.get('#save').click();
A11yCoreBuilder.announcements().then((said) => {
expect(said.map((a) => a.text)).to.include('Saved');
});

Comme pour le hook d’internals, le fichier de support couvre chaque visite, et seule la fenêtre de l’application elle-même les reçoit, pas les frames qu’elle contient. announcements() lit la fenêtre de l’application et renvoie un chainable, comme .analyze().

Comme le hook d’internals, tous deux sont destinés aux exécutions de test et d’audit : n’importe quel script de la page peut lire ce qu’ils enregistrent. announcements() est ce que la page a mis à disposition pour être annoncé, pas ce qu’un lecteur d’écran a dit, et vaut null sans le hook de régions live. Sans l’un ou l’autre hook, getScanNotes() signale quand des constats que ce hook aurait tranchés ont été jugés sans lui. Les sections du README sur le hook d’écouteurs et le hook de régions live donnent les détails.

Mode sombre, largeurs de téléphone et seuil de gravité. .conditions([{ colorScheme: 'dark' }, { width: 320 }]) analyse la page telle qu’elle est, puis à nouveau sous chaque condition, et renvoie un seul résultat : le résultat d’une règle est le pire de toutes les analyses, et chaque constat indique les conditions sous lesquelles il a été vu. Chaque condition est une analyse complète, et .frames(true) ne peut pas encore s’y combiner. Une largeur passe par cy.viewport() ; un schéma de couleurs exige un navigateur de la famille Chromium (Chrome, Edge, Electron), et dans Firefox .analyze() échoue avant toute analyse. getGatingFailures(results, { failOnSeverity: 'serious' }) renvoie les règles en échec à cette gravité ou au-dessus, pour qu’un test bloque d’abord les problèmes graves et critiques pendant que les autres restent signalés ; formatFailures() accepte la même option.

Des questions qui penchent, avec des étapes. Depuis core 1.14, un constat cantTell peut pencher vers fail ou pass, en disant pourquoi, et porte howToCheck, les étapes qu’une personne suit pour y répondre. formatFailures() liste les échecs probables juste après les échecs, et affiche les étapes avec { howToCheck: true }. Pour qu’un test échoue aussi sur les échecs probables, ajoutez leaningFailures(results) de core au seuil. Dans Cypress, les étapes viennent du plugin : setupNodeEvents doit renvoyer ce que renvoie surea11y(on, config). Sans cela, le résultat n’a pas d’étapes, et rien n’échoue.

A11yCoreBuilder a aussi .wcagLevel(), qui limite les critères rapportés à un niveau WCAG sans changer les règles exécutées ; .includeMode('or'), qui exécute les règles d’une étiquette plus les règles que vous nommez ; .plannedChecks() et .plannedCriteria(), qui listent ce qu’une analyse vérifiera avant qu’elle ne s’exécute, pour qu’une sélection qui n’exécute rien ne passe pas pour une analyse sans échec ; .fetchStyleSheets(false), qui empêche .analyze() de récupérer les feuilles de style d’autres origines que la page ne peut pas lire elle-même, pour les règles qui lisent le CSS ; et getScanNotes(results), qui indique comment la page a été analysée. Le README du binding les décrit chacun.

Voir aussi un exemple exécutable dans le dépôt du paquet.

Ensuite : choisir les règles à exécuter, intégrer l’analyse à la CI et consulter le schéma complet du résultat.