• domingo

Configurando tests de accesibilidad con Playwright

En este post vamos a ver cómo configurar tests de accesibilidad usando Playwright y axe-core, una de las librerías más usadas para detectar problemas automáticos de accesibilidad.
Imagen accesible?

Hay una frase que aparece mucho cuando hablamos de accesibilidad:

“la accesibilidad no es un feature”

Y sí, suena lindo para póster motivacional de LinkedIn. Pero después abrís el backlog y accesibilidad queda ahí, mirando desde el rincón, como ese ticket que todos saben que existe pero nadie quiere tocar porque “no hay tiempo”. A nadie le calienta la accesibilidad. Sobre todo en aplicaciones en las que el usuario no tiene opción de agarrar otra cosa (gobierno, bancos, etc)

La buena noticia es que, desde testing, podemos hacer algo bastante concreto: sumar chequeos automatizados de accesibilidad dentro de nuestros tests con Playwright.

No para reemplazar auditorías manuales. No para decir “listo, somos accesibles, cierren todo”. Eso sería una mentira con framework de automatización arriba (¡otra más!)

Pero sí para detectar problemas comunes temprano, evitar regresiones y empezar a tratar la accesibilidad como parte del flujo normal de desarrollo.

En este post vamos a ver cómo configurar tests de accesibilidad usando Playwright y axe-core, una de las librerías más usadas para detectar problemas automáticos de accesibilidad.

Qué vamos a cubrir

  • Qué es axe-core y por qué se usa tanto.

  • Cómo instalar Playwright y @axe-core/playwright.

  • Cómo escribir un primer test de accesibilidad.

  • Cómo excluir zonas específicas de la página.

  • Cómo filtrar reglas o estándares.

  • Cómo generar reportes útiles.

  • Qué cosas NO puede validar una herramienta automática.

  • Cómo integrarlo en CI.

Primero: qué es axe-core

axe-core es un motor de testing de accesibilidad desarrollado por Deque. Analiza el DOM de una página y detecta problemas comunes relacionados con estándares como WCAG.

Documentación oficial:

La integración con Playwright se hace usando el paquete:

npm install -D @axe-core/playwright

Eso nos permite correr análisis de accesibilidad sobre una página real abierta por Playwright.

Crear un proyecto con Playwright

Si todavía no tenés un proyecto de Playwright, podés crearlo así:

npm init playwright@latest

Esto te va a generar una estructura parecida a esta:

.
├── tests/
│   └── example.spec.ts
├── playwright.config.ts
├── package.json
└── tsconfig.json

Ahora instalamos axe:

npm install -D @axe-core/playwright

Y con eso ya podemos empezar a escribir tests.

Primer test de accesibilidad

Supongamos que queremos validar la home de una aplicación.

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

