O @axe-core/playwright executa verificações de acessibilidade junto com seus testes regulares a cada commit no CI. Detecta alt text ausente, inputs sem label, contraste de cor insuficiente e uso incorreto de ARIA. A varredura automatizada captura cerca de 30-40% das violações WCAG; o restante exige testes manuais de teclado e leitor de tela.
Por que Automatizar Testes de Acessibilidade?
Auditorias manuais de acessibilidade são lentas e caras. Verificações automatizadas encontram os problemas mais comuns na hora:
- Alt text ausente em imagens
- Inputs de formulário sem label
- Contraste de cor insuficiente
- Roles e atributos ARIA faltando
- Problemas de navegação por teclado
- Problemas de gerenciamento de foco
Ferramentas automatizadas detectam ~30-40% dos problemas de acessibilidade. O restante exige testes manuais com tecnologias assistivas reais. Mas capturar 30-40% automaticamente, a cada commit, é muito melhor do que nada.
Setup: axe-playwright
npm install --save-dev @axe-core/playwrightScan Básico de Acessibilidade
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('home page sem violações de acessibilidade', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});Se houver violações, o teste falha com detalhes:
● home page sem violações de acessibilidade
expect(received).toEqual(expected)
Expected: []
Received: [
{
id: 'color-contrast',
description: 'Elements must have sufficient color contrast',
nodes: [{ target: ['#nav-link'], ... }]
}
]Escaneando Páginas Específicas
test.describe('Verificações de acessibilidade', () => {
const pages = [
{ name: 'Home', url: '/' },
{ name: 'Login', url: '/login' },
{ name: 'Produtos', url: '/products' },
{ name: 'Contato', url: '/contact' },
];
for (const { name, url } of pages) {
test(`${name} é acessível`, async ({ page }) => {
await page.goto(url);
await page.waitForLoadState('networkidle');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
}
});Filtrando Regras
Execute apenas critérios WCAG específicos ou exclua problemas conhecidos:
test('página de login em conformidade com WCAG AA', async ({ page }) => {
await page.goto('/login');
const results = await new AxeBuilder({ page })
// Verifica apenas critérios WCAG 2.1 AA
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});
test('página de produtos com problemas conhecidos excluídos', async ({ page }) => {
await page.goto('/products');
const results = await new AxeBuilder({ page })
// Excluir temporariamente um problema conhecido enquanto está sendo corrigido
.disableRules(['color-contrast'])
.analyze();
expect(results.violations).toEqual([]);
});Escaneando Parte da Página
test('menu de navegação é acessível', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.include('#main-navigation') // Escaneia apenas o nav
.analyze();
expect(results.violations).toEqual([]);
});
test('modal é acessível', async ({ page }) => {
await page.goto('/products');
await page.click('[data-testid="add-to-cart"]');
// Aguarda o modal abrir
await page.waitForSelector('[role="dialog"]');
const results = await new AxeBuilder({ page })
.include('[role="dialog"]')
.analyze();
expect(results.violations).toEqual([]);
});Testes de Navegação por Teclado
O axe detecta atributos ARIA faltando. Testes manuais de teclado encontram problemas no fluxo de navegação:
test('formulário de login navegável por teclado', async ({ page }) => {
await page.goto('/login');
// Navega pelos campos do formulário com Tab
await page.keyboard.press('Tab');
await expect(page.locator(':focus')).toHaveAttribute('data-testid', 'email-input');
await page.keyboard.press('Tab');
await expect(page.locator(':focus')).toHaveAttribute('data-testid', 'password-input');
await page.keyboard.press('Tab');
await expect(page.locator(':focus')).toHaveAttribute('data-testid', 'submit-btn');
// Envia com Enter
await page.keyboard.press('Enter');
await page.waitForURL('/dashboard');
});
test('modal pode ser fechado com Escape', async ({ page }) => {
await page.goto('/products');
await page.click('[data-testid="filter-btn"]');
await expect(page.getByRole('dialog')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).not.toBeVisible();
});Gerenciamento de Foco
Quando modais abrem, o foco deve ir para dentro deles. Quando fecham, o foco deve retornar ao elemento que os abriu:
test('modal mantém o foco corretamente', async ({ page }) => {
await page.goto('/');
// Abre o modal
const triggerButton = page.getByTestId('open-modal');
await triggerButton.click();
const modal = page.getByRole('dialog');
await expect(modal).toBeVisible();
// Foco deve estar dentro do modal
const focusedElement = page.locator(':focus');
await expect(modal).toContainElement(focusedElement);
// Tab pelo modal — foco não deve sair
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
// Ainda dentro do modal
await expect(modal).toContainElement(page.locator(':focus'));
// Fecha o modal
await page.keyboard.press('Escape');
// Foco retorna ao botão que abriu o modal
await expect(triggerButton).toBeFocused();
});Verificação de Alt Text em Imagens
test('todas as imagens de produto têm alt text', async ({ page }) => {
await page.goto('/products');
// Encontra todas as imagens
const images = page.locator('img');
const count = await images.count();
for (let i = 0; i < count; i++) {
const img = images.nth(i);
const alt = await img.getAttribute('alt');
const src = await img.getAttribute('src');
// Alt deve existir e não ser string vazia (exceto decorativas com role="presentation")
const role = await img.getAttribute('role');
if (role !== 'presentation') {
expect(alt, `Imagem ${src} sem alt text`).not.toBeNull();
expect(alt, `Imagem ${src} com alt text vazio`).not.toBe('');
}
}
});Roles e Labels ARIA
test('inputs do formulário têm labels', async ({ page }) => {
await page.goto('/contact');
const inputs = page.locator('input, textarea, select');
const count = await inputs.count();
for (let i = 0; i < count; i++) {
const input = inputs.nth(i);
const type = await input.getAttribute('type');
// Ignora inputs hidden
if (type === 'hidden') continue;
const id = await input.getAttribute('id');
const ariaLabel = await input.getAttribute('aria-label');
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
// Input precisa ter label (via id+label, aria-label, ou aria-labelledby)
const hasLabel = id ? await page.locator(`label[for="${id}"]`).count() > 0 : false;
const isLabelled = hasLabel || ariaLabel || ariaLabelledBy;
expect(isLabelled, `Input sem label: ${id || 'sem nome'}`).toBeTruthy();
}
});Gerando Relatórios HTML Acessíveis
import AxeBuilder from '@axe-core/playwright';
// Helper que formata as violações de forma legível
async function checkAccessibility(page, selector?: string) {
const builder = new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa']);
if (selector) builder.include(selector);
const results = await builder.analyze();
if (results.violations.length > 0) {
const report = results.violations.map(v =>
`\n[${v.impact?.toUpperCase()}] ${v.id}: ${v.description}\n` +
v.nodes.map(n => ` - ${n.target.join(', ')}: ${n.failureSummary}`).join('\n')
).join('\n');
throw new Error(`Violações de acessibilidade encontradas:\n${report}`);
}
}
test('dashboard é acessível', async ({ page }) => {
await page.goto('/dashboard');
await checkAccessibility(page);
});Problemas Comuns Detectados pela Automação
| Problema | Regra axe | Correção |
|----------|-----------|----------|
| Imagem sem alt | image-alt | Adicionar alt="descrição" |
| Contraste baixo | color-contrast | Usar razão de contraste ≥ 4.5:1 |
| Input sem label | label | Adicionar ou aria-label |
| Botão sem texto | button-name | Adicionar texto ou aria-label |
| Ordem de heading | heading-order | Não pular h1 para h3 |
| Lang ausente | html-has-lang | Adicionar |
| Link sem nome | link-name | Adicionar texto descritivo ao link |
Resumo
# Instalação
npm install --save-dev @axe-core/playwright
# Uso básico
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
# Filtrar por nível WCAG
.withTags(['wcag2a', 'wcag2aa'])
# Limitar ao elemento
.include('#main-nav')
# Excluir problema conhecido
.disableRules(['color-contrast'])Execute verificações de acessibilidade nas suas páginas críticas da mesma forma que você roda testes funcionais: em cada PR, automaticamente. Combinado com testes de navegação por teclado e verificações de gerenciamento de foco, você captura a maioria dos problemas antes que cheguem a usuários que dependem de tecnologia assistiva.
→ Veja também: Testes de Acessibilidade para Engenheiros QA: Ferramentas, Técnicas e o Prazo EAA 2025 | Eventos de Teclado e Mouse no Playwright | Testes de Regressão Visual com IA: Além das Capturas Pixel-Perfeitas