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 navigateurs 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 sûr, la navigation sur les pages Web. Automatiser cette tâche revient essentiellement à automatiser les interactions avec la page Web.

Dans Puppeteer, vous pouvez interroger les éléments DOM à l'aide de sélecteurs basés sur des chaînes et effectuer des actions telles que cliquer sur ces éléments ou saisir du texte. Par exemple, un script qui ouvre developer.google.com, trouve le champ de recherche et recherche puppetaria peut ressembler à ceci:

(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 des sélecteurs de requête est donc un élément clé de l'expérience Puppeteer. Jusqu'à présent, les sélecteurs de Puppeteer se limitaient aux sélecteurs CSS et XPath. Bien que très puissants, ils peuvent présenter des inconvénients en termes de persistance des interactions avec le 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 la mesure où ils font référence à des ID et à des noms de classe à partir du DOM. Ils constituent donc un outil essentiel pour les développeurs Web, qui leur permet de modifier ou d'ajouter des styles à un élément d'une page. Toutefois, dans ce contexte, le développeur dispose d'un contrôle total sur la page et son arborescence DOM.

En revanche, 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 concernant la façon dont la page est implémentée, sur lesquelles le script Puppeteer n'a aucun contrôle.

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 pour une application Web contenant le nœud <button>Submit</button> en tant que troisième enfant de l'élément body. Voici à quoi pourrait ressembler un extrait de 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 "Envoyer", mais il est étroitement lié à cette version de la page Web. Si un élément est ajouté par la suite au-dessus du bouton, ce sélecteur ne fonctionne plus.

Ce n'est pas une bonne nouvelle pour les rédacteurs de tests: les utilisateurs de Puppeteer essaient déjà de choisir des sélecteurs robustes pour de tels changements. Dans cette quête, nous fournissons aux utilisateurs un nouvel outil Puppetaria.

Puppeteer est désormais fourni avec un autre gestionnaire de requêtes basé sur l'interrogation de l'arborescence d'accessibilité au lieu de s'appuyer sur des 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é.

Ces sélecteurs sont nommés "sélecteurs ARIA" et acceptons 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. Elles ne sont pas liées aux propriétés syntaxiques du DOM, mais à des 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();

Maintenant, si nous décidons ultérieurement de remplacer le contenu textuel Submit par Done, le test échouera à nouveau, mais dans ce cas, c'est souhaitable. En changeant le nom du bouton, nous modifions le contenu de la page, par opposition à sa présentation visuelle ou à la façon dont elle est structurée dans le DOM. Nos tests doivent nous avertir de ces modifications pour nous assurer qu'elles sont intentionnelles.

Pour en revenir à l'exemple plus large avec 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.

De manière plus générale, 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.
  • Rendez les scripts de test plus lisibles (les noms accessibles sont des descripteurs sémantiques).
  • Encourager les bonnes pratiques à suivre pour attribuer des propriétés d'accessibilité aux éléments

Le reste de cet article décrit en détail 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 en fonction de leur nom et de leur rôle accessibles. Il s'agit des propriétés de l'arborescence d'accessibilité, qui correspond à celle de l'arborescence DOM habituelle. Elle est utilisée par les appareils tels que les lecteurs d'écran pour afficher des pages Web.

En examinant la spécification pour calculer le nom accessible, il est évident que le calcul du nom d'un élément n'est pas une mince affaire. Nous avons donc décidé dès le départ de réutiliser l'infrastructure existante de Chromium pour cela.

Notre approche de sa mise en œuvre

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 affiche une interface de débogage via un protocole appelé CDP (Chrome DevTools Protocol). Cela expose des fonctionnalités telles que "actualiser la page" ou "exécuter cet extrait de code JavaScript dans la page et renvoyer le résultat" via une interface indépendante de la langue.

Le frontal des outils de développement et Puppeteer utilisent tous deux la plate-forme 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: le navigateur, le moteur de rendu, etc. CDP se charge d’acheminer les commandes au bon endroit.

Les actions Puppeteer, telles que l'interrogation, les clics et l'évaluation d'expressions, sont effectuées à l'aide de 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, telles que l'émulation d'une déficience de la vision des couleurs, la prise de captures d'écran ou la capture de 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:

  • Écrivez notre logique de requête en JavaScript et injectez-la 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 mis en œuvre 3 prototypes:

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

Traversée DOM JS

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

Traversée du marionnettiste AXTree

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

Traversée 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, l'interrogation peut avoir lieu sur le backend via une implémentation C++ plutôt que dans le contexte de la page via JavaScript.

Analyse comparative des tests unitaires

La figure suivante compare la durée d'exécution totale d'interrogation de quatre éléments 1 000 fois pour les trois prototypes. L'analyse comparative a été exécutée selon 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 des interrogations sur quatre éléments 1 000 fois

Il est évident qu'il existe un écart de performances important entre le mécanisme de requête basé sur la CDP et les deux autres, implémentés uniquement dans Puppeteer, et la différence relative semble s'accroître 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 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.

Lors du balayage DOM JS, nous demandons le nom et le rôle accessibles de chaque élément. Ainsi, 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 CDP, l'arborescence n'est supprimée qu'entre chaque appel à la 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 lors des 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, cela engendre un coût d'utilisation de mémoire supplémentaire. Pour les scripts Puppeteer qui, par exemple, enregistrent les fichiers de suivi, cela peut poser problème. 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 la mise en cache eux-mêmes en activant le domaine d'accessibilité du CDP.

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

Le benchmark précédent a montré que la mise en œuvre de notre mécanisme de requête au niveau de la couche CDP permet d'améliorer les performances dans un scénario de tests unitaires cliniques.

Pour voir si la différence est suffisamment prononcée pour être visible dans un scénario plus réaliste d'exécution d'une suite de tests complète, nous avons appliqué un 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 nous avons comparé les environnements d'exécution. Dans cette analyse comparative, nous avons remplacé au total 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. 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 étant de 2 253, seule une partie des sélections de requêtes est issue des 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 conclure quoi que ce soit de spécifique, mais il est clair que l'écart de performances entre les deux prototypes se reflète également dans ce scénario.

Un nouveau point de terminaison CDP

Compte tenu des benchmarks ci-dessus, et comme l'approche basée sur les indicateurs de lancement n'était généralement pas souhaitable, 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, le point de terminaison doit utiliser ce que l'on appelle RemoteObjectIds comme argument et, pour nous permettre de trouver les éléments DOM correspondants par la suite, il doit renvoyer une liste d'objets contenant la backendNodeIds pour ces éléments.

Comme le montre le graphique ci-dessous, nous avons essayé plusieurs approches adaptées à cette interface. Nous avons constaté que la taille des objets renvoyés (c'est-à-dire si nous renvoyions ou non des nœuds d'accessibilité complets ou seulement le backendNodeIds) ne présentait aucune différence visible. En revanche, nous avons constaté que l'utilisation de la NextInPreOrderIncludingIgnored existante n'était pas adaptée pour implémenter la logique de balayage dans ce contexte, car cela entraînait un ralentissement notable.

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

En résumé

Maintenant que le point de terminaison CDP est en place, nous avons implémenté le gestionnaire de requêtes du côté de Puppeteer. Le grognement lié à ce travail consistait à restructurer le code de gestion des requêtes pour permettre aux requêtes de se résoudre directement via CDP plutôt que 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 sommes impatients de voir comment les utilisateurs l'intégreront dans leurs scripts de test et nous sommes impatients de recevoir vos idées pour rendre cette fonctionnalité encore plus utile !

Télécharger les canaux de prévisualisation

Nous vous conseillons d'utiliser Chrome Canary, Dev ou Beta 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 pointe de plates-formes Web et de détecter les problèmes sur votre site avant même que vos utilisateurs ne le fassent.

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

Utilisez les options suivantes pour discuter des nouvelles fonctionnalités et des modifications dans le message, ou de tout autre sujet lié aux outils de développement.

  • Envoyez-nous une suggestion ou des commentaires via crbug.com.
  • Signalez un problème lié aux outils de développement en accédant à Plus d'options   More > Aide > Signaler un problème dans les outils de développement.
  • Envoyez un tweet à @ChromeDevTools.
  • Laissez des commentaires sur les nouveautés des outils de développement vidéos YouTube ou les vidéos YouTube de nos conseils sur les outils de développement.