Uso con WebdriverIO
WebdriverIO es un framework de automatización de pruebas de Node.js construido sobre el protocolo WebDriver, que soporta tanto pruebas end-to-end como pruebas de componentes. surea11y se integra mediante la integración (binding) @surea11y/webdriverio.
$ npm install @surea11y/core @surea11y/webdriverio webdriverio
WebdriverIO v9 gestiona automáticamente un chromedriver local la primera vez que lanza Chrome: una instalación limpia basta para lanzar un Chrome headless real por sí sola.
const { remote } = require('webdriverio');const { A11yCoreBuilder } = require('@surea11y/webdriverio');const browser = await remote({capabilities: {browserName: 'chrome','wdio:enforceWebDriverClassic': true, // recommended -- see below'goog:chromeOptions': { args: ['--headless=new'] }}});await browser.url('https://example.com/');const results = await new A11yCoreBuilder({ browser }).include('#main') // optional -- call multiple times for multi-region scans.exclude('.cookie-banner') // optional.withWcag('2.2', 'AA') // the rules for WCAG 2.2's A and AA criteria.disableRules(['meta-refresh-no-exceptions']).analyze();console.log(results.checksResults.filter(r => r.outcome === 'fail'));await browser.deleteSession();
.withWcag(version, level) selecciona por objetivo de conformidad: las reglas de los criterios de nivel A y AA de WCAG 2.2, según el nivel que cada criterio tiene en esa versión. .withBestPractices() añade las reglas que no comprueban ningún criterio de WCAG, como region; por sí solo ejecuta solo esas.
.withPacks() registra un pack en la página antes del análisis, y .options({ profile }) elige su perfil. Un pack que no se ejecuta se informa como una carencia del análisis de tipo 'pack-skipped', de modo que una prueba que comprueba getScanGaps(results) falla en lugar de pasar sin él.
Fuerza el WebDriver clásico, no BiDi, para esta carga de trabajo. WebdriverIO v9 usa el protocolo BiDi por defecto; una sesión de larga duración que emite muchas llamadas a browser.execute() (exactamente lo que hace un análisis) mostró una degradación grave del rendimiento con BiDi durante las pruebas: la latencia por análisis pasó de ~2s a varios minutos. Forzando el WebDriver clásico con 'wdio:enforceWebDriverClassic': true, como se muestra arriba, se obtuvo un ~150ms estable por análisis.
Componentes web. Un componente que guarda su ElementInternals en un campo privado, y se da un nombre, fija sus estados o se asigna un rol a través de él, no puede leerse desde ningún script de la página. Por defecto, el análisis pregunta por ese componente (cantTell) en lugar de juzgarlo, y un rol asignado solo de esa forma no se comprueba en absoluto. Si construyes o pruebas una biblioteca de componentes, instala el hook de internals opcional de la integración antes de que se ejecuten los scripts de la propia página, y esos componentes se juzgan como cualquier otro control:
await A11yCoreBuilder.installInternalsHook(browser); // 'bidi' or 'cdp'await browser.url('https://example.com/');
Llámalo antes de browser.url(); en el testrunner de WDIO, en un hook before. Usa browser.addInitScript() en una sesión BiDi y, si no, el protocolo DevTools a través de chromedriver en Chrome y Edge, lo que cubre la sesión clásica recomendada arriba. En una sesión clásica en Firefox o Safari, la llamada se rechaza.
El hook está pensado para ejecuciones de pruebas y auditorías: guarda lo que registra donde cualquier script de la página puede leerlo, y por eso es opcional. Sin él, el análisis avisa una vez cuando ha preguntado por esos componentes. La sección del README sobre el hook de internals tiene los detalles.
Manejadores de clic y regiones vivas. Desde core 1.14 hay dos hooks opcionales más. La mayoría de las páginas añaden sus manejadores de clic desde un script, donde ningún script de la página puede enumerarlos después, así que el análisis no ve que un <div role="button"> con un listener de clic no hace nada desde el teclado; con el hook de listeners, ese botón falla (CLICK_HANDLER_NO_KEY_LISTENER). El hook de regiones vivas registra lo que reciben las regiones vivas de la página: el análisis pregunta entonces por un mensaje de estado añadido con su texto ya dentro, que los lectores de pantalla no suelen anunciar, y announcements() devuelve lo que la página puso a disposición para anunciar, para que una prueba compruebe que una acción le dijo algo al usuario. Instálalos antes de que se ejecuten los scripts de la propia página, como el hook de internals:
await A11yCoreBuilder.installListenerHook(browser); // before browser.url()await A11yCoreBuilder.installLiveRegionHook(browser);await browser.url('https://example.com/settings');await $('#save').click();const said = await A11yCoreBuilder.announcements(browser);// [{ text: 'Saved', politeness: 'polite', ..., selector: '#status' }]
Ambos se instalan como el hook de internals, en las mismas sesiones, antes de browser.url() (en el testrunner de WDIO, en un hook before), y se resuelven en 'bidi' o 'cdp'.
Como el hook de internals, ambos son para ejecuciones de pruebas y auditorías: cualquier script de la página puede leer lo que registran. announcements() es lo que la página puso a disposición para anunciar, no lo que dijo un lector de pantalla, y es null sin el hook de regiones vivas. Sin alguno de los dos hooks, getScanNotes() avisa cuando se juzgaron sin él hallazgos que el hook habría decidido. Las secciones del README sobre el hook de listeners y el hook de regiones vivas tienen los detalles.
Varios perfiles a la vez. Con core 1.15 (binding 1.6.0), .options({ profile }) acepta también una lista, el perfil de un pack junto al de WCAG o al de otro pack: .withPacks(waiAria).options({ profile: ['wcag22-aa', 'wai-aria-1.2'] }) juzga la página con WCAG y con WAI-ARIA en un solo análisis. Una regla que ejecutan ambos es una sola comprobación: su resultado es el peor, y sus verdicts dicen qué hace de ella cada perfil (ver Opciones del motor). Un perfil que no se ejecutó figura en results.engine.profilesNotRun, y getScanGaps() lo informa como 'profile-not-run'.
Frames, leídos como los lee el navegador. Desde core 1.15, un documento en un frame no es una página propia, así que sus reglas de título, idioma, viewport y regiones de referencia son notApplicable; un frame bajo aria-hidden o inert se lee como tal, uno tras un diálogo modal abierto se deja fuera, y uno en un shadow root se lee a través de su host. Y donde solo se incumple WAI-ARIA o el modelo de contenido de HTML, las reglas de estructura ARIA y HTML ahora preguntan en lugar de fallar (qué cambió); los hallazgos conservan sus códigos de motivo, así que los baselines siguen coincidiendo. Un hallazgo de text-spacing-content-loss también se muestra en imagen con el espaciado aplicado, con .screenshots().
Modo oscuro, anchos de móvil y un umbral de gravedad. .conditions([{ colorScheme: 'dark' }, { width: 320 }]) analiza la página tal como está y de nuevo bajo cada condición, y devuelve un único resultado: el resultado de una regla es el peor de todos los análisis, y cada hallazgo indica las condiciones en las que se vio. Cada condición es un análisis completo, y .frames(true) todavía no se puede combinar con ella. En Chrome y Edge ambos pasan por el protocolo DevTools, en sesiones clásicas y BiDi; en otros navegadores un ancho redimensiona la ventana, y un esquema de color hace que .analyze() se rechace antes de analizar nada. getGatingFailures(results, { failOnSeverity: 'serious' }) devuelve las reglas que fallan con esa gravedad o más, para que una prueba bloquee primero los problemas graves y críticos mientras el resto se sigue informando; formatFailures() admite la misma opción.
Preguntas con tendencia, y sus pasos. Desde core 1.14, un hallazgo cantTell puede tener una tendencia hacia fail o pass, explicando por qué, y lleva howToCheck, los pasos que sigue una persona para responderlo. formatFailures() enumera los probables fallos justo después de los fallos, e imprime los pasos con { howToCheck: true }. Para que una prueba falle también con los probables fallos, añade al umbral leaningFailures(results) de core.
A11yCoreBuilder también tiene .wcagLevel(), que limita los criterios del informe a un nivel de WCAG sin cambiar qué reglas se ejecutan; .includeMode('or'), que ejecuta las reglas de una etiqueta más las reglas que nombres; .plannedChecks() y .plannedCriteria(), que enumeran lo que comprobará un análisis antes de ejecutarlo, para que una selección que no ejecuta nada no pase por un análisis limpio; .logger(), que envía los avisos de carencias del análisis a tu propio log; .fetchStyleSheets(false), que evita que .analyze() descargue las hojas de estilo de otros orígenes que la página no puede leer por sí misma, para las reglas que leen el CSS; y getScanNotes(results), que dice cómo se analizó la página. El README de la integración explica cada uno.
Consulta también un ejemplo ejecutable en el propio repositorio del paquete.
Siguiente: elige qué reglas se ejecutan, integra el análisis en CI y consulta la estructura completa del resultado.