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 importante du navigateur est, bien entendu, de consulter des 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 ces éléments. Par exemple, un script qui ouvre developer.google.com, trouve le champ de recherche et recherche puppetaria:

(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ête fait donc partie intégrante de l'expérience Puppeteer. Jusqu'à présent, les sélecteurs de Puppeteer étaient limités aux sélecteurs CSS et XPath, qui, même s'ils sont très puissants sur le plan de l'expression, peuvent présenter des inconvénients pour les interactions persistantes du navigateur dans les scripts.

Sélecteurs syntaxiques ou 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 à des ID et des noms de classe issus du DOM. Ils constituent donc un outil intégré permettant aux développeurs Web de modifier ou d'ajouter des styles à un élément d'une page, mais dans ce contexte, le développeur contrôle entièrement la page et son arborescence 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.

En conséquence, 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> comme troisième enfant de l'élément body. Voici à quoi pourrait ressembler un extrait d'un scénario de test:

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 bonne nouvelle pour les rédacteurs tests: les utilisateurs de Puppeteer essaient déjà de choisir des sélecteurs résistants à ce type de modifications. Avec Puppetaria, nous fournissons 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 la suivante : si l'élément concret que nous voulons sélectionner n'a pas changé, le nœud d'accessibilité correspondant ne devrait pas avoir non plus changé.

Nous nommons ces sélecteurs les "sélecteurs ARIA" et nous prenons en charge l'interrogation du nom accessible calculé et du rôle 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 aux descripteurs de la façon dont la page est observée par le biais de technologies d'assistance telles que les lecteurs d'écran.

Dans l'exemple de script de test ci-dessus, nous pourrions plutôt 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 doivent nous avertir de ces modifications afin de nous assurer qu'elles sont intentionnelles.

Pour revenir à l'exemple global 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 ces sélecteurs ARIA peut offrir les avantages suivants aux utilisateurs de Puppeteer:

  • Rendez les sélecteurs des scripts de test plus résilients aux modifications du code source.
  • Rendre les scripts de test plus lisibles (les noms accessibles sont des descripteurs sémantiques)
  • Encouragez les bonnes pratiques 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 voulons permettre d'interroger des éléments en fonction de leur nom et de 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.

Notre approche de la mise en œuvre

Même en nous limitant à l'utilisation de l'arborescence d'accessibilité de Chromium, il existe plusieurs 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 affiche une interface de débogage via un protocole appelé CDP (Chrome DevTools Protocol). Cela permet d'exposer des fonctionnalités telles que "Actualiser la page" ou "Exécuter cet extrait de code JavaScript sur la page et renvoyer le résultat" via une interface indépendante du langage.

L'interface des outils de développement et Puppeteer utilisent tous deux la CDP pour communiquer avec le navigateur. Pour implémenter les commandes CDP, tous les composants de Chrome disposent d'une infrastructure d'outils de développement: dans le navigateur, dans le moteur de rendu, etc. CDP s'occupe de router les commandes au bon endroit.

Les actions Puppeteer telles que les requêtes, les clics et l'évaluation d'expressions sont effectuées à l'aide de commandes CDP telles que Runtime.evaluate, qui évaluent le JavaScript directement dans le contexte de la page et restituent 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

Cela nous laisse déjà deux chemins pour implémenter notre fonctionnalité de requête. Nous pouvons:

  • É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 qui peut accéder à l'arborescence d'accessibilité et l'interroger directement dans le processus Blink.

Nous avons implémenté trois prototypes:

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

Traversée de 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.

Traversée d'une axe AXTree de Puppeteer

À la place, 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.

Balayage 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++ plutôt que dans le contexte de la page via JavaScript.

Benchmark de test unitaire

La figure suivante compare la durée totale d'interrogation de quatre éléments 1 000 fois pour les trois prototypes. L'analyse comparative a été exécutée dans trois configurations différentes selon la taille de la page et l'activation ou non de la mise en cache des éléments d'accessibilité.

Analyse comparative: durée d&#39;exécution totale de l&#39;interrogation de quatre éléments 1 000 fois

Il est évident qu'il existe un écart de performances considérable entre le mécanisme de requête reposant sur CDP et les deux autres, implémentés uniquement dans Puppeteer, et la différence relative semble augmenter considérablement avec la taille de la page. Il est assez intéressant de constater que le prototype de balayage DOM JS répond si bien à l'activation de la mise en cache de l'accessibilité. Lorsque la mise en cache est désactivée, l'arborescence d'accessibilité est calculée à la demande et est 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.

Pour le balayage JS DOM, nous demandons le nom et le rôle accessibles de chaque élément pendant le balayage. Par conséquent, si la mise en cache est désactivée, Chromium calcule et supprime l'arborescence d'accessibilité pour chaque élément consulté. En revanche, pour les approches basées sur la CDP, l'arborescence n'est supprimée qu'entre chaque appel à 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 moindre.

Bien que l'activation de la mise en cache semble souhaitable dans ce cas, elle entraîne un coût d'utilisation de la mémoire supplémentaire. 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 la mise en 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 d'interrogation au niveau de la couche CDP améliore les performances dans un scénario de tests unitaires cliniques.

Pour déterminer 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 remplacé un total de 43 sélecteurs [aria-label=…] par 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. Par conséquent, le nombre réel d'exécutions du gestionnaire de requêtes aria était de 113 par exécution de la suite. Le nombre total de sélections de requêtes était de 2 253, donc seule une fraction des sélections de requêtes a eu lieu dans les prototypes.

Benchmark: suite de tests e2e

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 performance entre les deux prototypes se manifeste également dans ce scénario.

Un nouveau point de terminaison CDP

Au vu des benchmarks ci-dessus, et comme l'approche basée sur les indicateurs de lancement n'était pas souhaitable en général, nous avons décidé d'implémenter une nouvelle commande CDP pour interroger l'arborescence d'accessibilité. Nous devions maintenant comprendre 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 satisfaire 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 du NextInPreOrderIncludingIgnored existant n'était pas un bon choix pour implémenter la logique de balayage ici, car cela provoquait 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. Le gros grognement du travail ici consistait à restructurer le code de traitement des requêtes pour permettre aux requêtes de se résoudre directement via CDP au lieu de les interroger via JavaScript évalué dans le contexte de la page.

Étape suivante

Le nouveau gestionnaire aria 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 dans leurs scripts de test, et nous avons hâte de connaître vos suggestions pour rendre cette fonctionnalité encore plus utile.

Télécharger les canaux de prévisualisation

Vous pouvez utiliser Chrome Canary, Dev ou Bêta comme navigateur de développement par défaut. Ces versions preview vous permettent d'accéder aux dernières fonctionnalités des outils de développement, de tester des API de plates-formes Web de pointe et de 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 et des modifications dans l'article, ou de tout autre sujet lié aux outils de développement.

  • Envoyez-nous une suggestion ou un commentaire via crbug.com.
  • Signalez un problème dans les outils de développement en sélectionnant Autres options   More   > Aide > Signaler un problème dans les outils de développement dans les outils de développement.
  • Tweetez à l'adresse @ChromeDevTools.
  • Faites-nous part de vos commentaires sur les vidéos YouTube sur les nouveautés des outils de développement ou sur les vidéos YouTube de conseils pour les outils de développement.