test('home page should not have automatically detectable accessibility issues', async ({ page }) => {
  await page.goto('<https://example.com>');

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

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

Este test hace algo bastante simple:

  1. Abre una página.

  2. Ejecuta axe sobre el DOM actual.

  3. Falla si encuentra violaciones de accesibilidad.

Simple. Directo. Sin magia negra.

Lo importante es entender qué estamos validando: problemas que axe puede detectar automáticamente. Por ejemplo:

  • Imágenes sin texto alternativo.

  • Problemas de contraste en algunos casos.

  • Inputs sin labels.

  • Landmarks incorrectos o faltantes.

  • Elementos ARIA mal usados.

  • Problemas de estructura semántica.

No estamos validando absolutamente toda la experiencia de una persona usando un lector de pantalla. Para eso sigue haciendo falta testing manual, criterio humano y, cuando se pueda, feedback de usuarios reales.

Hacer el error más legible

El test anterior funciona, pero cuando falla puede ser medio tosco de leer. Una alternativa es imprimir las violaciones para entender mejor qué pasó.

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

test('home page should not have accessibility violations', async ({ page }) => {
  await page.goto('<https://example.com>');

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

  if (results.violations.length > 0) {
    console.log(JSON.stringify(results.violations, null, 2));
  }

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

Esto no es el reporte más hermoso del planeta, pero para empezar alcanza. Y en automatización, “para empezar alcanza” suele ser bastante más valioso que “vamos a diseñar la arquitectura perfecta durante tres semanas y no automatizar nada”.

Validar una página después de una interacción

Un punto importante: la accesibilidad no solo importa cuando la página carga.

También importa después de que el usuario interactúa con la aplicación:

  • Abre un modal.

  • Envía un formulario.

  • Aparece un mensaje de error.

  • Se despliega un menú.

  • Cambia de pestaña.

  • Se muestra un toast.

Ejemplo con un modal:

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

test('modal should not have accessibility violations', async ({ page }) => {
  await page.goto('<https://example.com>');

  await page.getByRole('button', { name: 'Open settings' }).click();

  await expect(page.getByRole('dialog')).toBeVisible();

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

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

Acá hay algo lindo: Playwright ya nos empuja a usar locators accesibles, como getByRole. Eso no convierte automáticamente tu app en accesible, pero sí te obliga a pensar en roles, nombres accesibles y estructura semántica.

Documentación recomendada:

Analizar solo una parte de la página

A veces no querés escanear toda la página. Quizás estás probando un componente específico o querés evitar ruido de zonas que todavía no están bajo tu control.

Para eso podés usar include:

const results = await new AxeBuilder({ page })
  .include('#checkout-form')
  .analyze();

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

Esto limita el análisis al elemento que matchea ese selector.

También podés excluir una zona:

const results = await new AxeBuilder({ page })
  .exclude('#third-party-widget')
  .analyze();

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

Esto es útil cuando tenés, por ejemplo, un widget de terceros que mete HTML cuestionable. ¿Ideal? No. ¿Realista? Bastante.

Eso sí: excluir cosas porque “molestan” puede transformarse en la alfombra debajo de la cual barremos todos los cadáveres. Usalo con criterio.

Filtrar por reglas o tags

axe permite configurar qué reglas querés correr. Por ejemplo, podés enfocarte en ciertos tags relacionados con WCAG.

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

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

Esto es útil si querés alinear tus chequeos automáticos con un nivel específico de WCAG.

También podés deshabilitar reglas específicas:

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

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

¿Recomiendo deshabilitar reglas así porque sí? No.

Pero puede pasar que una regla genere ruido en cierto contexto, o que tengas una validación alternativa. Lo importante es que esa decisión quede documentada. Nada de “lo deshabilité porque fallaba el pipeline y me quería ir a dormir”. Aunque todos hemos estado ahí. Espiritualmente, al menos.

Referencias:

Crear un helper reutilizable

En vez de repetir el mismo bloque en todos los tests, podemos crear un helper.

// tests/helpers/accessibility.ts
import { expect, Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

type AccessibilityOptions = {
  include?: string;
  exclude?: string;
  tags?: string[];
};

export async function checkAccessibility(
  page: Page,
  options: AccessibilityOptions = {}
) {
  let builder = new AxeBuilder({ page });

  if (options.include) {
    builder = builder.include(options.include);
  }

  if (options.exclude) {
    builder = builder.exclude(options.exclude);
  }

  if (options.tags) {
    builder = builder.withTags(options.tags);
  }

  const results = await builder.analyze();

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

Y después lo usamos así:

import { test } from '@playwright/test';
import { checkAccessibility } from './helpers/accessibility';

test('checkout page should not have accessibility violations', async ({ page }) => {
  await page.goto('/checkout');

  await checkAccessibility(page, {
    include: '#checkout-form',
    tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'],
  });
});

Esto deja los tests más limpios y además centraliza la configuración.

Agregar un fixture custom

Si querés algo más prolijo, podés extender Playwright con un fixture.

// tests/fixtures/accessibility.fixture.ts
import { test as base, expect, Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

type AccessibilityFixture = {
  checkAccessibility: (options?: {
    include?: string;
    exclude?: string;
    tags?: string[];
  }) => Promise<void>;
};

export const test = base.extend<AccessibilityFixture>({
  checkAccessibility: async ({ page }, use) => {
    async function checkAccessibility(options = {}) {
      let builder = new AxeBuilder({ page });

      if (options.include) {
        builder = builder.include(options.include);
      }

      if (options.exclude) {
        builder = builder.exclude(options.exclude);
      }

      if (options.tags) {
        builder = builder.withTags(options.tags);
      }

      const results = await builder.analyze();

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

    await use(checkAccessibility);
  },
});

export { expect };

Y ahora el test queda así:

import { test } from './fixtures/accessibility.fixture';

test('profile page should not have accessibility violations', async ({ page, checkAccessibility }) => {
  await page.goto('/profile');

  await checkAccessibility({
    tags: ['wcag2a', 'wcag2aa'],
  });
});

Esto ya empieza a parecerse a algo que podríamos mantener en un framework real.

Generar reportes HTML

Para reportes más cómodos, podés usar paquetes como:

Instalación:

npm install -D axe-html-reporter

Ejemplo:

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

test('generate accessibility report', async ({ page }) => {
  await page.goto('<https://example.com>');

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

  createHtmlReport({
    results,
    options: {
      projectKey: 'Free Range Testers Demo',
      outputDir: 'accessibility-reports',
      reportFileName: 'home-page-accessibility-report.html',
    },
  });

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

Después podés guardar ese reporte como artifact en CI.

Integración con GitHub Actions

Un workflow básico podría ser:

name: Playwright Accessibility Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  accessibility-tests:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps

      - name: Run accessibility tests
        run: npx playwright test tests/accessibility

      - name: Upload accessibility reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: accessibility-reports
          path: accessibility-reports/

Referencias:

Dónde poner estos tests

No hay una única respuesta correcta, pero algunas opciones razonables son:

tests/
├── e2e/
│   └── checkout.spec.ts
├── accessibility/
│   ├── home.accessibility.spec.ts
│   ├── checkout.accessibility.spec.ts
│   └── profile.accessibility.spec.ts
└── helpers/
    └── accessibility.ts

A mí me gusta separarlos cuando empiezan a crecer, porque tienen una intención distinta a los tests E2E funcionales.

Un test E2E puede decir:

El usuario puede comprar un producto.

Un test de accesibilidad puede decir:

El formulario de checkout no tiene problemas automáticos de accesibilidad detectables.

Son conversaciones distintas. Parientes, pero no gemelos.

Cuándo correrlos

Podés correrlos en distintos momentos:

  • En cada Pull Request.

  • En nightly builds.

  • Contra ambientes de staging.

  • Antes de releases.

  • Sobre páginas críticas del negocio.

Mi recomendación: empezá chico.

No intentes meter 400 chequeos de accesibilidad de golpe sobre una aplicación legacy detonada porque vas a crear un festival de rojo en el pipeline y todo el mundo va a odiar accesibilidad, Playwright, axe, al tester y probablemente a la humanidad.

Mejor empezar con:

  • Home.

  • Login.

  • Registro.

  • Checkout.

  • Formularios críticos.

  • Flujos principales del producto.

Y desde ahí ir ampliando.

Qué problemas NO vas a detectar automáticamente

Esta parte es clave.

axe es muy útil, pero no reemplaza una evaluación completa de accesibilidad.

Hay cosas que una herramienta automática no puede saber bien:

  • Si el texto alternativo de una imagen es realmente útil.

  • Si el orden de foco tiene sentido para una persona real.

  • Si la experiencia con lector de pantalla es clara.

  • Si el contenido es comprensible.

  • Si la interacción con teclado es natural en todos los casos.

  • Si un mensaje de error realmente ayuda al usuario.

Ejemplo clásico: una imagen puede tener alt="imagen".

Técnicamente tiene alt text. ¿Es útil? No. Es el equivalente accesible de decir “coso”.

Por eso, la automatización sirve para detectar una parte del problema. Una parte importante, sí. Pero una parte.

Referencias:

Buenas prácticas para arrancar

1. Usá locators accesibles en Playwright

En vez de esto:

await page.locator('.submit-button').click();

Preferí esto:

await page.getByRole('button', { name: 'Submit' }).click();

Esto hace que tus tests se parezcan más a cómo una persona o tecnología asistiva interpreta la página.

2. No mezcles todo en el mismo test

Podés tener un test funcional y otro de accesibilidad.

test('user can complete checkout', async ({ page }) => {
  // flujo funcional
});

test('checkout page has no accessibility violations', async ({ page }) => {
  // chequeo accesibilidad
});

Cuando falla, sabés mejor qué conversación tener.

3. Documentá exclusiones

Si excluís una regla o una sección, dejá un comentario explicando por qué.

const results = await new AxeBuilder({ page })
  // Third-party chat widget. Tracked separately with vendor.
  .exclude('#chat-widget')
  .analyze();

El comentario no arregla el problema, pero al menos evita el clásico “¿y esto quién lo apagó?”.

4. Corré los tests sobre estados reales

No te quedes solo con la página cargada.

Validá también:

  • Estados con errores.

  • Formularios incompletos.

  • Modales abiertos.

  • Menús desplegados.

  • Componentes dinámicos.

  • Estados vacíos.

  • Estados de loading cuando tenga sentido.

La accesibilidad suele romperse en los bordes. Como casi todo en software, bah.

Ejemplo más real: formulario con errores

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

test('login form errors should be accessible', async ({ page }) => {
  await page.goto('/login');

  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page.getByText('Email is required')).toBeVisible();
  await expect(page.getByText('Password is required')).toBeVisible();

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

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

Este tipo de test es mucho más valioso que solo escanear la home. Porque los mensajes de error, los campos inválidos y los estados dinámicos son lugares donde se rompen muchas experiencias accesibles.

Cómo vender esto dentro de un equipo

A veces el problema no es técnico. Es político, cultural o de prioridades.

Una forma razonable de presentar esto es:

No estamos intentando resolver toda la accesibilidad de la empresa con un test. Estamos agregando una red de seguridad automática para detectar problemas comunes antes de que lleguen a producción.

Eso baja bastante la resistencia.

No prometas:

Con esto garantizamos WCAG AA.

Porque no.

Prometé algo más honesto:

Con esto reducimos regresiones básicas de accesibilidad y hacemos visible el problema más temprano.

Mucho menos marketinero. Mucho más cierto.

Conclusión

Configurar tests de accesibilidad con Playwright es bastante directo:

  1. Instalás @axe-core/playwright.

  2. Abrís una página con Playwright.

  3. Ejecutás axe.

  4. Fallás el test si hay violaciones.

  5. Guardás reportes si necesitás evidencia.

  6. Lo integrás en CI.

La clave no está solo en la herramienta. Está en cómo la incorporás al proceso.

Si lo hacés bien, estos tests pueden convertirse en una red de seguridad muy útil para detectar problemas comunes de accesibilidad antes de que lleguen a producción.

No reemplazan el testing manual. No reemplazan criterio. No reemplazan hablar con usuarios reales.

Pero ayudan.

Y en un mundo donde muchas apps ni siquiera pasan por el mínimo chequeo automático, “ayudan” ya es bastante.

Recursos recomendados

Obviamente, si querés meterle a FONDO a Playwright y sacarle todo el jugo, te recomiendo el curso que encontrás acá mismo en la web de Playwright para E2E Testing con TypeScript

Un poco

Sobre mi

Consultor privado e instructor en QA

Más de 16 años en el mercado, trabajando como consultor QA privado para empresas de Nueva Zelanda y Australia en proyectos de gran impacto y siempre a la vanguardia.

Lo que enseño viene de mi experiencia 🧑🏻‍💻

  • Entrega gratuita por correo electrónico

La guía 2027 para conseguir trabajo en Testing de Software

  • Descarga digital
  • 1 archivo

Conseguir trabajo en Testing de Software, en este 2025, presenta desafíos de los que necesitás enterarte YA mismo. En esta guía exclusiva de Free Range Testers vuelco en el tono informal de siempre, mis más de 16 años de experiencia y sobre todo lo relacionado a las nuevas tendencias que van a hacer la gran diferencia a la hora de buscar trabajo. ¡Nos vemos en el libro!

Suscríbete para estar informado de las actividades de Free Range Testers.

0 comments

Sign upor login to leave a comment