Puppetaria: scripts Puppeteer axés sur l'accessibilité

Johan Bay
Johan Bay

Puppeteer et son approche des sélecteurs

Puppeteer est une bibliothèque d'automatisation de navigateur pour Node. Elle vous permet de contrôler un navigateur à l'aide d'une API JavaScript simple et moderne.

La tâche la plus courante d'un navigateur est bien sûr la navigation sur les pages Web. Automatiser cette tâche revient essentiellement à automatiser les interactions avec la page Web.

Dans Puppeteer, cela se fait en interrogeant les éléments DOM à l'aide de sélecteurs basés sur des chaînes et en effectuant des actions telles que cliquer ou saisir du texte sur les éléments. Par exemple, un script qui ouvre developer.google.com, recherche le champ de recherche et recherche puppetaria peut se présenter comme suit:

(async () => {
   const browser = await puppeteer.launch({ headless: false });
   const page = await browser.newPage();
   await page.goto('https://developers.google.com/', { waitUntil: 'load' });
   // Find the search box using a suitable CSS selector.
   const search = await page.$('devsite-search > form > div.devsite-search-container');
   // Click to expand search box and focus it.
   await search.click();
   // Enter search string and press Enter.
   await search.type('puppetaria');
   await search.press('Enter');
 })();

La façon dont les éléments sont identifiés à l'aide de sélecteurs de requêtes est donc une partie déterminante de l'expérience Puppeteer. Jusqu'à présent, les sélecteurs de Puppeteer étaient limités aux sélecteurs CSS et XPath, qui, bien que très puissants en termes d'expression, peuvent présenter des inconvénients pour la persistance des interactions du navigateur dans les scripts.

Sélecteurs syntaxiques et sémantiques

Les sélecteurs CSS sont de nature syntaxique. Ils sont étroitement liés au fonctionnement interne de la représentation textuelle de l'arborescence DOM, dans le sens où ils font référence aux ID et aux noms de classe du DOM. Ils constituent donc un outil essentiel pour les développeurs Web qui souhaitent modifier ou ajouter des styles à un élément d'une page. Dans ce contexte, le développeur a un contrôle total sur la page et son arbre DOM.

D'autre part, un script Puppeteer est un observateur externe d'une page. Par conséquent, lorsque des sélecteurs CSS sont utilisés dans ce contexte, il introduit des hypothèses cachées sur la façon dont la page est mise en œuvre, sur lesquelles le script Puppeteer n'a aucun contrôle.

Par conséquent, ces scripts peuvent être fragiles et sensibles aux modifications du code source. Supposons, par exemple, que vous utilisiez des scripts Puppeteer pour effectuer des tests automatisés sur une application Web contenant le nœud <button>Submit</button> en tant que troisième enfant de l'élément body. Un extrait d'un cas de test peut ressembler à ceci :

const button = await page.$('body:nth-child(3)'); // problematic selector
await button.click();

Ici, nous utilisons le sélecteur 'body:nth-child(3)' pour trouver le bouton d'envoi, mais celui-ci est étroitement lié à cette version de la page Web. Si un élément est ajouté ultérieurement au-dessus du bouton, ce sélecteur ne fonctionne plus.

Ce n'est pas une surprise pour les rédacteurs de tests : les utilisateurs de Puppeteer tentent déjà de choisir des sélecteurs résistants à de tels changements. Avec Puppetaria, nous offrons aux utilisateurs un nouvel outil pour cette quête.

Puppeteer est désormais fourni avec un gestionnaire de requêtes alternatif basé sur l'interrogation de l'arborescence d'accessibilité plutôt que sur les sélecteurs CSS. La philosophie sous-jacente est que si l'élément concret que nous souhaitons sélectionner n'a pas changé, le nœud d'accessibilité correspondant ne devrait pas non plus avoir changé.

Nous appelons ces sélecteurs "sélecteurs ARIA" et permettons de demander le nom et le rôle accessibles calculés de l'arborescence d'accessibilité. Par rapport aux sélecteurs CSS, ces propriétés sont de nature sémantique. Ils ne sont pas liés aux propriétés syntaxiques du DOM, mais plutôt aux descripteurs de la façon dont la page est observée via des technologies d'assistance telles que les lecteurs d'écran.

Dans l'exemple de script de test ci-dessus, nous pourrions utiliser le sélecteur aria/Submit[role="button"] pour sélectionner le bouton souhaité, où Submit fait référence au nom accessible de l'élément :

const button = await page.$('aria/Submit[role="button"]');
await button.click();

Si, par la suite, nous décidons de modifier le contenu textuel de notre bouton de Submit à Done, le test échoue à nouveau, mais dans le cas présent, c'est souhaitable. En changeant le nom du bouton, nous modifions le contenu de la page plutôt que sa présentation visuelle ou la façon dont elle est structurée dans le DOM. Nos tests devraient nous alerter de ces modifications pour nous assurer qu'elles sont intentionnelles.

Pour en revenir à l'exemple plus vaste de la barre de recherche, nous pourrions exploiter le nouveau gestionnaire aria et remplacer

