# Automatizando Pruebas de Accesibilidad Web: Combinando axe-core y Playwright

## Introducción

En el [artículo anterior](https://blog.juniordiazbriceno.dev/pruebas-de-regresi-n-visual-con-playwright-detectando-cambios-en-la-ui-autom-ticamente) de esta serie vimos cómo detectar cambios visuales no intencionados con pruebas de regresión visual en Playwright. Esta vez nos metemos con otro tipo de "bug" que rara vez aparece en un test funcional tradicional: las violaciones de accesibilidad.

La accesibilidad web no es solo un requisito legal — es un principio fundamental del diseño inclusivo. A medida que una aplicación crece, probar manualmente el cumplimiento de accesibilidad se vuelve impráctico y propenso a errores. En este artículo armamos un marco de pruebas automatizado usando Playwright y axe-core, y lo probamos sobre AutoCatalog, mi demo de un catálogo de autos construido específicamente para esta serie (Next.js + TypeScript).

El objetivo es que al final tengas un helper que puedas copiar y adaptar a tu propio proyecto.

* * *

## ¿Por Qué Automatizar las Pruebas de Accesibilidad?

Incorporar pruebas de accesibilidad automatizadas trae varias ventajas concretas:

*   **Detección temprana:** identificar y corregir problemas antes de que lleguen a producción, reduciendo el costo de remediación.
    
*   **Estándares consistentes:** las pruebas automatizadas aplican el mismo criterio en todas las páginas y features.
    
*   **Prevención de regresiones:** un cambio de UI que rompe accesibilidad se detecta antes del merge, no después.
    
*   **Documentación de cumplimiento:** los reportes generados sirven como evidencia de los esfuerzos hacia WCAG 2.0/2.1 Nivel A y AA.
    
*   **Empoderamiento del equipo de desarrollo:** los devs reciben feedback inmediato sin necesitar ser expertos en accesibilidad.
    

* * *

## La Pila Tecnológica

### Playwright

Playwright es un framework moderno de pruebas end-to-end con capacidades de automatización multi-navegador. Para pruebas de accesibilidad aporta:

*   Ejecución confiable en Chromium, Firefox y WebKit.
    
*   Mecanismos de espera integrados que asegura que la página esté completamente cargada antes de analizarla.
    
*   Integración natural con el resto de la suite (mismos fixtures, mismos page objects).
    
*   Interacción real con el motor del navegador, lo que hace que las pruebas reflejen el DOM que realmente ve un usuario.
    

### axe-core

Desarrollado por Deque Systems, axe-core es el motor de referencia de la industria para pruebas de accesibilidad. Detecta violaciones de WCAG 2.0, WCAG 2.1 y otros estándares con muy pocos falsos positivos. Entre lo que identifica:

*   Texto alternativo faltante en imágenes
    
*   Contraste de color insuficiente
    
*   Jerarquías de encabezados incorrectas
    
*   Problemas de navegación por teclado
    
*   Etiquetas de formulario faltantes
    
*   Atributos ARIA incorrectos o ausentes
    

### @axe-core/playwright

Este paquete conecta Playwright y axe-core con una API fluida pensada específicamente para el objeto `Page` de Playwright. Se encarga de inyectar axe-core en el contexto del navegador y de extraer los resultados, así que uno puede enfocarse en la lógica de la prueba y no en la mecánica de la integración.

* * *

## Descripción General de la Arquitectura

El marco de pruebas de accesibilidad se apoya en tres capas:

*   **Capa de utilidades:** funciones reutilizables para analizar páginas y generar reportes.
    
*   **Capa de pruebas:** casos de prueba concretos para cada página, modal o estado.
    
*   **Capa de reportes:** transforma los resultados crudos de axe-core en reportes HTML legibles, con detalle de cada violación.
    

* * *

## Implementación: Construyendo la Capa de Utilidades

### Función de Análisis Principal

`analyzeAccessibility` es la interfaz principal para correr el análisis:

```typescript
import { Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import type { AxeResults } from 'axe-core';

export interface A11yAnalyzeOptions {
    tags?: string[];
    rules?: string[];
    disableRules?: string[];
    include?: string | string[];
    exclude?: string | string[];
}

export async function analyzeAccessibility(
    page: Page,
    options: A11yAnalyzeOptions = {}
): Promise<AxeResults> {
    const {
        tags = ['wcag2a', 'wcag2aa'],
        rules = [],
        disableRules = [],
        include = [],
        exclude = []
    } = options;

    const builder = new AxeBuilder({ page }).withTags(tags);

    if (rules.length > 0) {
        builder.withRules(rules);
    }

    if (disableRules.length > 0) {
        builder.disableRules(disableRules);
    }

    // Include specific elements/regions for testing
    if (include.length > 0 || (typeof include === 'string' && include)) {
        const includeSelectors = Array.isArray(include) ? include : [include];
        includeSelectors.forEach(selector => {
            builder.include(selector);
        });
    }

    // Exclude specific elements/regions from testing
    if (exclude.length > 0 || (typeof exclude === 'string' && exclude)) {
        const excludeSelectors = Array.isArray(exclude) ? exclude : [exclude];
        excludeSelectors.forEach(selector => {
            builder.exclude(selector);
        });
    }

    return await builder.analyze();
}
```

**Principios clave de diseño:**

*   **Cumplimiento WCAG por defecto:** las tags por defecto son `['wcag2a', 'wcag2aa']`, así que sin configurar nada ya estás cubriendo la línea base.
    
*   **Focalización flexible:** `include` y `exclude` permiten testear componentes específicos de forma aislada.
    
*   **Patrón builder:** delega en `AxeBuilder` para armar escenarios de análisis más complejos sin reinventar la rueda.
    

> 💡 **Sobre el default de** `tags`**:** `['wcag2a', 'wcag2aa']` no es un valor de ejemplo — es el objetivo de cumplimiento del proyecto, definido una sola vez acá en el helper. No hace falta repetirlo en cada test; solo lo vas a sobreescribir cuando un test específico necesite desviarse de esa línea base (por ejemplo, para validar una regla puntual con `rules`).

### Generación de Reportes

`generateA11yReport` transforma los resultados crudos de axe-core en un reporte HTML:

```typescript
import { createHtmlReport } from 'axe-html-reporter';
import * as fs from 'node:fs';
import * as path from 'node:path';

export interface A11yReportOptions {
    projectKey?: string;
    outputDir?: string;
    reportFileName?: string;
}

export function generateA11yReport(
    results: AxeResults,
    options: A11yReportOptions = {}
): void {
    const {
        projectKey = 'playwright-a11y',
        outputDir = path.join('test-results', 'a11y-reports'),
        reportFileName = 'a11y-report.html'
    } = options;

    // Asegura que el directorio de salida exista
    fs.mkdirSync(outputDir, { recursive: true });

    createHtmlReport({
        results,
        options: {
            projectKey,
            outputDir,
            reportFileName,
        }
    });
}
```

Cada reporte incluye descripción de la violación, los elementos afectados, sugerencias de corrección y links a la guía correspondiente — muy útil para pasarle el reporte directo a un dev sin tener que traducir el output de axe-core.

### Wrapper de Conveniencia

`analyzeAndReport` combina análisis y reporte en una sola llamada:

```typescript
export interface A11yOptions extends A11yAnalyzeOptions, A11yReportOptions { }

export async function analyzeAndReport(
    page: Page,
    options: A11yOptions = {}
): Promise<AxeResults> {
    const { tags, rules, disableRules, include, exclude, projectKey, outputDir, reportFileName } = options;

    const results = await analyzeAccessibility(page, { tags, rules, disableRules, include, exclude });
    generateA11yReport(results, { projectKey, outputDir, reportFileName });

    return results;
}
```

* * *

## Patrones de Implementación de Pruebas

### Pruebas de Páginas Completas

Una prueba de página completa asegura que toda la vista cumpla con los estándares de accesibilidad:

```typescript
test('Product Management - Default page - Accessibility validation', async ({ page }) => {
    await test.step('Validate accessibility', async () => {
        const results = await analyzeAndReport(page, {
            projectKey: 'autocatalog-a11y',
            reportFileName: 'manage-default.html',
        })
        expect(results.violations).toEqual([])
    })
})
```

Si la página tiene violaciones de accesibilidad, esta prueba va a fallar — y el reporte HTML generado en `manage-default.html` las detalla una por una, con su selector, descripción, impacto y sugerencia de corrección.

### Pruebas de Diálogos Modales (con `include`)

Los modales son un buen candidato para pruebas de accesibilidad automatizadas porque concentran varios criterios que axe-core sí puede verificar de forma estática: atributos ARIA del diálogo (`role`, `aria-label`, `aria-modal`), labels asociados a cada campo del formulario, y contraste o indicadores de foco visibles. Pero hay un problema con testear un modal mediante un escaneo de página completa: **arrastra el estado completo de la página, no solo el modal**.

```typescript
test('Product Management - Add Product modal open - Accessibility validation', async ({ page }) => {
    await test.step('Open Add Product modal', async () => {
        await managePage.openAddProductModal()
        await managePage.expectAddProductModalVisible()
    })

    await test.step('Validate accessibility', async () => {
        const results = await analyzeAndReport(page, {
            include: "[role='dialog']",
            projectKey: 'autocatalog-a11y',
            reportFileName: 'manage-add-product-modal.html',
        })
        expect(results.violations).toEqual([])
    })
})
```

![Reporte HTML de axe-core mostrando 3 violaciones detectadas en un escaneo de página completa: aria-valid-attr-value, button-name y document-title.](https://cdn.hashnode.com/uploads/covers/69fd57199f93a850a45529cb/fc590d47-224d-4db4-9cb2-bfdcb372cc9f.gif align="center")

> 📸 3 violations detected — full-page scan

![Reporte HTML de axe-core mostrando 0 violaciones al escopar el análisis al modal con include](https://cdn.hashnode.com/uploads/covers/69fd57199f93a850a45529cb/d010b00a-ee37-408f-821d-3dca3ba84516.gif align="center")

> 📸 0 violations — scoped to \[role='dialog'\]

* * *

## Mejores Prácticas y Patrones

### Organización de Pruebas

Organizar las pruebas de accesibilidad para que reflejen la estructura de la aplicación ayuda mucho a la hora de leer reportes y de ejecutar subconjuntos:

*   Usar `describe` para agrupar por página/feature.
    
*   Nombrar cada test de forma que se entienda qué estado o componente se está validando.
    
*   Mantener archivos de prueba separados por feature.
    
*   Usar tags como `@A11y` y tags específicos por feature (`@ProductManagement`) para ejecución selectiva.
    

Estructura de ejemplo, basada en la suite real de AutoCatalog:

```typescript
test.describe('@ProductManagement @A11y - Product Management accessibility tests', () => {
    test('Product Management - Default page - Accessibility validation', /* ... */)
    test('Product Management - Add Product modal open - Accessibility validation', /* ... */)
})
```

El mismo patrón se replica para `HomePage` (con tag `@Home @A11y`) y `CartPage` (`@Cart @A11y`), cada una con sus propios estados relevantes.

### Ejecución Selectiva de Pruebas

El sistema de tags permite ejecutar subconjuntos específicos según lo que se necesite validar:

```bash
# Ejecutar todas las pruebas de accesibilidad
npx playwright test --grep "@A11y"

# Ejecutar solo las pruebas de Product Management
npx playwright test --grep "@ProductManagement"

# Ejecutar solo las pruebas de Home
npx playwright test --grep "@Home"
```

Con esto, podés decidir si corren todas en cada PR, o si las dividís por feature para paralelizar — la estrategia de tags ya está lista para cualquiera de los dos enfoques.

* * *

## Comprendiendo las Limitaciones de las Pruebas Automatizadas

Las pruebas automatizadas de accesibilidad detectan muchos problemas, pero no todos. Las herramientas automatizadas típicamente identifican entre el **30% y el 50% de los problemas de accesibilidad**.

### Problemas que Requieren Pruebas Manuales

*   **Calidad del contenido:** si el `alt` describe con precisión la imagen y su contexto.
    
*   **Orden de foco:** si el orden de tabulación tiene sentido lógico.
    
*   **Accesibilidad cognitiva:** si el contenido y la navegación son comprensibles.
    
*   **Secuencia significativa:** si el contenido conserva sentido al leerse de forma lineal.
    
*   **Interacciones complejas:** si flujos multi-paso funcionan bien con tecnología asistiva.
    
*   **Experiencia real de lector de pantalla:** cómo se anuncia y se navega el contenido.
    
*   **Atajos de teclado:** si los shortcuts personalizados chocan con los de la tecnología asistiva.
    

### Estrategia Integral

1.  **Pruebas automatizadas (cobertura 30-50%):** detectan violaciones comunes temprano.
    
2.  **Pruebas manuales con teclado y lector de pantalla (suman 20-30% adicional).**
    
3.  **Investigación con usuarios reales de tecnología asistiva** (issues restantes + insights de UX).
    
4.  **Educación continua del equipo de desarrollo.**
    

> 💡 **¿Necesitás complementar con pruebas visuales?** Algunos criterios WCAG, como el 2.4.7 (Foco Visible), requieren verificación visual que axe-core no puede automatizar por completo. En el Artículo 3 de esta serie vemos cómo validar indicadores de foco con regresión visual en Playwright.

* * *

## Midiendo el Progreso

Algunas métricas que vale la pena trackear a medida que la suite crece:

*   **Tendencia de violaciones:** cantidad y severidad a lo largo del tiempo.
    
*   **Cobertura de pruebas:** qué porcentaje de la app está cubierto por pruebas de accesibilidad.
    
*   **Tiempo de remediación:** cuánto tarda en corregirse una violación detectada.
    
*   **Tasa de prevención:** violaciones detectadas en PR vs. las que llegan a producción.
    
*   **Hallazgos manuales:** problemas que la automatización no captó (te dicen dónde están las brechas reales).
    

* * *

## Estructura de la Suite

Así se organiza la suite de accesibilidad en `autocatalog-testing`:

```plaintext
tests/
├── accessibility/
│   ├── ProductManagementA11yTest.ts
│   │   └── @ProductManagement @A11y
│   │       ├── Default page
│   │       └── Add Product modal (include: [role='dialog'])
│   ├── HomeA11yTest.ts
│   │   └── @Home @A11y
│   └── CartA11yTest.ts
│       └── @Cart @A11y
│
├── utils/
│   └── AccessibilityHelper.ts
│       ├── analyzeAccessibility()
│       ├── generateA11yReport()
│       ├── analyzeAndReport()
│       └── generateReportFileName()
│
└── pageObject/
    ├── POManager.ts
    ├── HomePage.ts
    ├── ManagePage.ts
    └── CartPage.ts
```

* * *

## Solución de Problemas Comunes

### Las pruebas se quedan sin tiempo

```typescript
await page.waitForLoadState('networkidle');
await page.waitForSelector('.main-content');
const results = await analyzeAndReport(page, options);
```

### Pruebas inestables por contenido dinámico

```typescript
await page.waitForSelector('[data-testid="product-table"]', { state: 'visible' });
const results = await analyzeAndReport(page, options);
```

### Componentes de terceros con muchas violaciones

Si trabajás con un componente de terceros que no podés corregir y necesitás excluirlo del análisis, `@axe-core/playwright` soporta `exclude` para eso (ver la [documentación oficial](https://github.com/dequelabs/axe-core-npm/tree/develop/packages/playwright)). En AutoCatalog no tenemos ese escenario, así que no lo cubrimos en detalle acá — pero vale saber que la opción existe si la necesitás.

* * *

## Datos Curiosos

Tres cosas que aprendí mientras armaba esta suite y que cambian cómo leés (o configurás) un análisis con axe-core:

*   **axe-core lee el árbol de accesibilidad, no el DOM.** Por eso dos herramientas pueden escanear la misma página y reportar cosas distintas sin que ninguna esté "equivocada": axe-core evalúa el modelo que realmente consumen los lectores de pantalla, mientras que herramientas como WAVE leen el DOM más directamente. Un elemento con `aria-hidden="true"` puede tener problemas a nivel de DOM (label faltante, contraste) que WAVE marca, pero que axe-core correctamente ignora porque para cualquier tecnología asistiva ese elemento simplemente no existe.
    
*   `color-contrast` **no evalúa elementos deshabilitados.** axe-core excluye por completo de esta regla a cualquier elemento con `disabled` o `aria-disabled="true"` — no lo marca como "pass", "fail" ni "incomplete", simplemente no lo evalúa, siguiendo la excepción que WCAG 1.4.3 hace para componentes inactivos. Un botón deshabilitado con contraste pésimo nunca va a aparecer en el reporte.
    
*   **Las** `tags` **de axe-core van más allá de WCAG.** La documentación de Playwright muestra solo cuatro (`wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa`), pero axe-core soporta muchas más — `wcag22aa`, `best-practice`, `section508`, `EN-301-549`, entre otras. No es algo que necesites tocar todos los días, pero la superficie disponible es más amplia de lo que parece a primera vista, por si en algún momento necesitás alinear las pruebas con un estándar distinto a WCAG.
    

En todos los casos la herramienta está siendo precisa, no "permisiva" — pero hay que entender qué mide exactamente antes de interpretar (o confiar ciegamente en) sus resultados.

* * *

## Conclusión

Las pruebas automatizadas de accesibilidad con Playwright y axe-core son una base sólida para construir aplicaciones más inclusivas, y detectan una buena parte de los problemas mucho antes de que lleguen a producción.

**Puntos clave:**

*   **Empezar chico, iterar:** arrancar por los flujos más usados y expandir cobertura con el tiempo.
    
*   **Combinar automatizado y manual:** la automatización cubre 30-50%; las pruebas manuales siguen siendo necesarias.
    
*   **Usar** `include` **para aislar el componente que te interesa probar**, sin que el resultado se mezcle con el resto de la página.
    
*   **Entender qué mide la herramienta** — árbol de accesibilidad vs. DOM, exclusiones de `color-contrast` en elementos deshabilitados — antes de interpretar (o confiar en) sus resultados.
    
*   **Usar tags para ejecución selectiva**, así podés correr toda la suite de accesibilidad o solo el subconjunto que te interesa.
    

* * *

¿Tu equipo tiene cobertura de accesibilidad automatizada? Si quieres explorar cómo implementar este tipo de testing en tu proyecto, [cuéntame sobre tu equipo aquí](https://form.typeform.com/to/pUJwPnOw?utm_source=hashnode&utm_medium=article&utm_campaign=axe-core&utm_content=es).

* * *

## Recursos Adicionales

*   [Guías WCAG 2.1](https://www.w3.org/WAI/WCAG21/quickref/)
    
*   [Documentación de Playwright](https://playwright.dev/docs/intro)
    
*   [GitHub de axe-core](https://github.com/dequelabs/axe-core)
    
*   [Integración axe-core/playwright](https://github.com/dequelabs/axe-core-npm/tree/develop/packages/playwright)
    
*   [Proyecto A11Y](https://www.a11yproject.com/)
    

* * *

*Este es el segundo artículo de la serie — próximamente:* ***WCAG 2.4.7 Foco Visible: Pruebas de Regresión Visual con Playwright***
