Puppetaria: scripts Puppeteer com foco na acessibilidade

Baía Johan
Johan Bay

Puppeteer e sua abordagem para seletores

O Puppeteer é uma biblioteca de automação de navegador para o Node que permite controlar um navegador usando uma API JavaScript simples e moderna.

A tarefa mais proeminente do navegador é, obviamente, navegar em páginas da Web. Automatizar essa tarefa basicamente equivale à automatização das interações com a página da Web.

No Puppeteer, isso é possível com a consulta de elementos DOM usando seletores baseados em strings e com a execução de ações, como clicar ou digitar texto nos elementos. Por exemplo, um script que é aberto abre developer.google.com, encontra a caixa de pesquisa e procura por puppetaria da seguinte forma:

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

Portanto, como os elementos são identificados com o uso de seletores de consulta é uma parte de definição da experiência do Puppeteer. Até agora, os seletores no Puppeteer estavam limitados aos seletores CSS e XPath que, embora muito poderosos, podem ter desvantagens para a persistência de interações do navegador em scripts.

Seletores sintáticos e semânticos

Os seletores de CSS são de natureza sintática. Eles estão muito vinculados ao funcionamento interno da representação textual da árvore do DOM no sentido de que fazem referência a IDs e nomes de classes do DOM. Assim, elas oferecem uma ferramenta integral para desenvolvedores da Web modificarem ou adicionarem estilos a um elemento de uma página, mas nesse contexto o desenvolvedor tem controle total sobre a página e a árvore DOM.

Por outro lado, o script Puppeteer é um observador externo de uma página. Portanto, quando os seletores de CSS são usados nesse contexto, ele introduz suposições ocultas sobre como a página é implementada, sobre as quais o script Puppeteer não tem controle.

O efeito é que esses scripts podem ser frágeis e suscetíveis a alterações no código-fonte. Suponha, por exemplo, que um deles use scripts Puppeteer para testes automatizados de um aplicativo da Web que contenha o nó <button>Submit</button> como o terceiro filho do elemento body. Um snippet de um caso de teste pode ser assim:

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

Aqui, estamos usando o seletor 'body:nth-child(3)' para encontrar o botão "Enviar", mas ele está rigidamente vinculado a essa versão da página da Web. Se um elemento for adicionado mais tarde acima do botão, esse seletor não vai mais funcionar.

Isso não é uma novidade para os escritores de testes: os usuários do Puppeteer já tentam escolher seletores robustos para essas mudanças. Com a Puppetaria, oferecemos aos usuários uma nova ferramenta nesta missão.

Agora, a Puppeteer envia com um gerenciador de consulta alternativo baseado na consulta da árvore de acessibilidade em vez de depender de seletores de CSS. A filosofia subjacente é de que, se o elemento concreto que queremos selecionar não mudou, o nó de acessibilidade correspondente também não deve ter mudado.

Chamamos esses seletores de "seletores ARIA" e oferecemos suporte à consulta do nome acessível computado e da função da árvore de acessibilidade. Em comparação com os seletores de CSS, essas propriedades são de natureza semântica. Eles não estão vinculados às propriedades sintáticas do DOM, mas aos descritores de como a página é observada por meio de tecnologias assistivas, como leitores de tela.

No exemplo de script de teste acima, podemos usar o seletor aria/Submit[role="button"] para selecionar o botão desejado, em que Submit se refere ao nome acessível do elemento:

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

Se mais tarde decidirmos mudar o conteúdo de texto do nosso botão de Submit para Done, o teste falhará novamente, mas, nesse caso, isso é desejável. Ao mudar o nome do botão, mudamos o conteúdo da página, em vez da apresentação visual ou da estrutura dela no DOM. Nossos testes devem avisar sobre essas mudanças para garantir que elas sejam intencionais.

Voltando ao exemplo maior com a barra de pesquisa, poderíamos aproveitar o novo gerenciador aria e substituir

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

por

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

para localizar a barra de pesquisa.

De um modo mais geral, acreditamos que o uso desses seletores ARIA pode oferecer os seguintes benefícios aos usuários da Puppeteer:

  • tornar os seletores em scripts de teste mais resilientes a mudanças no código-fonte.
  • Torne os scripts de teste mais legíveis (nomes acessíveis são descritores semânticos).
  • Motive boas práticas para atribuir propriedades de acessibilidade a elementos.

O restante deste artigo trata dos detalhes de como implementamos o projeto Puppetaria.

O processo de design

Contexto

