Empezar
Auditoría de calidad
One config-driven audit for Alea and the apps built on it: axe, overflow, touch targets, focus, reduced motion, a screen-reader simulation and CSS guardrails, across viewports, themes and text sizes.
@alea-design/audit ejecuta tu app en un navegador real una vez por cada combinación de ancho, tema, tamaño de texto, idioma, puntero y ajuste de movimiento, y revisa cada ruta en todas. Es el núcleo común de los scripts de auditoría que Alea, Heliodore, Spinelle y Grenat escribieron por separado.
Instalar
El paquete no incluye un navegador. Maneja el Google Chrome del equipo (o el que indique CHROME_PATH) mediante playwright-core, y lee axe-core desde tu proyecto. Ambos son dependencias opcionales: la auditoría dice qué instalar cuando falta una.
npm i -D @alea-design/audit playwright-core axe-corenpx alea-audit init # writes alea-audit.config.mjs and a GitHub Actions workflow (one job, weekly)npx alea-audit # npx alea-audit list shows the checks
Con la CLI de Alea instalada, alea audit hace lo mismo y dice qué instalar cuando falta el paquete.
Configuración
Un solo archivo declara la app, las rutas, la matriz y las comprobaciones. La lógica queda en el paquete; el archivo son datos y unos pocos ganchos.
// alea-audit.config.mjsimport { defineConfig } from '@alea-design/audit';export default defineConfig({server: { command: 'npm run build && npx vite preview --port {port} --strictPort', url: 'http://127.0.0.1:{port}' },routes: ['/', '/pricing', { path: '/settings', setup: async ({ page }) => { await page.getByRole('button', { name: 'Load sample data' }).click(); } }],viewports: [320, 375, 768, 1024, 1280, 1920],themes: ['light', 'dark'],theme: { attribute: 'data-theme', storageKey: 'my-app-theme' },textSizes: [100, 150, 200],locales: ['en', 'es'],checks: {overflow: { scrollRegions: ['.alea-table-wrap'] },'layout-parity': true,guardrails: true,},guardrails: { files: ['src/**'], ignoreFiles: ['**/tokens.css'], rules: { 'raw-px': true, 'raw-hex': true, 'long-dash': true, 'alea-internals': true } },ignore: [{ check: 'axe', rule: 'color-contrast', route: '/legal', reason: 'The third-party widget draws this text; reported upstream.' },],});
Clave | Qué hace |
|---|---|
baseUrl / server | Una app que ya está corriendo, o cómo arrancarla: un comando con {port} (en cada ejecución se elige un puerto libre, así dos auditorías nunca chocan), o una carpeta de archivos estáticos. |
routes | Una lista, una función que devuelve una, o sitemap: true. Una ruta puede ser un objeto con su propio setup, navigate o skipChecks. Las rutas con hash, como #/plan, funcionan. |
setup | Se ejecuta una vez por sesión de navegador antes de las rutas: iniciar sesión, cargar datos de ejemplo, sembrar el almacenamiento. |
theme | Cómo se entera la app de su tema: un atributo en el elemento html, una clave de almacenamiento (para un ThemeProvider que la lee), o ambos. El esquema de color del navegador siempre se aplica. |
ignore | Hallazgos aceptados. Cada entrada necesita un motivo de al menos una frase, o se rechaza la configuración. |
flows | Guiones de acciones con sus propias verificaciones, para lo que una lectura estática no ve: enviar un formulario vacío, quitar una fila. |
failOn | La severidad mínima que hace fallar la ejecución: error (por defecto), warning, info o none. |
La matriz
La estrategia por defecto, cover, usa cada valor de cada eje al menos una vez y siempre cruza el ancho más estrecho con el texto más grande, lo que son unas pocas decenas de sesiones. Usa matrix: { strategy: "cross" } para el producto completo (los punteros táctiles solo se combinan con anchos hasta 1024).
Eje | Por defecto | Cómo se aplica |
|---|---|---|
viewports | 320, 375, 768, 1024, 1280, 1920 | El tamaño de la ventana del navegador. |
themes | light, dark | prefers-color-scheme, más el atributo o la clave de almacenamiento de la configuración del tema. |
textSizes | 100, 150, 200 | La preferencia real de tamaño de fuente del navegador, así rem, em y las media queries reaccionan como para una persona lectora. No es zoom de página ni una hoja de estilos. |
locales | en | El idioma del navegador, más el almacenamiento que la app use para su propio ajuste de idioma. |
pointers | fine, coarse | El puntero táctil activa la emulación táctil; el mínimo de tamaño de objetivo pasa a 44 px. |
motion | full, reduced | La característica de medios prefers-reduced-motion. |
Antes de que cualquier comprobación mida, la página tiene que estar quieta: sin indicador de carga, con las fuentes listas y dos pasadas seguidas sin ninguna animación finita en curso. Una pasada tranquila no basta, porque los datos que llegan tarde inician animaciones nuevas y una fila que sale sigue en el documento hasta que termina su animación. Medir antes da contrastes de 1,01 con la mitad de opacidad y botones de 43 px con una escala de 0,98.
Comprobaciones
Check | Qué revisa |
|---|---|
axe | axe-core con WCAG 2.0 a 2.2 A y AA más buenas prácticas. Las reglas de página completa se relajan mientras hay un modal abierto. El impacto se convierte en severidad. |
overflow | La página que se desplaza de lado, y las áreas con desplazamiento (scrollRegions) que se desplazan pero no son una región con nombre y foco, o se anuncian sin nada visible que desplazar. |
touch-targets | 44 px con puntero táctil, 24 px con puntero fino. Un control más pequeño pasa si una pulsación a la distancia requerida aún cae sobre él (áreas de toque ampliadas). |
focus | Un recorrido real con Tab: anillo de foco visible, sin tabindex positivo, orden, y todo lo interactivo que Tab nunca alcanza. |
console | Errores y avisos de consola, excepciones no capturadas, peticiones fallidas y errores HTTP. |
reduced-motion | Con el movimiento reducido activado, no empieza ninguna animación ni transición de más de unos milisegundos. |
layout-parity | El diseño ya asentado es el mismo con y sin movimiento. Está apagada hasta que la actives. |
unique-names | Los puntos de referencia, los avisos y las regiones con desplazamiento del mismo tipo tienen nombres accesibles distintos. |
structure | Un h1 visible, un título y un lang que coincide con el idioma que se audita. |
clipped-text | Texto cortado por una caja que oculta el desborde, sin puntos suspensivos. |
screen-reader | El árbol de accesibilidad leído como un lector de pantalla: controles sin nombre, nombres repetidos en una región, regiones vacías, tablas que faltan en la lectura, campos anunciados como no válidos al llegar. |
guardrails | Un escaneo estático del código, sin navegador: valores crudos de px, hex y ms, rayas largas en los textos, px en estilos en línea, selectores que se meten en las clases .alea-*, o tus propias reglas. |
Las comprobaciones propias van en customChecks: un objeto con un nombre, una descripción y una función run que recibe la página y devuelve hallazgos.
Simulación de lector de pantalla
La simulación lee el árbol de accesibilidad de Chrome y calcula lo que anunciaría un lector: nombre, rol, estado, posición, filas de tabla con sus encabezados, las regiones que se entran y se salen, y las regiones vivas capturadas mientras actúas. Está disponible por separado para pruebas de extremo a extremo.
import { ScreenReader, verifyErrorAnnounced } from '@alea-design/audit/reader';const reader = await new ScreenReader(page).attach();await reader.readScreen(); // what the whole screen says, in orderawait reader.press('Tab'); // { text: 'Save, button', live: [...] }const problems = await verifyErrorAnnounced(page, reader, {field: '#email',trigger: () => page.keyboard.press('Enter'), // focus is already in the field});console.log(reader.toText(), reader.issues, problems);
Límites conocidos, no hallazgos: los segmentos de los campos de fecha siguen el idioma del navegador; no hay comportamiento de puntuación ni de siglas por lector; los guiones no separables y los saldos negativos escritos con signo menos necesitan un lector real. No sustituye a VoiceOver ni a NVDA.
Integración continua
Los resultados se guardan por ruta, combinación, compilación y ajustes de las comprobaciones, así que una ejecución sin cambios termina en segundos. Un navegador que se cierra bajo carga se reinicia para esa combinación y nunca es un hallazgo. AUDIT_JOBS o --jobs fija cuántas combinaciones corren a la vez; por defecto sigue la carga de la CPU.
name: Quality auditon:workflow_dispatch:schedule:- cron: '0 5 * * 1'# pull_request:# paths: ['src/**', 'public/**', 'package-lock.json']concurrency:group: audit-${{ github.ref }}cancel-in-progress: truejobs:audit:runs-on: ubuntu-latesttimeout-minutes: 30steps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with: { node-version: 22, cache: npm }- run: npm ci --no-audit --no-fund- uses: actions/cache@v4with:path: .alea-audit/cachekey: alea-audit-${{ hashFiles('package-lock.json', 'src/**') }}restore-keys: alea-audit-- run: npx alea-audit run --jobs 2 --json audit.json- uses: actions/upload-artifact@v4if: failure()with: { name: audit, path: '.alea-audit/artifacts' }
Los repositorios privados pagan cada minuto de cada trabajo, y cada trabajo vuelve a instalar el proyecto. Por eso el flujo que escribe init es un solo trabajo que corre a pedido y una vez por semana, no en cada push; el disparador pull_request está como opción comentada para las rutas que cambian la interfaz. Una ejecución nueva cancela la anterior, una ejecución se detiene a los 30 minutos y se guardan en caché la instalación de npm y los resultados. El encabezado del archivo estima los minutos facturados.
Ejecuta npx alea-audit run --plan para contar la matriz y estimar el tiempo antes de añadir un disparador. init --shards 3 divide la matriz en tres trabajos: termina antes pero factura unos dos minutos más por cada fragmento. La auditoría usa el Chrome que ya traen los ejecutores Ubuntu de GitHub, y ejecutarla en tu equipo no cuesta nada.
El código de salida es 0 cuando no se encontró nada en failOn o por encima, 1 cuando sí, y 2 cuando la auditoría misma no pudo ejecutarse. --json escribe el informe completo y las capturas de las pantallas con fallos van a .alea-audit/artifacts.