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 sûr, de parcourir les pages Web. L'automatisation de cette tâche revient essentiellement à automatiser les interactions avec la page Web.
Dans Puppeteer, cela est possible 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, trouve le champ de recherche et recherche puppetaria pourrait 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ête est donc un élément déterminant de l'expérience Puppeteer. Jusqu'à présent, les sélecteurs dans 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 conserver les 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, car 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. Toutefois, dans ce contexte, le développeur a 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, cela introduit des hypothèses cachées sur la façon dont la page est implémentée, 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 tester automatiquement 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 scénario 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 il est étroitement lié à cette version exacte 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 nouveauté pour les rédacteurs de tests : les utilisateurs de Puppeteer essaient déjà de choisir des sélecteurs qui résistent à 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 voulons sélectionner n'a pas changé, le nœud d'accessibilité correspondant ne devrait pas avoir changé non plus.
Nous appelons ces sélecteurs "ARIA" et nous acceptons les requêtes pour le nom et le rôle accessibles calculés de l'arborescence d'accessibilité. Contrairement 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 décrivent plutôt la façon dont la page est observée par les 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 nous décidons ultérieurement de remplacer le contenu textuel de notre bouton par Done au lieu de Submit, le test échouera à nouveau. Dans ce cas, c'est le résultat souhaité : en modifiant le nom du bouton, nous modifions le contenu de la page, et non sa présentation visuelle ou sa structure dans le DOM. Nos tests doivent nous avertir de ces modifications pour s'assurer qu'elles sont intentionnelles.
Pour en revenir à l'exemple plus complet 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 trouver 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 :
- Rendre les sélecteurs des scripts de test plus résistants aux modifications du code source
- Rendre les scripts de test plus lisibles (les noms accessibles sont des descripteurs sémantiques)
- Encourager les bonnes pratiques pour l'attribution de propriétés d'accessibilité aux éléments.
Le reste de cet article explique en détail comment nous avons mis en œuvre le projet Puppetaria.
Le processus de conception
Arrière-plan
Comme indiqué ci-dessus, nous voulons permettre l'interrogation des éléments par leur nom accessible et leur rôle. Il s'agit de propriétés de l'arborescence d'accessibilité, un double de l'arborescence DOM habituelle, qui est utilisée par des appareils tels que les lecteurs d'écran pour afficher les pages Web.
En examinant la spécification pour le calcul du nom accessible, il est clair que le calcul du nom d'un élément n'est pas une tâche anodine. Nous avons donc décidé dès le départ de réutiliser l'infrastructure existante de Chromium à cette fin.
Comment nous avons abordé son implémentation
Même si nous nous limitons à l'utilisation de l'arborescence d'accessibilité de Chromium, il existe plusieurs façons d'implémenter l'interrogation 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é Chrome DevTools Protocol (CDP). Cela permet d'exposer des fonctionnalités telles que "recharger la page" ou "exécuter ce morceau de code JavaScript dans 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 le CDP pour communiquer avec le navigateur. Pour implémenter les commandes CDP, l'infrastructure DevTools est présente dans tous les composants de Chrome : dans le navigateur, dans le moteur de rendu, etc. La CDP se charge d'acheminer les commandes au bon endroit.
Les actions Puppeteer telles que l'interrogation, le clic et l'évaluation d'expressions sont effectuées en exploitant des commandes CDP telles que Runtime.evaluate, qui évalue le code JavaScript directement dans le contexte de la page et renvoie le résultat. D'autres actions Puppeteer, telles que l'émulation d'un déficit de vision des couleurs, la capture d'écran ou la capture de traces, utilisent le CDP pour communiquer directement avec le processus de rendu Blink.