Como motivado acima, queremos permitir elementos de consulta por nome e função acessíveis. Essas são as propriedades da árvore de acessibilidade, uma combinação da árvore DOM normal, que é usada por dispositivos como leitores de tela para exibir páginas da Web.

Analisando a especificação para calcular o nome acessível, fica claro que computar o nome de um elemento não é uma tarefa trivial. Portanto, desde o início, decidimos que queríamos reutilizar a infraestrutura existente do Chromium para isso.

Como abordamos a implementação

Mesmo nos limitando a usar a árvore de acessibilidade do Chromium, há algumas maneiras de implementar a consulta ARIA no Puppeteer. Para entender o porquê, vamos primeiro ver como o Puppeteer controla o navegador.

O navegador expõe uma interface de depuração por meio de um protocolo chamado Chrome DevTools Protocol (CDP). Isso expõe funcionalidades como "recarregar a página" ou "executar esta parte do JavaScript na página e devolver o resultado" por meio de uma interface que não depende de linguagem.

Tanto o front-end do DevTools quanto o Puppeteer estão usando o CDP para se comunicar com o navegador. Para implementar os comandos do CDP, existe uma infraestrutura de DevTools em todos os componentes do Chrome: no navegador, no renderizador e assim por diante. O CDP é responsável por rotear os comandos para o lugar certo.

As ações do Puppeteer, como consultar, clicar e avaliar expressões, são realizadas usando os comandos do CDP, como o Runtime.evaluate, que avalia o JavaScript diretamente no contexto da página e retorna o resultado. Outras ações do Puppeteer, como emular a deficiência de visão de cores, fazer capturas de tela ou capturar rastros, usam o CDP para se comunicar diretamente com o processo de renderização do Blink.

CDP

Isso já nos deixa com dois caminhos para implementar a funcionalidade de consulta. Podemos:

  • Escreva nossa lógica de consulta em JavaScript e faça a injeção na página usando Runtime.evaluate.
  • Use um endpoint do CDP que possa acessar e consultar a árvore de acessibilidade diretamente no processo do Blink.

Implementamos três protótipos:

  • Travessia de DOM JS: baseada na injeção de JavaScript na página
  • Travessia de AXTree do Puppeteer (link em inglês): baseada no uso do acesso do CDP à árvore de acessibilidade
  • Travessia de DOM do CDP: usa um novo endpoint do CDP criado especificamente para consultar a árvore de acessibilidade

Travessia de DOM em JS

Esse protótipo faz uma travessia completa do DOM e usa element.computedName e element.computedRole, controlados pela flag de lançamento ComputedAccessibilityInfo, para recuperar o nome e a função de cada elemento durante a travessia.

Travessia de Puppeteer AXTree

Aqui, recuperamos a árvore de acessibilidade completa pelo CDP e a percorremos no Puppeteer. Os nós de acessibilidade resultantes são então mapeados para nós DOM.

Travessia de DOM do CDP

Neste protótipo, implementamos um novo endpoint do CDP especificamente para consultar a árvore de acessibilidade. Dessa forma, a consulta pode acontecer no back-end por meio de uma implementação em C++ em vez de no contexto da página via JavaScript.

Comparativo de mercado do teste de unidade

A figura a seguir compara o tempo de execução total da consulta de quatro elementos mil vezes para os três protótipos. A comparação foi executada em três configurações diferentes, variando o tamanho da página e se o armazenamento em cache de elementos de acessibilidade estava ativado ou não.

Comparativo de mercado: tempo de execução total da consulta de quatro elementos mil vezes

Está claro que há uma lacuna de desempenho considerável entre o mecanismo de consulta apoiado por CDP e os dois outros implementados exclusivamente no Puppeteer, e a diferença relativa parece aumentar drasticamente com o tamanho da página. É interessante observar que o protótipo de travessia de DOM de JS responde tão bem à ativação do armazenamento em cache de acessibilidade. Com o armazenamento em cache desativado, a árvore de acessibilidade é calculada sob demanda e descarta a árvore após cada interação se o domínio estiver desativado. A ativação do domínio faz com que o Chromium armazene em cache a árvore calculada.

Para a travessia de DOM de JS, pedimos o nome e a função acessíveis para cada elemento durante a travessia. Portanto, se o armazenamento em cache estiver desativado, o Chromium computa e descarta a árvore de acessibilidade para cada elemento visitado. Por outro lado, nas abordagens baseadas em CDP, a árvore é descartada apenas entre cada chamada ao CDP, ou seja, para todas as consultas. Essas abordagens também se beneficiam da ativação do armazenamento em cache, já que a árvore de acessibilidade é mantida em todas as chamadas do CDP, mas o aumento no desempenho é, portanto, comparativamente menor.