const search = await page.$('devsite-search > form > div.devsite-search-container');

avec

const search = await page.$('aria/Open search[role="button"]');

pour localiser la barre de recherche.

Plus généralement, nous pensons que l'utilisation de tels sélecteurs ARIA peut offrir les avantages suivants aux utilisateurs de Puppeteer :

  • Renforcez la résilience des sélecteurs dans les scripts de test face aux modifications du code source.
  • Rendre les scripts de test plus lisibles (les noms accessibles sont des descripteurs sémantiques).
  • Expliquez les bonnes pratiques à suivre pour attribuer des propriétés d'accessibilité aux éléments.

Le reste de cet article approfondit la façon dont nous avons mis en œuvre le projet Puppetaria.

Le processus de conception

Contexte

Comme indiqué ci-dessus, nous souhaitons permettre d'interroger les éléments par leur nom et leur rôle accessibles. Il s'agit des propriétés de l'arborescence d'accessibilité, double de l'arborescence DOM habituelle, qui est utilisée par des appareils tels que les lecteurs d'écran pour afficher des pages Web.

En examinant la spécification permettant de calculer le nom accessible, il apparaît clairement que le calcul du nom d'un élément n'est pas une mince affaire. C'est pourquoi, dès le début, nous avons décidé de réutiliser l'infrastructure existante de Chromium pour cela.

Comment nous avons procédé à l'implémentation

Même en nous limitant à l'utilisation de l'arborescence d'accessibilité de Chromium, il existe de nombreuses façons d'implémenter des requêtes ARIA dans Puppeteer. Pour comprendre pourquoi, voyons d'abord comment Puppeteer contrôle le navigateur.

Le navigateur expose une interface de débogage via un protocole appelé protocole Chrome DevTools (CDP). Cela permet d'exposer des fonctionnalités telles que "Rafraîchir la page" ou "Exécuter ce code JavaScript sur la page et renvoyer le résultat" via une interface indépendante du langage.

L'interface utilisateur de DevTools et Puppeteer utilisent CDP pour communiquer avec le navigateur. Pour implémenter les commandes CDP, une infrastructure DevTools est intégrée à tous les composants de Chrome: dans le navigateur, dans le moteur de rendu, etc. La CDP s'occupe de router les commandes au bon endroit.

Les actions Puppeteer telles que l'interrogation, le clic et l'évaluation des expressions sont effectuées en exploitant des commandes CDP telles que Runtime.evaluate, qui évalue JavaScript directement dans le contexte de la page et renvoie le résultat. D'autres actions de Puppeteer, comme émuler une déficience de la vision des couleurs, effectuer des captures d'écran ou capturer des traces, utilisent la CDP pour communiquer directement avec le processus de rendu Blink.

CDP

Nous avons donc déjà deux options pour implémenter notre fonctionnalité de requête:

  • Écrire notre logique de requête en JavaScript et l'injecter dans la page à l'aide de Runtime.evaluate, ou
  • Utilisez un point de terminaison CDP pouvant accéder à l'arborescence d'accessibilité et l'interroger directement dans le processus Blink.

Nous avons implémenté trois prototypes:

  • Parcours du DOM JavaScript : basé sur l'injection de code JavaScript dans la page
  • Balayage AXTree de Puppeteer : basé sur l'utilisation de l'accès CDP existant à l'arborescence d'accessibilité
  • Parcours DOM du CDP : utilisation d'un nouveau point de terminaison CDP conçu pour interroger l'arborescence d'accessibilité.

Parcours du DOM JS

Ce prototype effectue un balayage complet du DOM et utilise element.computedName et element.computedRole, restreints par l'indicateur de lancement ComputedAccessibilityInfo, pour récupérer le nom et le rôle de chaque élément lors du balayage.

Parcours AXTree Puppeteer

Ici, nous récupérons l'arborescence d'accessibilité complète via CDP et nous la balayons dans Puppeteer. Les nœuds d'accessibilité obtenus sont ensuite mappés à des nœuds DOM.

Parcours DOM CDP

Pour ce prototype, nous avons implémenté un nouveau point de terminaison CDP spécifiquement pour interroger l'arborescence d'accessibilité. De cette façon, les requêtes peuvent être effectuées sur le backend via une implémentation C++ au lieu du contexte de la page via JavaScript.

Benchmark de test unitaire

La figure suivante compare le temps d'exécution total de la requête de quatre éléments 1 000 fois pour les trois prototypes. Le benchmark a été exécuté dans trois configurations différentes, en modifiant la taille de la page et en activant ou non la mise en cache des éléments d'accessibilité.

Benchmark: Durée d&#39;exécution totale de la requête de quatre éléments 1 000 fois

Il est clair qu'il existe un écart de performances considérable entre le mécanisme de requêtes basé sur le CDP et les deux autres implémentés uniquement dans Puppeteer. La différence relative semble augmenter considérablement avec la taille de la page. Il est intéressant de constater que le prototype de parcours DOM JS répond si bien à l'activation de la mise en cache d'accessibilité. Lorsque la mise en cache est désactivée, l'arborescence d'accessibilité est calculée à la demande et supprimée après chaque interaction si le domaine est désactivé. Si vous activez le domaine, Chromium met en cache l'arborescence calculée à la place.

