Puppetaria: secuencias de comandos de Puppeteer centrados en la accesibilidad

Johan Bay
Johan Bay

Puppeteer y su enfoque de los selectores

Puppeteer es una biblioteca de automatización de navegadores para Node: 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 páginas web. Automatizar esta tarea equivale, básicamente, a automatizar las interacciones con la página web.

En Puppeteer, esto se logra consultando elementos del DOM con selectores basados en cadenas y realizando acciones como hacer clic o escribir texto en los elementos. Por ejemplo, una secuencia de comandos que abre developer.google.com, encuentra el cuadro de búsqueda y busca puppetaria podría verse de la siguiente manera:

(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 definitoria de la experiencia de Puppeteer. Hasta ahora, los selectores de Puppeteer se limitaban a los selectores CSS y XPath que, aunque son muy potentes en cuanto a expresiones, pueden tener desventajas para conservar las interacciones del navegador en las secuencias de comandos.

Selectores sintácticos vs. semánticos

Los selectores CSS son de naturaleza sintáctica; están estrechamente vinculados al funcionamiento interno de la representación textual del árbol del DOM en el sentido de que hacen referencia a IDs y nombres de clases del DOM. Por lo tanto, 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 que, cuando se usan selectores CSS en este contexto, se introducen suposiciones ocultas sobre cómo se implementa la página, sobre las que la secuencia de comandos de Puppeteer no tiene control.

El efecto es que estas secuencias de comandos pueden ser frágiles y susceptibles a cambios en el código fuente. Supongamos, por ejemplo, que se usan 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 esto está estrechamente vinculado a exactamente esta versión de la página web. Si más adelante se agrega un elemento arriba del botón, este selector ya no funcionará.

Esto no es una novedad para los escritores de pruebas: los usuarios de Puppeteer ya intentan elegir selectores que sean robustos ante esos cambios. Con Puppetaria, les brindamos a los usuarios una nueva herramienta para esta búsqueda.

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 cambió, el nodo de accesibilidad correspondiente tampoco debería haber cambiado.

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

En el ejemplo de secuencia de comandos de prueba anterior, podríamos usar el selector aria/Submit[role="button"] para seleccionar el botón deseado, donde Submit se refiere al nombre de accesibilidad del elemento:

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

Ahora, si más adelante decidimos cambiar el contenido de texto de nuestro botón de Submit a Done, la prueba volverá a fallar, pero en este caso, eso es lo que se espera. Al cambiar el nombre del botón, cambiamos el contenido de la página, en lugar de su presentación visual o cómo se estructura en el DOM. Nuestras pruebas deberían advertirnos sobre esos cambios para garantizar que sean intencionales.

Volviendo 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 ubicar la barra de búsqueda.

En términos más generales, creemos que el uso de estos selectores de ARIA puede proporcionar los siguientes beneficios a los usuarios de Puppeteer:

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

En el resto de este artículo, se detallan los pormenores de la implementación del proyecto Puppetaria.

El proceso de diseño

Fondo

Como se mencionó anteriormente, queremos habilitar la consulta de elementos por su nombre de accesibilidad y su rol. Estas son propiedades del árbol de accesibilidad, un elemento dual del árbol del DOM habitual, que usan dispositivos como lectores de pantalla para mostrar páginas web.

Si observamos la especificación para calcular el nombre de accesibilidad, queda claro que calcular el nombre de un elemento no es una tarea sencilla, por lo que, desde el principio, decidimos que queríamos reutilizar la infraestructura existente de Chromium para esto.

Cómo abordamos su implementación

Incluso si nos limitamos a usar el árbol de accesibilidad de Chromium, hay bastantes formas en las que podríamos implementar las consultas de ARIA en Puppeteer. Para ver 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 DevTools como Puppeteer usan CDP para comunicarse con el navegador. Para implementar los comandos de CDP, hay infraestructura de Herramientas para desarrolladores dentro de todos los componentes de Chrome: en el navegador, en el renderizador, 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 aprovechando los 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 cromática, tomar capturas de pantalla o capturar seguimientos, usan CDP para comunicarse directamente con el proceso de renderización de Blink.

CDP

Esto ya nos deja con dos rutas para implementar nuestra funcionalidad de consulta. Podemos hacer lo siguiente:

  • Escribir nuestra lógica de consulta en JavaScript y hacer que se inserte en la página con Runtime.evaluate
  • Usa un extremo de CDP que pueda acceder al árbol de accesibilidad y consultarlo directamente en el proceso de 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 uso del acceso existente de CDP al árbol de accesibilidad.
  • Recorrido del DOM de CDP: Usa un nuevo extremo de CDP diseñado específicamente 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, controlados por la marca de lanzamiento ComputedAccessibilityInfo, para recuperar el nombre y el rol de cada elemento durante el recorrido.

Recorrido de AXTree de Puppeteer

En este caso, recuperamos el árbol de accesibilidad completo a través de CDP y lo recorremos en Puppeteer. Luego, los nodos de accesibilidad resultantes se asignan a los 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 se puede realizar 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.

Comparativa de pruebas de unidades

En la siguiente figura, se compara el tiempo de ejecución total de la consulta de cuatro elementos 1,000 veces para los 3 prototipos. La comparativa se ejecutó en 3 configuraciones diferentes, en las que se varió el tamaño de la página y se habilitó o no el almacenamiento en caché de los elementos de accesibilidad.

Comparativa: Tiempo de ejecución total de la consulta de cuatro elementos 1,000 veces

Es 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 de forma drástica con el tamaño de la página. Es interesante ver que el prototipo de recorrido del DOM de JS responde tan bien a la habilitación del almacenamiento en caché de accesibilidad. Cuando se inhabilita el almacenamiento en caché, el árbol de accesibilidad se calcula a pedido y se 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, en 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 se conserva en las llamadas de CDP, pero el aumento del rendimiento es, por lo tanto, comparativamente menor.

Aunque habilitar el almacenamiento en caché parece deseable aquí, tiene un costo de uso de memoria adicional. Esto podría ser problemático para las secuencias de comandos de Puppeteer que, por ejemplo, registran archivos de seguimiento. 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 su cuenta habilitando el dominio de accesibilidad del CDP.

Comparativa del paquete de pruebas de DevTools

La comparativa anterior mostró que implementar nuestro mecanismo de consulta en la capa de CDP mejora el rendimiento en una situación de prueba de unidades clínica.

Para ver si la diferencia es lo suficientemente pronunciada como para que se note en una situación más realista de ejecución de un paquete de pruebas completo, parcheamos el paquete de pruebas de extremo a extremo de las Herramientas para desarrolladores para usar los prototipos basados en JavaScript y CDP, y comparamos los tiempos 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ó a través de los prototipos.

Comparativa: Paquete de pruebas de extremo a extremo

Como se ve en la figura anterior, hay una diferencia perceptible en el tiempo de ejecución total. Los datos están demasiado contaminados como para llegar a una conclusión específica, pero está claro que la brecha de rendimiento entre los dos prototipos también se muestra en esta situación.

Un nuevo extremo de CDP

En vista 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 de CDP para consultar el árbol de accesibilidad. Ahora, tuvimos que averiguar la interfaz de este nuevo extremo.

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

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

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

Conclusión

Ahora, con el extremo de CDP en su lugar, implementamos el controlador de consultas en el lado de Puppeteer. La mayor parte del trabajo aquí consistió en reestructurar el código de control de consultas para permitir que las consultas se resolvieran directamente a través de CDP en lugar de consultar a través de JavaScript evaluado en el contexto de la página.

Próximos pasos

El nuevo controlador aria se envió con Puppeteer v5.4.0 como un controlador de consultas integrado. Esperamos ver cómo los usuarios lo adoptan en sus secuencias de comandos de prueba y estamos ansiosos por escuchar tus ideas sobre cómo podemos hacerlo aún más útil.

Descarga los canales de versión preliminar

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

Comunícate con el equipo de Herramientas para desarrolladores de Chrome

Usa las siguientes opciones para analizar las funciones nuevas, las actualizaciones o cualquier otro tema relacionado con las Herramientas para desarrolladores.