Embora a ativação do armazenamento em cache pareça desejável aqui, ela vem com um custo de uso adicional da memória. Para scripts da biblioteca Puppeteer que, por exemplo, registram arquivos de rastreamento, isso pode ser problemático. Por isso, decidimos não ativar o armazenamento em cache da árvore de acessibilidade por padrão. Os usuários podem ativar o armazenamento em cache ativando o domínio de acessibilidade do CDP.

Comparativo de mercado do pacote de testes do DevTools

O comparativo de mercado anterior mostrou que a implementação do nosso mecanismo de consulta na camada do CDP melhora a performance em um cenário de teste de unidade clínico.

Para ver se a diferença é significativa o suficiente para torná-la perceptível em um cenário mais realista de execução de um pacote de testes completo, aplicamos patches no pacote de testes de ponta a ponta do DevTools para usar os protótipos baseados em JavaScript e CDP e comparamos os tempos de execução. Neste comparativo de mercado, mudamos um total de 43 seletores de [aria-label=…] para um gerenciador de consultas personalizado aria/…, que implementamos usando cada um dos protótipos.

Alguns dos seletores são usados várias vezes em scripts de teste, de modo que o número real de execuções do gerenciador de consultas aria foi de 113 por execução do pacote. O número total de seleções de consultas foi 2.253, portanto, apenas uma fração das seleções de consulta ocorreu por meio dos protótipos.

Comparativo de mercado: pacote de testes e2e

Como mostrado na figura acima, há uma diferença perceptível no tempo de execução total. Os dados são muito ruidosos para concluir algo específico, mas é claro que a lacuna de desempenho entre os dois protótipos também aparece nesse cenário.

Um novo endpoint do CDP

Considerando os comparativos de mercado acima, e como a abordagem baseada em sinalização de lançamento era indesejável em geral, decidimos avançar com a implementação de um novo comando CDP para consultar a árvore de acessibilidade. Agora, tivemos que descobrir a interface desse novo endpoint.

Para nosso caso de uso no Puppeteer, precisamos que o endpoint use o chamado RemoteObjectIds como argumento e, para podermos encontrar os elementos DOM correspondentes, ele retornará uma lista de objetos que contém o backendNodeIds dos elementos DOM.

Como mostrado no gráfico abaixo, tentamos algumas abordagens para satisfazer essa interface. A partir disso, descobrimos que o tamanho dos objetos retornados, ou seja, se retornamos ou não nós de acessibilidade completos ou apenas o backendNodeIds não fez diferença perceptível. Por outro lado, descobrimos que usar o NextInPreOrderIncludingIgnored atual não era uma escolha adequada para implementar a lógica de travessia, já que isso produzia uma desaceleração perceptível.

Comparativo de mercado: comparação de protótipos de travessia do AXTree baseados em CDP

Resumindo

Agora, com o endpoint do CDP ativado, implementamos o gerenciador de consultas no lado do Puppeteer. A parte principal do trabalho aqui foi reestruturar o código de processamento de consultas para permitir que elas fossem resolvidas diretamente pelo CDP, e não pelo JavaScript avaliado no contexto da página.

A seguir

O novo gerenciador aria fornecido com o Puppeteer v5.4.0 como um gerenciador de consultas integrado. Estamos ansiosos para saber como os usuários vão adotar essa abordagem nos scripts de teste e mal podemos esperar para ouvir suas ideias sobre como podemos tornar isso ainda mais útil.

Fazer o download dos canais de visualização

Use o Chrome Canary, Dev ou Beta como navegador de desenvolvimento padrão. Esses canais de pré-visualização dão acesso aos recursos mais recentes do DevTools, testam as APIs de plataforma da Web modernas e encontram problemas no seu site antes que os usuários o encontrem.

Entrar em contato com a equipe do Chrome DevTools

Use as opções abaixo para discutir os novos recursos e mudanças na postagem ou qualquer outro assunto relacionado ao DevTools.

  • Envie uma sugestão ou feedback em crbug.com.
  • Informe um problema do DevTools em Mais opções   Mais   > Ajuda > Informar problemas no DevTools.
  • Publique no Twitter em @ChromeDevTools.
  • Deixe comentários nos vídeos do YouTube sobre o que há de novo ou nos vídeos do YouTube de dicas sobre o DevTools.