Pour l'exploration du DOM JS, nous demandons le nom et le rôle accessibles pour chaque élément lors de l'exploration. Par conséquent, si la mise en cache est désactivée, Chromium calcule et supprime l'arborescence d'accessibilité pour chaque élément que nous consultons. En revanche, pour les approches basées sur le CDP, l'arbre n'est supprimé qu'entre chaque appel au CDP, c'est-à-dire pour chaque requête. Ces approches bénéficient également de l'activation de la mise en cache, car l'arborescence d'accessibilité est ensuite conservée entre les appels CDP, mais l'amélioration des performances est donc relativement plus faible.

Bien que l'activation de la mise en cache semble souhaitable dans ce cas, elle entraîne un coût supplémentaire pour l'utilisation de la mémoire. Cela peut poser problème pour les scripts Puppeteer, par exemple, qui enregistrent les fichiers de suivi. Nous avons donc décidé de ne pas activer le cache de l'arborescence d'accessibilité par défaut. Les utilisateurs peuvent activer eux-mêmes la mise en cache en activant le domaine d'accessibilité CDP.

Analyse comparative de la suite de tests des outils de développement

Le benchmark précédent a montré que l'implémentation de notre mécanisme de requête au niveau de la couche CDP améliore les performances dans un scénario de test unitaire clinique.

Pour voir si la différence est suffisamment prononcée pour qu'elle soit visible dans un scénario plus réaliste d'exécution d'une suite de tests complète, nous avons correctif la suite de tests de bout en bout des outils de développement afin d'utiliser les prototypes basés sur JavaScript et CDP et comparé les environnements d'exécution. Dans ce benchmark, nous avons modifié un total de 43 sélecteurs, passant de [aria-label=…] à un gestionnaire de requêtes personnalisé aria/…, que nous avons ensuite implémenté à l'aide de chacun des prototypes.

Certains sélecteurs sont utilisés plusieurs fois dans les scripts de test. Le nombre réel d'exécutions du gestionnaire de requêtes aria était donc de 113 par exécution de la suite. Le nombre total de sélections de requêtes était de 2 253. Par conséquent, seule une fraction des sélections de requêtes a été effectuée via les prototypes.

Benchmark: suite de tests de bout en bout

Comme le montre la figure ci-dessus, il existe une différence notable au niveau de la durée d'exécution totale. Les données sont trop bruyantes pour tirer des conclusions spécifiques, mais il est clair que l'écart de performances entre les deux prototypes se manifeste également dans ce scénario.

Un nouveau point de terminaison CDP

Compte tenu des benchmarks ci-dessus et comme l'approche basée sur le flag de lancement était indésirable en général, nous avons décidé de mettre en œuvre une nouvelle commande CDP pour interroger l'arborescence d'accessibilité. Nous avons ensuite dû déterminer l'interface de ce nouveau point de terminaison.

Pour notre cas d'utilisation dans Puppeteer, nous avons besoin que le point de terminaison accepte ce qu'on appelle RemoteObjectIds comme argument. Pour nous permettre ensuite de trouver les éléments DOM correspondants, il doit renvoyer une liste d'objets contenant le backendNodeIds pour les éléments DOM.

Comme le montre le graphique ci-dessous, nous avons essayé plusieurs approches pour répondre à cette interface. Nous avons alors constaté que la taille des objets renvoyés, c'est-à-dire si nous renvoyions des nœuds d'accessibilité complets ou seulement backendNodeIds ne comportait aucune différence visible. D'autre part, nous avons constaté que l'utilisation de NextInPreOrderIncludingIgnored existante était un mauvais choix pour implémenter la logique de parcours ici, car cela entraînait un ralentissement notable.

Benchmark: comparaison des prototypes de balayage AXTree basés sur CDP

Conclusion

Maintenant que le point de terminaison CDP est en place, nous avons implémenté le gestionnaire de requêtes du côté Puppeteer. L'essentiel du travail consistait à restructurer le code de gestion des requêtes pour permettre aux requêtes de se résoudre directement via le CDP au lieu de les interroger via JavaScript évalué dans le contexte de la page.

Étape suivante

Le nouveau gestionnaire aria est fourni avec Puppeteer v5.4.0 en tant que gestionnaire de requêtes intégré. Nous avons hâte de voir comment les utilisateurs l'intégreront à leurs scripts de test et nous avons hâte de connaître vos idées pour l'améliorer.

Télécharger les canaux de prévisualisation

Envisagez d'utiliser Chrome Canary, Dev ou Bêta comme navigateur de développement par défaut. Ces canaux de prévisualisation vous donnent accès aux dernières fonctionnalités de DevTools, vous permettent de tester les API de plates-formes Web de pointe et vous aident à détecter les problèmes sur votre site avant vos utilisateurs.

Contacter l'équipe des outils pour les développeurs Chrome

Utilisez les options suivantes pour discuter des nouvelles fonctionnalités, des mises à jour ou de tout autre sujet lié aux outils de développement.