Testes Automatizados de Acessibilidade

A11Y fazendo parte do seu dia a dia com Jest, aXe e Puppeteer.

Eduardo Rabelo
8 min readFeb 24, 2018
Um trio parada dura para ambientes de integração e testes E2E com acessibilidade! 💪 💅

Como desenvolvedores web, sempre tivemos ao nosso lado os padrões web. Tá certo que, para quem já está nesse barco a um tempo, essa afirmação não passa aquela segurança. Mas ó, pelo menos tem alguém tentando :P, tem melhorado ano após ano, ou, olhando por outro lado, ano após ano o navegador vilão e mocinho são alterados! (Estou 👀 para vocês Chrome/IE/Safari).

O que muitas vezes passa despercebido, é que Acessibilidade também é um padrão, e a web é Acessível por padrão, somos nós (e os Designers 👀) que gostamos de "pensar fora da caixa" (se é que esse termo se encaixa aqui).

Nossas soluções atuais

Se tem uma coisa que todas as aplicações tem (ok, nem todas :P) são testes. De unitários a funcionais, o que é importante é extrair valor dos seus testes.

Com a evolução de nossas ferramentas na linguagem JavaScript, hoje temos soluções bem sólidas para rodar testes, e uma delas é o Jest (você pode conferir meu guia sobre Jest aqui).

Também temos uma ferramentas para automatizar testes de acessibilidade em HTML chamada aXe. Que deixa bem simples a integração com diversas ferramentas. Vale lembra, que ele necessita de um navegador real, com sua aplicação rodando, para excutar os testes.

Até aqui, temos o seguinte:

  • Uma ótima plataforma para rodar testes, Jest
  • Uma ótima ferramenta para rodar testes de acessibilidade, aXe

O que está pendente aqui? Um navegador real para rodar os testes!

Como podemos resolver isso?

Resolvendo a última pendência

No final do ano passado, o time do Chrome DevTools lançou uma ferramenta chamada Puppeteer.

Puppeteer é um biblioteca Node, que disponibiliza uma API para controlar uma versão headless (sem a interface gráfica) do Chrome.

Ou seja, você pode rodar o Chrome, através da linha de comando e executar qualquer comando do navegador que você desejar, tais como: gerar PDF, testar eventos, prints de páginas e até mesmo, testes, principalmente os de acessibilidade!

Essa é a peça que faltava para criarmos testes automatizados de alta qualidade para validar a acessibilidade da sua aplicação.

Nosso projeto de exemplo

No exemplo a seguir, iremos abordar a seguinte estrutura:

  • Uma pasta public para o código do nosso exemplo
  • Dentro de public , um arquivo index.html
  • Usaremos jest , jest-axe e puppeteer para criar um novo ambiente de testes para o Jest + Puppeteer e executar a validação padrão do aXe.

Vamos lá:

mkdir -p jest-axe-puppeteer/public
cd jest-axe-puppeteer
touch public/index.html

Iremos iniciar um repositório Git, para acompanhar as mudanças:

git init
git add -A
git commit -m "core: initial commit"

Vamos iniciar nosso package.json e adicionar nossas depêndencias:

yarn init -y
yarn add jest jest-axe puppeteer

Vamos adicionar as mudanças ao nosso repositório:

git add -A
git commit -m "core: add Jest, aXe and Puppeteer deps"

Agora, vamos focar na configuração do Jest. Teremos que alterar 3 ciclos de vida do Jest, e segundo a documentação, a integração é simples.

Configurando Jest e Puppeteer

Essas alterações são relacionadas ao globalSetup , globalTeardown e testEnvironment. Precisamos criar um ambiente diferente para os testes de integração de acessibilidade. Esse ambiente que iremos criar, contém um navegador real, rodando através da linha de comando. Ou seja, isso é um ambiente de testes E2E completo!

Vamos criar, a raíz do nosso projeto, um arquivo jest.config.js

touch jest.config.js

E adicionar:

module.exports = {
globalSetup: "./integration/config/setup.js",
globalTeardown: "./integration/config/teardown.js",
testEnvironment: "./integration/config/puppeteer_environment.js"
};

Vamos analisar passo a passo cada arquivo, mas primeiro, vamos criar a pasta integration e adicionar tudo ao Git.

mkdir -p integration/config
touch integration/config/{setup,teardown,puppeteer_environment}.js
git add -A
git commit -m "feat: add integration/ and jest.config.js"

Agora, vamos abrir integration/config/setup.js e adicionar:

const fs = require('fs');
const os = require('os');
const path = require('path');
const puppeteer = require('puppeteer');
const mkdirp = require('mkdirp');
const express = require('express');
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');const IS_WATCH = process.argv.includes('--watch');module.exports = async () => {
// [A]
global.BROWSER = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
// [B]
mkdirp.sync(DIR);
fs.writeFileSync(
path.join(DIR, 'wsEndpoint'),
global.BROWSER.wsEndpoint()
);
// [C]
if (!IS_WATCH) {
const app = express();
app.use('/', express.static(path.join('public')));
global.SERVER = app.listen(5000);
}
};

Eita! Quanta coisa acontecendo! Vamos analisar as etapas [A] , [B] e [C] :

  • [A]: Estamos guardando a instância do Puppeteer em global.BROWSER , para podermos fechar o navegador após todos os testes forem realizados no teardown.js
  • [B]: Estamos escrevendo, na pasta tmp do sistema operacional, o endereço do WebSockets do DevTools criado pela etapa [A]. Iremos precisar dele em puppeteer_environment.js , onde iremos conectar ao navegador para realizar os testes
  • [C]: Aqui, um pequeno truque, quando não estivermos usando --watch, iremos criar uma instância do express em global.SERVER apontando para a pasta public . Isso é necessário pois, ao rodar os testes em CI, você não terá seu servidor de desenvolvimento rodando. Iremos ver mais detalhes disso em puppeteer_environment.js

Vamos adicionar express ao package.json e colocar tudo no Git:

yarn add expressgit add -A
git commit -m "feat: add integration/setup and express as dep"

Agora, vamos para o próximo arquivo, teardown.js :

const rimraf = require('rimraf');
const os = require('os');
const path = require('path');
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');module.exports = async () => {
await global.BROWSER.close();
rimraf.sync(DIR); if (global.SERVER) {
await global.SERVER.close();
}
};

Como o próximo nome diz, iremos destruir/limpar toda a bagunça que fizemos. No nosso caso, fechar a instância do navegador, remover a pasta do WebSocket e, caso nossa instância do express exista, fechar o servidor.

Perceba que temos rimraf como nova dependência.

Vamos adicionar isso ao package.json e ao Git:

yarn add rimrafgit add -A
git commit -m "feat: add integration/teardown and rimraf as dep"

Vamos voltar nossa atenção ao arquivo puppeteer_environment :

const NodeEnvironment = require('jest-environment-node');
const puppeteer = require('puppeteer');
const fs = require('fs');
const os = require('os');
const path = require('path');
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');const IS_WATCH = process.argv.includes('--watch');/**
* Testes de integração E2E automatizados construído com Puppeteer.
* Veja https://facebook.github.io/jest/docs/en/puppeteer.html.
*/
// [A]
class PuppeteerEnvironment extends NodeEnvironment {
constructor(config, options) {
super(config, options);
// [B]
const port = IS_WATCH ? 3000 : 5000;
const index = '';
// [C]
this.global.ROOT = `http://localhost:${port}/${index}`;
}
async setup() {
await super.setup();
// [D]
const wsEndpoint = fs.readFileSync(
path.join(DIR, 'wsEndpoint'),
'utf8',
);
if (!wsEndpoint) {
throw new Error('wsEndpoint not found');
}
this.global.BROWSER = await puppeteer.connect({
browserWSEndpoint: wsEndpoint,
});
}
async teardown() {
await super.teardown();
}
runScript(script) {
return super.runScript(script);
}
}
module.exports = PuppeteerEnvironment;

Simple, mas complexo? Vamos analisar as etapas [A] , [B], [C] e [D]:

  • [A]: Estamos criando uma nova classe PuppeteerEnvironment que extende o NodeEnvironment, que é um dos ambientes disponíveis no Jest, estamos usando a dependência jest-envrironment-node para isso. Isso é necessário para não executarmos nehum outro ambiente disponível no Jest (como o jsdom ). Ou seja, esse novo ambiente, é e roda por padrão, em Node.
  • [B]: Aqui é a continuação do truque que vimos em setup.js . Caso você execute os testes de integração com --watch , iremos buscar a aplicação na porta 3000 (normalmente, a porta de desenvolvimento), disponibilizando assim, uma experiência em tempo real para o desenvolvedor detectar os erros de acessibilidade. Caso contrário, iremos procurar a porta da instância express que criamos.
  • [C]: Aqui, outro pequeno truque, estamos guarando a URL em global.ROOT , para facilitar o acesso da URL da aplicação nos testes de integração. Iremos ver mais detalhes disso ao escrever nosso primeiro teste.
  • [D]: Nesse ponto, estamos lendo o endereço de WebSockets do DevTools para podermos realizar a comunicação entre instância e API.

