Uso con Selenium

Selenium WebDriver es el estándar histórico para controlar navegadores reales desde cualquier lenguaje: la herramienta de automatización multinavegador más consolidada, usada habitualmente para pruebas end-to-end. surea11y se integra mediante la integración (binding) @surea11y/selenium.

$ npm install @surea11y/core @surea11y/selenium selenium-webdriver

selenium-webdriver incluye Selenium Manager, que descarga y gestiona automáticamente la versión correspondiente de ChromeDriver la primera vez que construyes un driver: no hace falta un servidor Selenium aparte ni instalar el driver manualmente (Chrome sí debe estar ya instalado en la máquina).

const { Builder, Browser } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const { A11yCoreBuilder } = require('@surea11y/selenium');
 
const options = new chrome.Options().addArguments('--headless=new');
const driver = await new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).build();
await driver.get('https://example.com/');
 
const results = await new A11yCoreBuilder({ driver })
.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 driver.quit();

.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.

El builder recibe { driver } (un WebDriver de Selenium), mientras que Puppeteer/Playwright reciben { page }: esa es la única diferencia de construcción. Todo lo demás es idéntico.

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:

const options = new chrome.Options().enableBidi(); // the webSocketUrl: true capability
const driver = await new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).build();
 
await A11yCoreBuilder.installInternalsHook(driver); // 'bidi' (or 'cdp' without BiDi on Chrome)
await driver.get('https://example.com/');

Llámalo antes de driver.get(). El WebDriver clásico no puede ejecutar un script antes de los de la página, así que el hook usa WebDriver BiDi cuando la sesión lo tiene (Chrome, Edge y Firefox, con BiDi activado al construir el driver, como arriba) y, si no, el protocolo DevTools en Chrome y Edge; se resuelve con 'bidi' o 'cdp'. En una sesión clásica en Safari, o en Firefox sin BiDi, 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(driver); // before driver.get()
await A11yCoreBuilder.installLiveRegionHook(driver);
await driver.get('https://example.com/settings');
await driver.findElement(By.css('#save')).click();
 
const said = await A11yCoreBuilder.announcements(driver);
// [{ text: 'Saved', politeness: 'polite', ..., selector: '#status' }]

Ambos se instalan como el hook de internals, antes de driver.get(): mediante WebDriver BiDi cuando la sesión lo tiene y, si no, mediante el protocolo DevTools en Chrome y Edge. Cada uno se resuelve en 'bidi' o 'cdp'. By viene de selenium-webdriver.

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 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.