O Puppeteer e sua abordagem aos seletores
Puppeteer é uma biblioteca de automação de navegadores para Node. Com ela, é possível controlar um navegador usando uma API JavaScript simples e moderna.
A tarefa mais importante do navegador é, obviamente, navegar em páginas da web. Automatizar essa tarefa basicamente equivale a automatizar interações com a página da Web.
No Puppeteer, isso é possível ao consultar elementos DOM usando seletores baseados em string e realizar ações como clicar ou digitar texto nos elementos. Por exemplo, um script que abre developer.google.com, encontra a caixa de pesquisa e as pesquisas por puppetaria
pode ter esta aparência:
(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');
})();
A forma como os elementos são identificados usando seletores de consulta é, portanto, uma parte definidora da experiência do Puppeteer. Até agora, os seletores no Puppeteer estavam limitados a seletores de CSS e XPath que, embora expressivos muito eficientes, podem ter desvantagens para interações persistentes do navegador em scripts.
Seletores sintáticos x semânticos
Os seletores de CSS são sintáticos por natureza. eles estão fortemente vinculados ao funcionamento interno da representação textual da árvore do DOM, no sentido de que fazem referência a IDs e nomes de classe do DOM. Assim, eles oferecem uma ferramenta integral para que desenvolvedores Web modifiquem ou adicionem estilos a um elemento em uma página, mas nesse contexto o desenvolvedor tem controle total sobre a página e a árvore do DOM.
Por outro lado, o script do Puppeteer é um observador externo de uma página. Portanto, quando os seletores CSS são usados nesse contexto, ele introduz suposições ocultas sobre como a página é implementada sobre as quais o script do 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 alguém use scripts do Puppeteer para fazer testes automatizados com 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 ter esta aparência:
const button = await page.$('body:nth-child(3)'); // problematic selector
await button.click();
Estamos usando o seletor 'body:nth-child(3)'
para encontrar o botão "Enviar", mas ele está diretamente vinculado a essa versão da página da Web. Se um elemento for adicionado posteriormente acima do botão, esse seletor não funcionará mais.
Isso não é novidade para os escritores de teste: os usuários do Puppeteer já tentam escolher seletores que são robustos para essas mudanças. Com a Puppetaria, os usuários terão uma nova ferramenta nesta quest.
O Puppeteer agora vem com um gerenciador de consultas alternativo baseado na consulta da árvore de acessibilidade, em vez de depender de seletores de CSS. A filosofia subjacente é que, se o elemento concreto que queremos selecionar não mudou, o nó de acessibilidade correspondente também não deveria ter mudado.
Chamamos esses seletores de "seletores ARIA" e oferecer suporte à consulta do nome acessível computado e do papel 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 a propriedades sintáticas do DOM, mas sim 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();
Agora, se mais tarde decidirmos mudar o conteúdo de texto do nosso botão de Submit
para Done
, o teste vai falhar outra vez, mas nesse caso isso é desejável. alterando o nome do botão, alteramos o conteúdo da página, em vez de sua apresentação visual ou de como ela é estruturada no DOM. Nossos testes precisam nos alertar sobre essas mudanças para garantir que elas sejam intencionais.
Voltando ao exemplo maior com a barra de pesquisa, podemos aproveitar o novo gerenciador aria
e substituir
const search = await page.$('devsite-search > form > div.devsite-search-container');
com
const search = await page.$('aria/Open search[role="button"]');
para localizar a barra de pesquisa!
De modo mais geral, acreditamos que o uso desses seletores ARIA pode oferecer os seguintes benefícios aos usuários do Puppeteer:
- Torne os seletores em scripts de teste mais resilientes a alterações no código-fonte.
- Torne os scripts de teste mais legíveis (nomes acessíveis são descritores semânticos).
- Motivar as práticas recomendadas para atribuir propriedades de acessibilidade a elementos.
O restante deste artigo aprofunda-se nos detalhes de como implementamos o projeto Puppetaria.
O processo de design
Contexto
Conforme motivado acima, queremos habilitar elementos de consulta pelo nome e função acessíveis. Essas são propriedades da árvore de acessibilidade, uma dupla da árvore DOM comum, que é usada por dispositivos como leitores de tela para mostrar páginas da Web.
Analisando a especificação para calcular o nome acessível, está claro que computar o nome de um elemento é uma tarefa não trivial. Portanto, desde o início, decidimos reutilizar a infraestrutura existente do Chromium para isso.
Como fizemos a implementação
Mesmo nos limitando a usar a árvore de acessibilidade do Chromium, há algumas maneiras pelas quais podemos implementar consultas ARIA no Puppeteer. Para entender o porquê, primeiro vamos ver como o Puppeteer controla o navegador.
O navegador expõe uma interface de depuração usando um protocolo chamado Protocolo do Chrome DevTools (CDP, na sigla em inglês). Isso expõe funcionalidades como "recarregar a página" ou "execute este JavaScript na página e entregue o resultado" por meio de uma interface independente de idioma.
O front-end do DevTools e o Puppeteer usam o CDP para se comunicar com o navegador. Para implementar os comandos do CDP, há uma infraestrutura do DevTools dentro de todos os componentes do Chrome: no navegador, no renderizador e assim por diante. O CDP é responsável por rotear os comandos para o local certo.
As ações do Puppeteer, como consultar, clicar e avaliar expressões, são realizadas usando comandos do CDP, como Runtime.evaluate
, que avalia o JavaScript diretamente no contexto da página e retorna o resultado. Outras ações do Puppeteer, como emular deficiências na 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.
Isso já nos deixa com dois caminhos para implementar nossa funcionalidade de consulta: podemos:
- Escreva nossa lógica de consulta em JavaScript e injete-a na página usando
Runtime.evaluate
. - Use um endpoint de CDP que possa acessar e consultar a árvore de acessibilidade diretamente no processo do Blink.
Implementamos 3 protótipos:
- Travessia de DOM do JS: baseada na injeção de JavaScript na página
- Puppeteer AXTree traversal: com base no uso do acesso do CDP à árvore de acessibilidade.
- Travessia de CDP DOM: usando um novo endpoint de CDP projetado especificamente para consultar a árvore de acessibilidade
Travessia de DOM do JS
Esse protótipo faz uma travessia completa do DOM e usa element.computedName
e element.computedRole
, controlados pela flag de inicialização ComputedAccessibilityInfo
, para extrair o nome e a função de cada elemento durante a travessia.
Travessia AXTree do Puppeteer
Aqui, em vez disso, recuperamos a árvore de acessibilidade completa pelo CDP e a transferimos no Puppeteer. Os nós de acessibilidade resultantes são, então, mapeados para nós DOM.
Travessia de DOM do CDP
Para este protótipo, implementamos um novo endpoint de CDP especificamente para consultar a árvore de acessibilidade. Dessa forma, a consulta pode acontecer no back-end com uma implementação em C++ em vez de no contexto da página via JavaScript.
Comparativo de teste de unidade
A figura a seguir compara o tempo de execução total da consulta de quatro elementos mil vezes para os 3 protótipos. A comparação foi executada em três configurações diferentes, variando o tamanho da página e a ativação do armazenamento em cache de elementos de acessibilidade.
Está bem claro que há uma lacuna de desempenho considerável entre o mecanismo de consulta apoiado pelo CDP e os dois outros implementados exclusivamente no Puppeteer, e a diferença relativa parece aumentar drasticamente com o tamanho da página. É interessante ver que o protótipo de travessia de DOM do 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 descartada após cada interação, se o domínio estiver desativado. Ativar o domínio faz com que o Chromium armazene a árvore calculada em cache.
Para a travessia de DOM do JS, solicitamos o nome acessível e o papel de 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 só é descartada entre cada chamada para o CDP, ou seja, para cada consulta. Essas abordagens também se beneficiam da ativação do armazenamento em cache, já que a árvore de acessibilidade é mantida nas chamadas do CDP, mas o aumento de desempenho é, portanto, comparativamente menor.
Mesmo que a ativação do armazenamento em cache pareça interessante aqui, ela tem um custo de uso adicional da memória. Para scripts do Puppeteer que, por exemplo, grava 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 por conta própria ativando o domínio de acessibilidade do CDP.
Comparativo do pacote de testes do DevTools
O comparativo anterior mostrou que a implementação do nosso mecanismo de consulta na camada CDP melhora o desempenho em um cenário de teste de unidade clínico.
Para ver se a diferença é pronunciada o suficiente para ser perceptível em um cenário mais realista da execução de um pacote de testes completo, corrigimos o pacote de testes completo do DevTools para usar os protótipos baseados em JavaScript e CDP e comparamos os ambientes de execução. Neste comparativo, 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. Dessa forma, o número real de execuções do gerenciador de consultas aria
foi 113 por execução do pacote. O número total de seleções de consulta foi de 2.253, portanto, apenas uma fração dessas seleções aconteceu por meio dos protótipos.
Como mostrado na figura acima, há uma diferença perceptível no tempo de execução total. Os dados são muito barulhentos para concluir algo específico, mas está claro que a lacuna de desempenho entre os dois protótipos também aparece nesse cenário.
Um novo endpoint de CDP
À luz dos comparativos de mercado acima e como a abordagem baseada em sinalizações de lançamento era indesejável em geral, decidimos prosseguir com a implementação de um novo comando do CDP para consultar a árvore de acessibilidade. Agora, tínhamos 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 posteriormente, ele precisa retornar uma lista de objetos que contêm o backendNodeIds
dos elementos DOM.
Conforme mostrado no gráfico abaixo, testamos algumas abordagens que satisfazem 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 fazia diferença perceptível. Por outro lado, descobrimos que usar o NextInPreOrderIncludingIgnored
existente era uma má escolha para implementar a lógica de travessia aqui, porque isso produzia uma lentidão perceptível.
Conclusão
Agora, com o endpoint do CDP em vigor, implementamos o gerenciador de consultas no lado do Puppeteer. O grunho do trabalho aqui foi reestruturar o código de processamento de consultas para permitir que as consultas fossem resolvidas diretamente pelo CDP, em vez de fazerem consultas por meio do JavaScript avaliado no contexto da página.
A seguir
O novo gerenciador aria
incluído no Puppeteer v5.4.0 como gerenciador de consultas integrado. Estamos ansiosos para ver como os usuários o adotarão nos scripts de teste e mal podemos esperar para ouvir suas ideias sobre como podemos tornar esse recurso ainda mais útil.
Fazer o download dos canais de visualização
Use o Chrome Canary, Dev ou Beta como seu navegador de desenvolvimento padrão. Esses canais de pré-lançamento dão acesso aos recursos mais recentes do DevTools, testam APIs modernas da plataforma Web e encontram problemas no seu site antes que os usuários o façam!
Entrar em contato com a equipe do Chrome DevTools
Use as opções a seguir para discutir os novos recursos e mudanças na postagem ou qualquer outro assunto relacionado ao DevTools.
- Envie uma sugestão ou feedback pelo site crbug.com.
- Informe um problema do DevTools usando Mais opções
> Ajuda > Relate problemas no DevTools no DevTools.
- Envie um tweet em @ChromeDevTools.
- Deixe comentários nos vídeos do YouTube sobre as novidades do DevTools ou nos vídeos do YouTube com dicas sobre o DevTools.