Puppetaria: secuencias de comandos de Puppeteer centrados en la accesibilidad

Bahía de Johan
Johan Bay

Puppeteer y su enfoque hacia los selectores

Puppeteer es una biblioteca de automatización de navegadores para Node que te permite controlar un navegador con una API de JavaScript simple y moderna.

La tarea más importante del navegador es, por supuesto, navegar por las páginas web. Automatizar esta tarea básicamente equivale a automatizar las interacciones con la página web.

En Puppeteer, esto se logra consultando los elementos del DOM mediante selectores basados en cadenas y realizando acciones como hacer clic o escribir texto en los elementos. Por ejemplo, una secuencia de comandos que se abre para abrir developer.google.com, encuentra el cuadro de búsqueda y las búsquedas de puppetaria podrían verse así:

(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');
 })();

Por lo tanto, la forma en que se identifican los elementos con selectores de consultas es una parte fundamental de la experiencia de Puppeteer. Hasta ahora, los selectores en Puppeteer se limitaban a los selectores CSS y XPath que, aunque son muy potentes, pueden tener desventajas para las interacciones persistentes del navegador en las secuencias de comandos.

Selectores sintácticos y semánticos

Los selectores CSS son sintácticos por naturaleza; están estrechamente vinculados al funcionamiento interno de la representación textual del árbol del DOM en el sentido de que hacen referencia a los ID y los nombres de clase del DOM. Por ello, proporcionan una herramienta integral para que los desarrolladores web modifiquen o agreguen estilos a un elemento de una página, pero, en ese contexto, el desarrollador tiene control total sobre la página y su árbol del DOM.

Por otro lado, una secuencia de comandos de Puppeteer es un observador externo de una página. Por lo tanto, cuando se utilizan selectores CSS en este contexto, se presentan suposiciones ocultas sobre cómo se implementará la página, y la secuencia de comandos de Puppeteer no tiene control sobre ella.

Como resultado, esas secuencias de comandos pueden ser frágiles y susceptibles a cambios en el código fuente. Supongamos, por ejemplo, que usa secuencias de comandos de Puppeteer para realizar pruebas automatizadas de una aplicación web que contiene el nodo <button>Submit</button> como el tercer elemento secundario del elemento body. Un fragmento de un caso de prueba podría verse de la siguiente manera:

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

Aquí, usamos el selector 'body:nth-child(3)' para encontrar el botón de envío, pero está estrechamente vinculado a esta versión de la página web. Si posteriormente se agrega un elemento encima del botón, este selector dejará de funcionar.

Esto no es ninguna novedad para los escritores de prueba: los usuarios de Puppeteer ya intentan elegir selectores que sean resistentes a estos cambios. Con Puppetaria, les brindamos a los usuarios una nueva herramienta en esta Quest.

Puppeteer ahora incluye un controlador de consultas alternativo basado en la consulta del árbol de accesibilidad en lugar de depender de los selectores CSS. La filosofía subyacente aquí es que si el elemento concreto que queremos seleccionar no ha cambiado, el nodo de accesibilidad correspondiente tampoco debería haber cambiado.

Los nombres de estos selectores son "selectores ARIA" y admiten consultas para el nombre accesible calculado y la función del árbol de accesibilidad. En comparación con los selectores CSS, estas propiedades son de naturaleza semántica. No están vinculadas a las propiedades sintácticas del DOM, sino que describen cómo se observa la página a través de tecnologías de asistencia, como los lectores de pantalla.

En el ejemplo de la secuencia de comandos de prueba anterior, podríamos usar el selector aria/Submit[role="button"] para seleccionar el botón deseado, en el que Submit hace referencia al nombre accesible del elemento:

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

Ahora, si luego decidimos cambiar el contenido de texto de nuestro botón de Submit a Done, la prueba fallará nuevamente, pero, en este caso, lo conveniente. Cuando se cambia el nombre del botón, cambiamos el contenido de la página, en contraposición a su presentación visual o a la estructura del DOM. Nuestras pruebas deben advertirnos sobre esos cambios para garantizar que sean intencionales.

Si volvemos al ejemplo más grande con la barra de búsqueda, podríamos aprovechar el nuevo controlador aria y reemplazar

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

Con

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

para encontrar la barra de búsqueda.

En general, creemos que el uso de esos selectores ARIA puede proporcionar los siguientes beneficios a los usuarios de Puppeteer:

  • Hacer que los selectores en las secuencias de comandos de prueba sean más resistentes a los cambios en el código fuente
  • Haz que las secuencias de comandos de prueba sean más legibles (los nombres accesibles son descriptores semánticos).
  • Motiva las buenas prácticas para asignar propiedades de accesibilidad a los elementos.

En el resto de este artículo, se analizan en detalle cómo implementamos el proyecto Puppetaria.

El proceso de diseño

Información general

