Testes de acessibilidade automatizados com axe-core e Playwright

1. Introdução à acessibilidade web e automação de testes

A acessibilidade web não é mais uma opção — é uma necessidade. Com mais de 1 bilhão de pessoas vivendo com alguma forma de deficiência, garantir que sites e aplicações sejam utilizáveis por todos é uma questão de inclusão digital. Além disso, requisitos legais como as Diretrizes de Acessibilidade para Conteúdo Web (WCAG) e leis como a ADA (Americans with Disabilities Act) tornam a conformidade obrigatória em muitos países. A acessibilidade também impacta positivamente o SEO, já que mecanismos de busca favorecem sites com estrutura semântica clara e navegação intuitiva.

Testar acessibilidade manualmente é um processo demorado e sujeito a erros humanos. Ferramentas automatizadas como o axe-core surgem como uma solução eficiente, aplicando centenas de regras de verificação em segundos. O axe-core, desenvolvido pela Deque Systems, é um motor de regras de acessibilidade que pode ser integrado a frameworks de automação de testes. Quando combinado com o Playwright — uma ferramenta moderna de automação de navegadores —, obtemos uma dupla poderosa para garantir que cada página seja acessível desde o início do desenvolvimento.

2. Configuração do ambiente de teste com Playwright e axe-core

Para começar, precisamos instalar as dependências necessárias. O Playwright oferece suporte nativo ao axe-core através do pacote @axe-core/playwright.

npm init -y
npm install @playwright/test @axe-core/playwright
npx playwright install

Após a instalação, configuramos o Playwright no arquivo playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30000,
  use: {
    browserName: 'chromium',
    viewport: { width: 1280, height: 720 },
  },
});

Agora, em cada arquivo de teste, importamos o AxeBuilder para realizar as análises:

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

3. Escrevendo o primeiro teste de acessibilidade automatizado

Vamos criar um teste simples que navega para uma página e executa a análise de acessibilidade:

test('Página inicial deve ser acessível', async ({ page }) => {
  await page.goto('https://exemplo.com');

  const results = await new AxeBuilder({ page }).analyze();

  expect(results.violations).toEqual([]);
});

O método analyze() retorna um objeto contendo:
- violations: violações de regras de acessibilidade
- passes: verificações que passaram
- incomplete: verificações que não puderam ser concluídas automaticamente
- inapplicable: regras que não se aplicam à página

Para um relatório mais detalhado, podemos percorrer as violações:

test('Verificar violações de acessibilidade', async ({ page }) => {
  await page.goto('https://exemplo.com');

  const results = await new AxeBuilder({ page }).analyze();

  if (results.violations.length > 0) {
    console.log(`Encontradas ${results.violations.length} violações:`);
    results.violations.forEach(violation => {
      console.log(`- ${violation.id}: ${violation.description}`);
      violation.nodes.forEach(node => {
        console.log(`  Elemento: ${node.html}`);
      });
    });
  }

  expect(results.violations.length).toBe(0);
});

4. Personalizando regras e configurando a análise

Nem todas as regras do axe-core são relevantes para todos os projetos. Podemos filtrar regras específicas:

test('Testar apenas regras WCAG 2.1 AA', async ({ page }) => {
  await page.goto('https://exemplo.com');

  const results = await new AxeBuilder({ page })
    .withTags(['wcag21aa', 'wcag2aa'])
    .analyze();

  expect(results.violations).toEqual([]);
});

Para excluir elementos específicos da análise (como banners de terceiros ou modais temporários):

test('Excluir banner de cookies', async ({ page }) => {
  await page.goto('https://exemplo.com');

  const results = await new AxeBuilder({ page })
    .exclude('#cookie-banner')
    .analyze();

  expect(results.violations).toEqual([]);
});

Também podemos definir limites de severidade:

test('Permitir apenas violações menores', async ({ page }) => {
  await page.goto('https://exemplo.com');

  const results = await new AxeBuilder({ page })
    .withRules(['color-contrast', 'label'])
    .analyze();

  const violacoesCriticas = results.violations.filter(
    v => v.impact === 'critical' || v.impact === 'serious'
  );

  expect(violacoesCriticas).toEqual([]);
});

5. Integração com relatórios e CI/CD

Para gerar relatórios detalhados, podemos salvar os resultados em formato JSON:

import fs from 'fs';

test('Gerar relatório de acessibilidade', async ({ page }) => {
  await page.goto('https://exemplo.com');

  const results = await new AxeBuilder({ page }).analyze();

  fs.writeFileSync(
    'relatorio-acessibilidade.json',
    JSON.stringify(results, null, 2)
  );

  expect(results.violations).toEqual([]);
});

Em pipelines de CI/CD (como GitHub Actions), podemos falhar o build automaticamente:

test('Falhar build se houver violações críticas', async ({ page }) => {
  await page.goto('https://exemplo.com');

  const results = await new AxeBuilder({ page }).analyze();

  const violacoesCriticas = results.violations.filter(
    v => v.impact === 'critical'
  );

  if (violacoesCriticas.length > 0) {
    console.error('Violações críticas encontradas:');
    violacoesCriticas.forEach(v => {
      console.error(`- ${v.id}: ${v.help}`);
    });
  }

  expect(violacoesCriticas).toEqual([]);
});

6. Boas práticas e armadilhas comuns

Para testar múltiplos estados da UI, é essencial interagir com a página antes da análise:

test('Testar menu aberto', async ({ page }) => {
  await page.goto('https://exemplo.com');
  await page.click('#menu-toggle');
  await page.waitForSelector('#menu.open');

  const results = await new AxeBuilder({ page }).analyze();

  expect(results.violations).toEqual([]);
});

Para evitar falsos positivos com componentes dinâmicos, use exclude() ou configure regras específicas:

test('Testar formulário com erro de validação', async ({ page }) => {
  await page.goto('https://exemplo.com/contato');
  await page.click('#submit');
  await page.waitForSelector('.error-message');

  const results = await new AxeBuilder({ page })
    .exclude('#recaptcha') // Excluir reCAPTCHA
    .analyze();

  expect(results.violations).toEqual([]);
});

7. Ampliando a cobertura: testes visuais e de teclado

A acessibilidade vai além do axe-core. Podemos combinar com testes de navegação por teclado:

test('Navegação por teclado deve ser funcional', async ({ page }) => {
  await page.goto('https://exemplo.com');

  // Simular navegação por teclado
  await page.keyboard.press('Tab');
  await page.keyboard.press('Tab');
  await page.keyboard.press('Enter');

  // Verificar se o foco está no elemento correto
  const focusedElement = await page.evaluate(() => document.activeElement?.tagName);
  expect(focusedElement).toBe('A');

  // Executar análise de acessibilidade
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

Para verificar contraste de cores, podemos usar ferramentas complementares:

test('Verificar contraste de cores', async ({ page }) => {
  await page.goto('https://exemplo.com');

  // Tirar screenshot para análise visual
  await page.screenshot({ path: 'screenshot.png' });

  const results = await new AxeBuilder({ page })
    .withRules(['color-contrast'])
    .analyze();

  expect(results.violations).toEqual([]);
});

Referências