Puppetaria: secuencias de comandos de Puppeteer centrados en la accesibilidad

Johan Bay
Johan Bay

Puppeteer y su enfoque sobre 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 de los navegadores 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, puedes consultar los elementos del DOM con selectores basados en cadenas y realizar acciones como hacer clic en los elementos o escribir en ellos. 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 los selectores de consultas es una parte que define la experiencia de Puppeteer. Hasta ahora, los selectores de Puppeteer estaban limitados a los selectores CSS y XPath que, si bien son muy potentes a nivel expresivo, pueden tener desventajas para las interacciones persistentes del navegador en las secuencias de comandos.

Selectores sintácticos frente a 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 ID y nombres de clase del DOM. De este modo, brindan una herramienta integral para que los desarrolladores web puedan 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 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, introduce 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 frágiles y susceptibles a cambios en el código fuente. Por ejemplo, supongamos que uno 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 luego se agrega un elemento sobre el botón, este selector ya no funcionará.

Esto no es una novedad para los escritores de prueba: los usuarios de Puppeteer ya intentan elegir selectores que sean sólidos ante esos cambios. Con Puppetaria, brindamos a los usuarios una nueva herramienta en esta misión.

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

Llamamos a estos selectores "selectores de ARIA" y admitimos consultas para el nombre de accesibilidad 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 vinculados 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 secuencia de comandos de prueba anterior, en su lugar, podríamos usar el selector aria/Submit[role="button"] para elegir 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 bien, si luego decidimos cambiar el contenido de texto de nuestro botón de Submit a Done, la prueba volverá a fallar, pero en este caso es lo deseable. Si cambiamos el nombre del botón, cambiamos el contenido de la página en lugar de su presentación visual o su estructura en el DOM. Nuestras pruebas deben advertirnos sobre dichos 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 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 del código fuente
  • Haz que las secuencias de comandos de prueba sean más legibles (los nombres accesibles son descriptores semánticos).
  • Motivar las prácticas recomendadas para asignar propiedades de accesibilidad a los elementos.

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

El proceso de diseño

Información general

Como lo motivamos anteriormente, queremos habilitar los elementos de consulta por sus nombres y roles accesibles. Estas son propiedades del árbol de accesibilidad, una característica doble del árbol de DOM habitual, que usan dispositivos como los lectores de pantalla para mostrar páginas web.

Al mirar la especificación para procesar el nombre de accesibilidad, 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

Aunque nos limitemos a usar el árbol de accesibilidad de Chromium, hay varias formas en las que podríamos implementar las consultas de ARIA en Puppeteer. Para conocer los motivos, 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). De esta manera, se exponen funcionalidades 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. 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 en color, tomar capturas de pantalla o capturar registros, usan CDP para comunicarse directamente con el proceso de renderización de Blink.

CDP

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

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

Implementamos 3 prototipos:

  • Recorrido de JS DOM: Se basa en la inserción de JavaScript en la página.
  • Recorrido de Puppeteer AXTree: Se basa en el acceso de CDP existente al árbol de accesibilidad.
  • Recorrido del CDP DOM: 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 en la marca de lanzamiento ComputedAccessibilityInfo, para recuperar el nombre y el rol de cada elemento durante el recorrido.

Recorrido de Puppeteer AXTree

En su lugar, recuperamos el árbol de accesibilidad completo a través de CDP y lo atravesamos en Puppeteer. Los nodos de accesibilidad resultantes luego 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, las consultas pueden ocurrir en el backend a través de una implementación de C++ en lugar de hacerlo en el contexto de la página a través de JavaScript.

Comparativas de prueba de unidades

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

Comparativa: Es el tiempo de ejecución total de la consulta de cuatro elementos 1,000 veces.

Está bastante claro que existe una brecha de rendimiento considerable entre el mecanismo de consulta respaldado por CDP y los dos otros implementados únicamente en Puppeteer, y la diferencia relativa parece aumentar drásticamente con el tamaño de la página. Es un poco interesante ver que el prototipo de recorrido del DOM de JS responde tan bien al permitir el almacenamiento en caché de accesibilidad. Con el almacenamiento en caché inhabilitado, 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 en su lugar.

Para el recorrido del DOM de JS, solicitamos el nombre y rol accesibles para cada elemento durante el recorrido, por lo que si el almacenamiento en caché está inhabilitado, Chromium computa y descarta el árbol de accesibilidad para 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, por cada consulta. Estos enfoques también se benefician de habilitar el almacenamiento en caché, ya que el árbol de accesibilidad luego persiste en todas las llamadas de CDP, pero el aumento del rendimiento es comparativamente menor.

Aunque habilitar el almacenamiento en caché parece conveniente en este caso, tiene un costo de uso de memoria adicional. En el caso de las secuencias de comandos de Puppeteer que, p. ej., registran los archivos de registro, esto podría ser problemático. 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 de CDP.

Comparativas del paquete de pruebas de Herramientas para desarrolladores

La comparativa anterior mostró que la implementación de nuestro mecanismo de consulta en la capa de CDP proporciona un aumento del rendimiento en una situación clínica de prueba de unidades.

Para ver si la diferencia se pronuncia lo suficientemente pronunciada como para destacarla en una situación más realista de ejecutar un conjunto de pruebas completo, aplicamos un parche al conjunto de pruebas de extremo a extremo de Herramientas para desarrolladores a fin de usar los prototipos basados en JavaScript y CDP y comparar 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. El número 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: 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 como para concluir algo específico, pero está claro que la brecha de rendimiento entre los dos prototipos también se muestra en este escenario.

Un extremo de CDP nuevo

En función de las comparativas anteriores, y como el enfoque basado en marcas de lanzamiento no era conveniente en general, decidimos avanzar con la implementación de un nuevo comando de CDP para consultar el árbol de accesibilidad. Ahora, teníamos que averiguar 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 más adelante los elementos del DOM correspondientes, debería mostrar una lista de objetos que contenga backendNodeIds para los elementos del DOM.

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

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

Resumen

Ahora, con el extremo de CDP implementado, implementamos el controlador de consultas en el lado de Puppeteer. El grueso del trabajo aquí fue reestructurar el código de manejo de consultas para permitir que las consultas se resuelvan directamente a través de CDP en lugar de realizar consultas a través de JavaScript evaluado en el contexto de la página.

¿Qué sigue?

El nuevo controlador aria se incluyó con Puppeteer v5.4.0 como un controlador de consultas integrado. Estamos ansiosos por ver cómo los usuarios la adoptarán en sus secuencias de comandos de prueba y estamos ansiosos por escuchar sus 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 navegadores de desarrollo predeterminados. Estos canales de vista previa te brindan acceso a las funciones más recientes de Herramientas para desarrolladores, prueban API de plataforma web de vanguardia y detectan problemas en tu sitio antes que los usuarios.

Comunicarse 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 otra cosa relacionada con Herramientas para desarrolladores.

  • Para enviarnos sugerencias o comentarios, accede a crbug.com.
  • Para informar un problema de Herramientas para desarrolladores, use Más opciones   Más   > Ayuda > Informar problemas de Herramientas para desarrolladores en Herramientas para desarrolladores.
  • Twittea a @ChromeDevTools.
  • Deja comentarios en nuestros videos de YouTube de Herramientas para desarrolladores o en videos de YouTube de las Sugerencias de las Herramientas para desarrolladores.