Como te motivamos con anterioridad, queremos habilitar los elementos de consulta por su nombre y rol accesibles. Estas son propiedades del árbol de accesibilidad, un árbol doble del DOM habitual, que usan dispositivos como los lectores de pantalla para mostrar páginas web.

Al mirar la especificación para computar el nombre accesible, queda claro que calcular el nombre de un elemento es una tarea no trivial, por lo que desde el principio decidimos que queríamos reutilizar la infraestructura existente de Chromium para esto.

Cómo abordamos la implementación

Incluso aunque nos limitemos al uso del árbol de accesibilidad de Chromium, existen varias formas de implementar la consulta de ARIA en Puppeteer. Para entender por qué, primero veamos cómo Puppeteer controla el navegador.

El navegador expone una interfaz de depuración a través de un protocolo llamado Protocolo de herramientas para desarrolladores de Chrome (CDP). Esto expone funciones como "volver a cargar la página" o "ejecutar este fragmento de JavaScript en la página y devolver el resultado" a través de una interfaz independiente del lenguaje.

Tanto el frontend de Herramientas para desarrolladores como Puppeteer usan CDP para comunicarse con el navegador. Para implementar comandos de CDP, hay infraestructura de Herramientas para desarrolladores dentro de todos los componentes de Chrome: en el navegador, en el procesador, etcétera. CDP se encarga de enrutar los comandos al lugar correcto.

Las acciones de Puppeteer, como consultar, hacer clic y evaluar expresiones, se realizan mediante comandos de CDP, como Runtime.evaluate, que evalúa JavaScript directamente en el contexto de la página y devuelve el resultado. Otras acciones de Puppeteer, como emular la deficiencia de visión de color, tomar capturas de pantalla o capturar seguimientos, usan CDP para comunicarse directamente con el proceso de renderización de Blink.

CDP

Esto nos deja dos rutas para implementar nuestra funcionalidad de consulta: podemos hacer lo siguiente:

  • Escribe nuestra lógica de consulta en JavaScript y la inserta en la página con Runtime.evaluate, o
  • Usa un extremo de CDP que pueda acceder al árbol de accesibilidad y consultarlo directamente en el proceso Blink.

Implementamos 3 prototipos:

  • Recorrido del DOM de JS: Se basa en la inserción de JavaScript en la página.
  • Recorrido de AXTree de Puppeteer: Se basa en el acceso de CDP existente al árbol de accesibilidad.
  • Recorrido del DOM de CDP: Usa un nuevo extremo de CDP diseñado para consultar el árbol de accesibilidad.

Recorrido del DOM de JS

Este prototipo realiza un recorrido completo del DOM y usa element.computedName y element.computedRole, restringidos por la marca de inicio ComputedAccessibilityInfo, para recuperar el nombre y el rol de cada elemento durante el recorrido.

Recorrido de AXTree de Puppeteer

Aquí, recuperaremos el árbol de accesibilidad completo de CDP y lo recorreremos en Puppeteer. Los nodos de accesibilidad resultantes se asignan a nodos del DOM.

Recorrido del DOM de CDP

Para este prototipo, implementamos un nuevo extremo de CDP específicamente para consultar el árbol de accesibilidad. De esta manera, la consulta puede ocurrir en el backend a través de una implementación de C++ en lugar de en el contexto de la página a través de JavaScript.

Comparativas de prueba de unidades

En la siguiente figura, se compara el tiempo de ejecución total de consulta de cuatro elementos 1,000 veces para los 3 prototipos. Las comparativas se ejecutaron en 3 configuraciones diferentes, que variaron el tamaño de la página y si estaba habilitado o no el almacenamiento en caché de los elementos de accesibilidad.

Comparativa: Tiempo de ejecución total para consultar cuatro elementos 1,000 veces

Está bastante claro que hay una brecha de rendimiento considerable entre el mecanismo de consulta respaldado por CDP y los otros dos implementados únicamente en Puppeteer, y la diferencia relativa parece aumentar drásticamente con el tamaño de la página. Es algo interesante ver que el prototipo de recorrido del DOM de JS responde tan bien a la habilitación del almacenamiento en caché de la accesibilidad. Cuando el almacenamiento en caché está inhabilitado, el árbol de accesibilidad se calcula según demanda y lo descarta después de cada interacción si el dominio está inhabilitado. Habilitar el dominio hace que Chromium almacene en caché el árbol calculado.

Para el recorrido del DOM de JS, solicitamos el nombre y el rol de accesibilidad de cada elemento durante el recorrido, por lo que si el almacenamiento en caché está inhabilitado, Chromium calcula y descarta el árbol de accesibilidad de cada elemento que visitamos. Por otro lado, para los enfoques basados en CDP, el árbol solo se descarta entre cada llamada a CDP, es decir, para cada consulta. Estos enfoques también se benefician de habilitar el almacenamiento en caché, ya que el árbol de accesibilidad persiste en las llamadas de CDP, pero el aumento del rendimiento es comparativamente menor.