Vamos adicionar nossa nova dependência e colocar tudo no Git:

yarn add jest-environment-nodegit add -A
git commit -m "feat: add integration/puppeteer_environment and jest-environment-node as dep"

Ufa 😅… a parte de configuração já passou. Agora, vamos ao divertido!

Nosso primeiro teste e um pouco de HTML!

Lembra que criamos um arquivo HTML em public/index.html ? (É, nem eu, depois de tudo isso de configuração 👽).

Vamos adicionar um pouco de HTML nele, para podermos criar algum teste:

Em public/index.html :

<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<title>Jest, aXe e Puppeteer</title>
</head>
<body>
<h1>Garçom, aqui nesse HTML...</h1>
</body>
</html>

E agora, dentro de integration iremos criar home.test.js e adicionar:

const { toHaveNoViolations } = require("jest-axe");expect.extend(toHaveNoViolations);jest.setTimeout(10000);describe("/", () => {
let page;
// [A]
beforeAll(async () => {
page = await global.BROWSER.newPage();
await page.goto(global.ROOT);
// [B]
await page.addScriptTag({ path: require.resolve("axe-core") });
});
// [C]
it("carrega corretamente", async () => {
const text = await page.evaluate(
() => document.body.textContent
);
expect(text).toContain("Garçom, aqui nesse HTML...");
});
// [D]
it("a11y", async () => {
const result = await page.evaluate(() => {
return new Promise(resolve => {
window.axe.run((err, results) => {
if (err) throw err;
resolve(results);
});
});
});
expect(result).toHaveNoViolations();
});
});

Parece um teste, mas e aí? Curiosidades em [A] , [B], [C] e [D]:

  • [A]: Antes de todos os testes, precisamos conectar o navegador na nossa aplicação. Com os truques que introduzimos na configuração, podemos acessar a instância do navegador e a URL aplicação através do objeto global .
  • [B]: Estamos adicionando, dinamicamente, a URL para o axe-core , assim como demonstrado no repositório oficial. Dessa maneira, você não precisa ficar lembrando de adicionar o script que contém as validações de acessibilidade. axe-core é uma dependência de jest-axe .
  • [C]: Como exemplo de um ambiente completo E2E, aqui estamos avaliando o conteúdo da página e testando se está correto.
  • [D]: É aqui que executamos nosso teste de acessibilidade utilizando a biblioteca aXe. Da mesma maneira que realizamos na interface do navegador. Irado!!1 :) 🎉🎉🎉

Agora, vamos executar nosso teste com:

yarn jest
🔥 Booom! Nosso primeiro teste falha na validação de acessibilidade!

E nossa validação de acessibilidade com aXe já achou um erro e os testes estão em vermelho! O bacana é que o erro é bem amigável, contendo até uma URL para as regras utilizadas pelo aXe com possíveis soluções!

🔥 Acessando a URL mostrada na mensagem de erro, temos essa ótima ajuda sobre o erro em questão!

Antes de resolver esse erro, vamos adicionar tudo ao Git:

git add -A
git commit -m "feat: add home HTML and initial tests"

Refatorando código para passar nos testes!

Para resolver o problema acima, podemos adicionar um HTML semântico para a regra landmark-one-main , em public/index.html adicione:

...
<main>
<h1>Garçom, aqui nesse HTML...</h1>
</main>
...

E vamos rodar os testes novamente:

yarn jest
Com HTML semântico nossos testes de acessibilidades passam!

Com nosso código arrumado, vamos adicionar ao Git:

git add -A
git commit -m "task: HTML refactor to make a11y tests happy"

Agora não tem desculpa, A11Y todo dia!

Como exercício, vou deixar você encarregado de me mandar um PR para o servidor de desenvolvimento. Assim, podemos executar yarn jest --watch e ter um ambiente de desenvolvimento com testes de integração de acessibilidade ao mesmo tempo.

O repositório desse exemplo você pode encontrar aqui.

Utilizando essa integração, temos certeza estarmos sempre verificando a acessibilidade básica da nossa aplicação, com um simples script na linha de comando, você faz a validação completa da sua aplicação.

Puppeteer é uma solução e tanto. Unindo esse ambiente com Jest, as possibilidades de testes de integração e regressão são enormes! (Por exemplo, você pode usar a API do Puppeteer e tirar print do simulador web mobile do Chrome em N devices e gravar tudo isso com snapshots).

Você leitor, meu muito obrigado! Se quiser me perguntar algo (dúvidas, projetos, etc), pode me mandar uma mensagem no Twitter ou deixar seu comentário abaixo. 🙏 🎉 🐔

--

--