Nous avons déjà deux façons d'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 qui peut accéder à l'arborescence d'accessibilité et l'interroger directement dans le processus Blink.
Nous avons implémenté trois prototypes :
- Parcours du DOM JS : basé sur l'injection de JavaScript dans la page
- Parcours de l'AXTree Puppeteer : basé sur l'utilisation de l'accès CDP existant à l'arborescence d'accessibilité
- Parcours DOM CDP : utilisation d'un nouveau point de terminaison CDP spécialement conçu pour interroger l'arborescence d'accessibilité
Parcours du DOM JS
Ce prototype effectue un parcours complet du DOM et utilise element.computedName et element.computedRole, contrôlés par l'option de lancement ComputedAccessibilityInfo, pour récupérer le nom et le rôle de chaque élément pendant le parcours.
Parcours de l'AXTree Puppeteer
Ici, nous récupérons l'arborescence d'accessibilité complète via CDP et la parcourons dans Puppeteer. Les nœuds d'accessibilité obtenus sont ensuite mappés aux nœuds DOM.
Parcours du 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 se faire sur le backend via une implémentation C++ au lieu de se faire dans le contexte de la page via JavaScript.
Benchmark de tests unitaires
La figure suivante compare la durée d'exécution totale de l'interrogation de quatre éléments 1 000 fois pour les trois prototypes. Le benchmark a été exécuté dans trois configurations différentes, en faisant varier la taille de la page et en activant ou non la mise en cache des éléments d'accessibilité.
Il est évident qu'il existe un écart de performances considérable entre le mécanisme de requête basé sur CDP et les deux autres implémentés uniquement dans Puppeteer. De plus, 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 du 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é. L'activation du domaine permet à Chromium de mettre en cache l'arborescence calculée.
Pour le parcours du DOM JS, nous demandons le nom accessible et le rôle de chaque élément pendant le parcours. Par conséquent, si la mise en cache est désactivée, Chromium calcule et supprime l'arborescence d'accessibilité de chaque élément que nous visitons. 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'arbre d'accessibilité est alors conservé entre les appels CDP, mais l'amélioration des performances est donc comparativement plus faible.
Même si l'activation de la mise en cache semble souhaitable ici, elle entraîne une utilisation supplémentaire de la mémoire. Cela peut poser problème pour les scripts Puppeteer qui, par exemple, enregistrent des fichiers de trace. 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é du CDP.
Analyse comparative de la suite de tests DevTools
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 être perceptible dans un scénario plus réaliste d'exécution d'une suite de tests complète, nous avons corrigé 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 ce benchmark, nous avons remplacé un total de 43 sélecteurs de [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 était de 2 253. Seule une fraction des sélections de requêtes a donc été effectuée via les prototypes.
Comme le montre la figure ci-dessus, il existe une différence notable dans la durée d'exécution totale. Les données sont trop bruitées pour en tirer une conclusion précise, 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 les options de lancement n'était pas souhaitable en général, nous avons décidé de mettre en œuvre une nouvelle commande CDP pour interroger l'arborescence d'accessibilité. Nous devions maintenant 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 prenne ce que l'on appelle des 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 les backendNodeIds pour les éléments DOM.
Comme le montre le graphique ci-dessous, nous avons essayé plusieurs approches répondant à cette interface. Nous avons ainsi constaté que la taille des objets renvoyés (c'est-à-dire si nous renvoyions des nœuds d'accessibilité complets ou seulement le backendNodeIds) ne faisait aucune différence notable. En revanche, nous avons constaté que l'utilisation de l'élément NextInPreOrderIncludingIgnored existant n'était pas un bon choix pour implémenter la logique de parcours, car cela entraînait un ralentissement notable.
En résumé
Maintenant que le point de terminaison CDP est en place, nous avons implémenté le gestionnaire de requêtes côté Puppeteer. Le plus gros du travail a consisté à restructurer le code de gestion des requêtes pour permettre leur résolution directement via le CDP au lieu d'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'adopteront dans leurs scripts de test, et nous avons hâte de connaître vos idées pour le rendre encore plus utile.
Télécharger les canaux de prévisualisation
Envisagez d'utiliser Chrome Canary, Dev ou Beta comme navigateur de développement par défaut. Ces canaux de prévisualisation vous donnent accès aux dernières fonctionnalités des outils de développement, vous permettent de tester des API de plate-forme 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.
- Envoyez-nous vos commentaires et demandes de fonctionnalités sur crbug.com.
- Signalez un problème lié aux outils de développement en accédant à Autres options > Aide > Signaler un problème lié aux outils de développement dans les outils de développement.
- Envoyez un tweet à @ChromeDevTools.
- Laissez des commentaires sur les vidéos YouTube "Nouveautés des outils de développement" ou les vidéos YouTube "Conseils sur les outils de développement".