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, en esencia, a automatizar las interacciones con la página web.

En Puppeteer, esto se logra mediante la consulta de elementos DOM con selectores basados en cadenas y la realización de 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 consulta es una parte definitoria de la experiencia de Puppeteer. Hasta ahora, los selectores de Puppeteer se limitaban a los selectores CSS y XPath que, si bien son muy potentes de forma expresiva, pueden tener inconvenientes para conservar las interacciones del navegador en las secuencias de comandos.

Selectores sintácticos frente a selectores semánticos

Los selectores de CSS son de naturaleza sintáctica; están estrechamente vinculados al funcionamiento interno de la representación textual del árbol DOM en el sentido de que hacen referencia a IDs y nombres de clase del DOM. Por lo tanto, proporcionan una herramienta integral para los desarrolladores web para modificar o agregar estilos a un elemento en una página, pero en ese contexto, el desarrollador tiene control total sobre la página y su árbol de 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 de 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 esas secuencias de comandos pueden ser inestables y susceptibles a cambios en el código fuente. Supongamos, por ejemplo, que se usan secuencias de comandos de Puppeteer para 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 este está estrechamente vinculado exactamente a esta versión de la página web. Si más adelante se agrega un elemento sobre el 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 resistentes a esos cambios. Con Puppetaria, les brindamos a los usuarios una nueva herramienta en esta búsqueda.

Puppeteer ahora se envía con un controlador de consultas alternativo basado en la consulta del árbol de accesibilidad en lugar de depender de los selectores de 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.

Llamamos a esos selectores "selectores ARIA" y admitimos la consulta del nombre y el rol accesibles calculados 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 la secuencia de comandos de prueba anterior, en su lugar, 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 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 es deseable. Cuando cambiamos 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 encontrar la barra de búsqueda.

En términos más generales, creemos que el uso de estos 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 prácticas recomendadas para asignar propiedades de accesibilidad a los elementos.

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

El proceso de diseño

Segundo plano

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

Después de analizar la especificación para calcular 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 volver a usar la infraestructura existente de Chromium para esto.

Cómo abordamos la implementación

Incluso si nos limitamos a usar el árbol de accesibilidad de Chromium, hay varias formas en las que podríamos implementar la consulta 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 Chrome DevTools (CDP). Esto expone funciones como "volver a cargar la página" o "ejecutar este fragmento de JavaScript en la página y mostrar 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 una infraestructura de DevTools 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úan JavaScript directamente en el contexto de la página y devuelven el resultado. Otras acciones de Puppeteer, como emular la deficiencia de visión en color, 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 opciones para implementar nuestra funcionalidad de consulta:

  • Escribir nuestra lógica de consulta en JavaScript y, luego, insertarla 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 del CDP: Usa un nuevo extremo del CDP diseñado específicamente para consultar el árbol de accesibilidad.

Recorrido de DOM de JS

Este prototipo realiza un recorrido completo del DOM y usa element.computedName y element.computedRole, con control de acceso en la marca de lanzamiento ComputedAccessibilityInfo, para recuperar el nombre y el rol de cada elemento durante el recorrido.

Recorrido de AXTree de Puppeteer

En su lugar, recuperamos el árbol de accesibilidad completo a través de CDP y lo recorremos en Puppeteer. Los nodos de accesibilidad resultantes se asignan a los nodos 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 consultar cuatro elementos 1,000 veces para los 3 prototipos. Las comparativas se ejecutaron en 3 configuraciones diferentes que varían el tamaño de la página y si se habilitó o no la caché de los elementos de accesibilidad.

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

Está bastante claro que existe una brecha considerable en el rendimiento entre el mecanismo de consulta respaldado por CDP y los otros dos implementados solo en Puppeteer, y la diferencia relativa parece aumentar de forma significativa 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. Si 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. Si habilitas el dominio, Chromium almacenará en caché el árbol calculado.

Para la navegación por el DOM de JS, solicitamos el nombre y el rol accesibles de cada elemento durante la navegación, de modo que, si se inhabilita el almacenamiento en caché, Chromium calcula y descarta el árbol de accesibilidad de cada elemento que visitamos. Por otro lado, en el caso de 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 todas las llamadas a CDP, pero el aumento de rendimiento es comparativamente menor.

Aunque habilitar el almacenamiento en caché parece conveniente, tiene el costo de un uso adicional de memoria. Esto podría ser un problema para las secuencias de comandos de Puppeteer que, por ejemplo, registran archivos de seguimiento. Por lo tanto, decidimos no habilitar la caché de árboles de accesibilidad de forma predeterminada. Los usuarios pueden activar la caché por su cuenta habilitando el dominio de accesibilidad de 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 unidad 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 conjunto de pruebas completo, aplicamos un parche al conjunto de pruebas de extremo a extremo de DevTools 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 2253, por lo que solo una fracción de las selecciones de consultas se realizó a través de los prototipos.

Comparativa: conjunto de pruebas de extremo a extremo

Como se ve en la figura anterior, hay una diferencia discernible en el tiempo de ejecución total. Los datos tienen demasiado ruido para concluir algo específico, 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 función de las comparativas anteriores y dado que el enfoque basado en la marca de lanzamiento no era deseable en general, decidimos seguir adelante con la implementación de un nuevo comando de 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 permitirnos encontrar los elementos DOM correspondientes después, debe mostrar una lista de objetos que contenga el backendNodeIds para los elementos DOM.

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

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

Resumen

Ahora que tenemos el extremo de CDP, implementamos el controlador de consultas en el lado de Puppeteer. La mayor parte del trabajo consistió en reestructurar el código de control de consultas para permitir que las consultas 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.

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 la adoptan en sus secuencias de prueba y no podemos esperar a escuchar tus ideas sobre cómo podemos hacer que 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 versión preliminar te brindan acceso a las funciones más recientes de DevTools, te permiten probar las APIs de plataformas web de vanguardia y te ayudan a encontrar problemas en tu sitio antes que tus usuarios.

Comunícate con el equipo de Chrome DevTools

Usa las siguientes opciones para hablar sobre las funciones nuevas, las actualizaciones o cualquier otro tema relacionado con DevTools.