Aunque es conveniente habilitar el almacenamiento en caché, esto implica un costo de uso adicional de memoria. En el caso de las secuencias de comandos de Puppeteer que, p. ej., registran archivos de registro, podrían ser un problema. Por lo tanto, decidimos no habilitar el almacenamiento en caché del árbol de accesibilidad de forma predeterminada. Los usuarios pueden activar el almacenamiento en caché por sí mismos habilitando el dominio de accesibilidad de CDP.

Comparativas del paquete de pruebas de Herramientas para desarrolladores

Las comparativas anteriores mostraron que la implementación de nuestro mecanismo de consulta en la capa de CDP aumenta el rendimiento en una situación de prueba de unidades clínicas.

A fin de comprobar si la diferencia es lo suficientemente marcada como para que se note en un escenario más realista de ejecución de un conjunto de pruebas completo, aplicamos un parche al conjunto de pruebas de extremo a extremo de Herramientas para desarrolladores para usar los prototipos basados en JavaScript y CDP, y comparamos los entornos de ejecución. En esta comparativa, cambiamos un total de 43 selectores de [aria-label=…] a un controlador de consultas personalizado aria/…, que luego implementamos con cada uno de los prototipos.

Algunos de los selectores se usan varias veces en las secuencias de comandos de prueba, por lo que la cantidad real de ejecuciones del controlador de consultas aria fue de 113 por ejecución del paquete. La cantidad total de selecciones de consultas fue de 2,253, por lo que solo una fracción de las selecciones de consultas se realizó mediante los prototipos.

Comparativa: Paquete de pruebas e2e

Como se ve en la figura anterior, hay una diferencia perceptible en el tiempo de ejecución total. Los datos son demasiado ruidosos para determinar algo específico, pero está claro que la brecha de rendimiento entre los dos prototipos también se muestra en este escenario.

Un nuevo extremo de CDP

A la luz de las comparativas anteriores, y dado que el enfoque basado en marcas de lanzamiento no era deseable en general, decidimos avanzar con la implementación de un nuevo comando CDP para consultar el árbol de accesibilidad. Ahora, tuvimos que descubrir la interfaz de este nuevo extremo.

Para nuestro caso de uso en Puppeteer, necesitamos que el extremo tome el llamado RemoteObjectIds como argumento y, para que podamos encontrar los elementos DOM correspondientes después, debería mostrar una lista de objetos que contenga el backendNodeIds para los elementos del DOM.

Como se ve en el siguiente gráfico, probamos varios enfoques para satisfacer esta interfaz. A partir de esto, descubrimos que el tamaño de los objetos mostrados, es decir, si mostramos o no nodos de accesibilidad completos o solo el backendNodeIds no generó ninguna diferencia perceptible. Por otro lado, descubrimos que usar el elemento NextInPreOrderIncludingIgnored existente no era una buena opción para implementar la lógica de recorrido aquí, ya que eso generó una desaceleración notable.

Comparativa: Comparación de prototipos de recorrido de AXTree basados en CDP

Resumen

Con el extremo de CDP listo, implementamos el controlador de consultas del lado de Puppeteer. Lo importante del trabajo aquí era reestructurar el código de control de consultas para permitir que estas se resuelvan directamente a través de CDP en lugar de consultar a través de JavaScript evaluado en el contexto de la página.

¿Qué sigue?

El nuevo controlador aria se incluye en Puppeteer v5.4.0 como controlador de consultas integrado. Estamos ansiosos por ver cómo los usuarios lo adoptarán en sus secuencias de comandos de prueba y esperamos conocer tus ideas sobre cómo podemos hacer que esto sea aún más útil.

Descarga los canales de vista previa

Considera usar Chrome Canary, Dev o Beta como tu navegador de desarrollo predeterminado. Estos canales de vista previa te brindan acceso a las funciones más recientes de Herramientas para desarrolladores, prueban las API de vanguardia de la plataforma web y te permiten encontrar problemas en tu sitio antes que los usuarios.

Cómo comunicarte con el equipo de Herramientas para desarrolladores de Chrome

Usa las siguientes opciones para hablar sobre las nuevas funciones y los cambios en la publicación, o cualquier otro aspecto relacionado con Herramientas para desarrolladores.

  • Envíanos una sugerencia o un comentario a través de crbug.com.
  • Informa un problema en Herramientas para desarrolladores con Más opciones   Más   > Ayuda > Informar problemas de Herramientas para desarrolladores en esta herramienta.
  • Envía un tweet a @ChromeDevTools.
  • Deje comentarios en las Novedades de los videos de YouTube de Herramientas para desarrolladores o en las sugerencias de Herramientas para desarrolladores los videos de YouTube.