Limitaciones conocidas
Qué no evalúa el motor, qué depende de dónde se ejecuta un análisis (jsdom, un navegador o los propios scripts de la página) y qué necesita la revisión de una persona. Algunos puntos son decisiones de diseño, cada una con su razonamiento; cada uno indica qué no puede establecer un análisis y, cuando lo hay, qué hacer al respecto. Léelo antes de confiar en un resultado sin hallazgos.
Qué hacer al respecto
| Si… | Entonces… |
|---|---|
| El contenido aparece solo tras un clic, un hover o una carga de datos | Lleva tú mismo la página a ese estado y luego analízala. Analiza cada estado que importe. |
| Una comprobación necesita diseño: tamaño del objetivo, espaciado del texto, contraste frente a lo que se pinta bajo el texto | Analiza en un navegador real. jsdom no tiene diseño (el engine.environment.layout del resultado es false), así que estas reglas miden menos ahí o informan notApplicable, como indican las secciones siguientes. |
| Un requisito trata del manejo con teclado, como una trampa de teclado | Pruébalo con un teclado. Ningún análisis lo cubre. |
Un resultado es cantTell | Sigue los pasos de howToCheck de la ocurrencia y decídelo a mano. uncertainty dice qué faltaba, y lean hacia dónde apunta la evidencia de la página (consulta el esquema de salida). |
| La página es adaptable | Analiza con cada ancho que te importe. |
| Dos ejecuciones de la misma página difieren | Compara engine.version y engine.environment (viewport, fuentes e imágenes aún cargando), y recarga la página entre análisis. |
| Un análisis no informa de fallos | Eso no es una declaración de conformidad. Consulta Mapeo de conformidad con WCAG. |
Las secciones siguientes dan las razones.
Fuera de lo que un análisis puede ver
Un análisis lee la página en un momento, en un estado y en un viewport: el DOM, sus estilos calculados y, en un navegador real, su diseño. No usa la página como lo hace una persona. No la recorre con un teclado, no abre sus menús, no espera a sus datos ni cambia su tamaño. (En un navegador, algunas reglas enfocan un elemento o añaden una hoja de estilo mientras dura su comprobación, y lo restablecen; consulta la sección siguiente.) Los puntos de aquí necesitan ese tipo de interacción en el tiempo, así que otra regla no puede cubrirlos. Necesitan una prueba de interacción o una persona:
- Detección de trampas de teclado (WCAG 2.1.2): requiere mover el foco por la página y observar dónde termina, algo que ninguna lectura única del DOM puede hacer. Se ha definido el alcance de una técnica que funcionaría (consulta la sección sobre trampas de teclado de
ACT_RULE_MAPPING.md), pero no está implementada: un análisis no explora las interacciones del usuario ni los estados a los que llevan, y ninguna de las comprobaciones que modifican la página brevemente mueve el foco por ella como lo haría la detección de trampas. Mientras tanto, nada en el marcado estático la sustituye. - Reflujo / recorte al aplicar zoom (WCAG 1.4.10): requiere una medición real del diseño (
clientWidth/scrollWidth) en un viewport simulado equivalente a 320px. A diferencia de algunas heurísticas basadas en declaraciones CSS presentes en otras partes de este motor, no existe ningún sustituto en marcado estático para determinar si "el contenido se recorta al 400% de zoom". - Estado dinámico o posterior a la interacción: todo lo que solo existe después de un clic, un hover o una carga de datos asíncrona (el contenido de un modal, las opciones de un desplegable, los mensajes de error de validación de un formulario) es invisible para un análisis del DOM actual de la página. Si tu framework lo renderiza de forma anticipada (incluso fuera de pantalla u
hidden), es analizable; si solo existe tras la interacción, no lo es, a menos que produzcas esa interacción tú mismo antes de analizar (por ejemplo, haz clic en el botón y luego analiza). Analizar el estado abierto comprueba lo que hay en él, no si una persona que usa teclado o lector de pantalla puede abrirlo, usarlo y cerrarlo.
Dependientes del entorno: depende de cómo lo ejecutes
- jsdom (Node, sin navegador real) no tiene motor de diseño CSS. Las reglas que necesitan geometría real (sobre todo
target-size-minimum, WCAG 2.5.8, necesita ungetBoundingClientRect()real) informannotApplicablebajo jsdom puro en lugar de adivinar. Ejecuta bajo un navegador real (Puppeteer/Playwright, consultaINTEGRATION.mdPatrón 2) para obtener resultados reales de estas reglas. - Saber si el texto hace salto de línea requiere diseño, así que los hallazgos de text-spacing en texto que no hace salto se informan para revisión. WCAG 1.4.12 y las reglas ACT que lo sustentan se aplican solo a texto que contiene un salto de línea suave, algo que un análisis estático no puede determinar.
avoid-inline-spacingtrata el texto como si hiciera salto de línea por defecto, así que un valor forzado ordinario por debajo de la métrica sigue fallando; cuando el salto de línea no es posible por una razón que sí es visible sin diseño (texto al que no se le permite hacer salto o un elemento de ancho fijo dentro de un ancestro con desplazamiento horizontal), informacantTellen su lugar. El texto que nunca hace salto de línea por otro motivo sigue informándose como fallo. - El motor se ejecuta sobre los objetos integrados de JavaScript de la propia página. Una página cuyos scripts rompen uno del que depende (
Array.prototype.filter,Map,JSON.stringify,String.prototype.toLowerCasey otros) detiene el análisis con un error de códigoPAGE_BUILTINS_BROKENque nombra cada uno, en lugar de dar un resultado que parece correcto y es erróneo; uno roto antes de inyectar el bundle (Object.keys) impide que el bundle se cargue. Una página que añadethenaObject.prototypese analiza como cualquier otra. Una página que sustituyeArray.from, como hacen Prototype y MooTools, también se analiza (desde core 1.16.0): el código propio de core no lo usa. Una regla de un pack o personalizada que lo usa detiene ahí el análisis, conPAGE_BUILTINS_BROKENnombrandoArray.from, salvo que la selección deje fuera la regla. Los métodos del DOM que una página sustituye (getComputedStyle,querySelectorAll,getBoundingClientRect) no se comprueban: las reglas leen lo que devuelven. - Un nombre no se calcula a partir de contenido anidado a más de 256 niveles de profundidad. El recorrido se detiene ahí (Chrome no da ningún nombre a partir de contenido a 100 niveles de profundidad), así que
button-name-present,link-name-present, las reglas de nombre de elemento de menú, opción, summary, pestaña, tooltip y elemento de árbol, yempty-headingpreguntan (name_contentTooDeep) por un elemento cuyo único nombre está más abajo; otras reglas que obtienen el nombre del contenido leen ese contenido como vacío. - El contenido de una raíz shadow cerrada no se puede leer. Ningún script llega dentro de
attachShadow({ mode: 'closed' })ni de unshadowrootmode="closed"declarativo, así que el motor no puede ver el nombre que un navegador obtiene de ahí.button-name-presentylink-name-present, y las reglas de nombre de elemento de menú, opción, pestaña, elemento de árbol, summary y tooltip, preguntan por un elemento cuyo nombre podría venir de un elemento personalizado que se renderiza pero no muestra contenido legible, en lugar de hacerlo fallar. Una raíz cerrada en un elemento incorporado (un<span>) parece una vacía, como un icono dibujado con CSS, así que un botón sobre una sigue fallando. Comprueba a mano lo que expone ese componente, por ejemplo con un lector de pantalla. - No se puede leer un nombre o un estado que un elemento personalizado define mediante
ElementInternals.this.attachInternals().ariaLabel = 'Save'da nombre al elemento en todos los navegadores, pero ningún script puede volver a leerlo. Cuando un componente guarda sus internals en una propiedad (_internals,internals,internals_,__internals), el motor los lee a través de ella, y aprueba o hace fallar el elemento según lo que digan. Para cualquier otro elemento personalizado definido sin nombre (o sin un estado obligatorio), las reglas de nombre yaria-required-attrpreguntan (name_internals,state_internals) en lugar de hacerlo fallar. Compruébalos en el árbol de accesibilidad del navegador, o instala el hook de internals opcional antes de que se ejecuten los scripts de la página:installInternalsHook()en cada integración (consulta Primeros pasos) y en test-matchers, ointernalsHookScript()de core para una página propia (consultaINTEGRATION.md, que muestra dónde instalarlo). El hook registra los internals de cada elemento para el análisis, así que también reconoce un elemento que nunca adjuntó ninguno. Un rol definido solo mediante internals (internals.role) se lee donde se leen los internals, a través de una propiedad o del hook, como el rol predeterminado del elemento; guardado en un campo privado sin el hook, no se ve, y no se pregunta por el elemento, ya que casi cualquier componente podría tener uno. Los elementos incorporados, y los elementos personalizados que no están definidos, se juzgan como antes. - No se pueden enumerar los manejadores de eventos añadidos desde script. Un manejador añadido con
addEventListener, o asignado como propiedad (el.onclick = fn), no deja nada en el marcado y ningún script puede leerlo, así que las reglas de teclado (click-handler-keyboard-operable,mouse-only-event-handlers) solo juzgan los atributos de manejador en línea: unrole="button"enfocable que recibe su manejador de clic desde script no se informa (por uno que no puede recibir el foco pregunta igualmentewidget-keyboard-focusable), un manejador de teclado añadido desde script no puede justificar unonclicken línea, ypassword-paste-enabledno puede saber si un manejador de pegado añadido desde script devuelve el texto que cancela unonpasteen línea. Instala el hook opcional de manejadores (listenerHookScript()) antes de que se ejecuten los scripts de la página: las reglas juzgan entonces también los manejadores que registra y, donde no vio ningún manejador de teclado, hacen fallar un control que Intro y Espacio no pueden accionar. Un manejador de clic que un framework añade a su elemento raíz sirve a elementos que nada nombra, así que incluso con el hook esos se juzgan por su marcado. Dónde instalarlo:INTEGRATION.md, en GitHub. - No se puede ver después cómo se llenó una región viva. Un mensaje de estado se anuncia cuando una región viva cambia mientras la tecnología de asistencia puede alcanzarla (4.1.3), pero un análisis ve la página en un momento. Sin ayuda,
live-region-announcementssolo juzga lo que hay: una región viva oculta a la tecnología de asistencia conaria-hiddenoinert, o detrás de un<dialog>modal abierto, o que se quedó enaria-busy. Con el hook opcional de regiones vivas (liveRegionHookScript()), pregunta además por una región que la página añadió ya con su texto, o que mostró y llenó a la vez, ygetAnnouncements()enumera lo que la página dejó disponible para anunciar. Ninguno de los dos dice qué pronunció un lector de pantalla: eso depende del lector. El hook no ve dentro de los shadow roots. Detalles:INTEGRATION.md, en GitHub. - Un análisis hecho con un diálogo modal abierto ve el diálogo, no la página. Un
<dialog>abierto conshowModal()vuelve inerte el resto del documento, y los navegadores solo exponen el diálogo a la tecnología de asistencia. Un diálogo abierto con el atributoopeno conshow()no es modal, aunque tengaaria-modal="true": la página de detrás sigue siendo alcanzable, y el motor la juzga. jsdom no tiene capa superior y no puede abrir un diálogo modal, así que con jsdom nada queda detrás de uno, y el mismo marcado recibe los mismos veredictos que en un navegador (desde core 1.16.0); una prueba con jsdom que sustituyeshowModal()poniendoopenyaria-modal="true"en el diálogo poneengineOptions.assumeModal: truepara que ese diálogo se tome como modal. De varios diálogos modales, solo se juzga el superior. El motor sigue al diálogo modal: el contenido detrás de él queda fuera de todas las reglas, igual que el resto del contenido oculto, y no recibe el foco, y las reglas sobre la estructura de la propia página (page-has-heading-one,landmark-one-main,bypass-blocks-present,region) informannotApplicable, porque el análisis no puede ver esa estructura. Vuelve a analizar la página con el diálogo cerrado para juzgar lo que hay detrás. Un modal propio, un elemento mostrado conrole="dialog"orole="alertdialog"yaria-modal="true", también deja esas reglas de estructura ennotApplicable;aria-modalno vuelve nada inerte, así que todas las demás reglas siguen juzgando la página de detrás. Un diálogo modal dentro de un shadow root abierto cuenta como uno del documento; uno en un shadow root cerrado no se puede encontrar. Un modal propio sinaria-modal="true"solo se trata a través delinerto elaria-hiddenque la página pone en el resto, que ocultan la página pero no indican a las reglas de estructura que hay un diálogo abierto. - Un documento mostrado en un frame no se juzga como una página propia. En un frame, las reglas sobre la página (
page-title-present,page-title-patterns,html-lang-attr-present,meta-viewport-zoom-enabled,meta-viewport-large,page-has-heading-one,landmark-one-main,bypass-blocks-presentyregion) sonnotApplicable: la página que rodea el frame es la que tiene el título, el idioma y las regiones de referencia que usan las personas, y un navegador ignora el<meta>viewport de un documento en un frame. Un ejecutor de pruebas que muestra la página bajo prueba en un frame propio estableceengineOptions.topLevelDocument: true, para que esa página se juzgue como la página (@surea11y/cypress lo hace).runa11yCoreAcrossFrameslee un frame bajoaria-hidden="true"como contenido aria-hidden y uno bajo[inert]como contenido inerte, en los que solo se juzga cómo se ve el contenido, y deja fuera un frame tras un diálogo modal abierto; un binding que analiza los frames uno a uno establece él mismoembeddedAriaHiddenyembeddedInert(ver Opciones del motor). Core 1.15.0 y posteriores. <dialog>y otros elementos ocultos por la hoja de estilos predeterminada del agente de usuario (sin atributoopen,display: nonesegún la especificación), junto con cualquier otro subárbol oculto mediantedisplay:none,visibility:hidden,[hidden]o un<details>cerrado, y un subárbol[inert](que las reglas sobre el aspecto del contenido, como el contraste y el espaciado del texto, siguen juzgando, ya que se dibuja), quedan excluidos de la evaluación de reglas por defecto. Este es un valor predeterminado deliberado (engineOptions.includeHiddenElements: false), no un descuido: el contenido oculto no es accesible mediante tecnología de asistencia ni teclado hasta que se muestra, así que marcar por defecto un defecto de marcado dentro de él a menudo sería ruido. DefineengineOptions.includeHiddenElements: truepara evaluar igualmente subárboles ocultos o colapsados: por ejemplo, para detectar un defecto de marcado (como una referencia de ID ARIA rota) antes de que un diálogo llegue a abrirse. ConsultaENGINE_OPTIONS.mdpara conocer la opción y exactamente qué mecanismos de ocultación cubre.- No se informa de un
aria-detailsque nombra contenido oculto.aria-valid-attr-valuecomprueba que el id nombra un elemento, no que el elemento se muestre. Chromium deja la relación fuera de su árbol de accesibilidad mientras el elemento está oculto y la recupera cuando se muestra, así que unos detalles que se abren a petición se anuncian al abrirse, y WCAG no pide más. Para comprobar la relación tal como la recibe la tecnología de asistencia, analiza la página con los detalles visibles. - jsdom no tiene popovers. Incluye la regla de HTML que oculta un
[popover]cerrado, pero noshowPopover(), y lee:popover-opende forma inconsistente. Cuando el DOM no tieneshowPopover, el motor trata todo[popover]que no sea un<dialog>abierto como cerrado y oculto, así que su contenido no se comprueba; un popover que el propio CSS de la página mantiene visible tampoco se comprueba bajo jsdom. En un navegador, el motor lee lo que se muestra. Para comprobar el contenido de un popover, analiza la página en un navegador con el popover abierto. - Bajo jsdom, el texto de
contentde CSS no da nombre. jsdom no calcula estilos para::beforeni::after, así que un botón o un enlace cuyo único texto es generado (content: "Save",content: attr(data-label)) tiene nombre en un navegador y no lo tiene bajo jsdom, dondebutton-name-present,link-name-presenty las demás reglas de nombre lo hacen fallar. Analiza esas páginas en un navegador. - jsdom conserva el tamaño de fuente tal como está escrito. Un navegador informa de un tamaño de fuente calculado en px; jsdom conserva una palabra clave o un
calc()tal como está escrito. El motor lee las palabras clave con los tamaños de un navegador para un valor predeterminado de 16px (xx-largees 32px) y elcalc()de longitudes y números, así que el texto grande se distingue como en un navegador.largerysmaller, que dependen del tamaño del padre, no se resuelven bajo jsdom: ese texto se somete a la relación del texto normal, y su mensaje indica su tamaño como 0px. Un navegador resuelve ambos. - jsdom ignora las hojas de estilo propias de un shadow root. Confirmado en jsdom 29.1.1: ni un
<style>dentro de un shadow root ni susadoptedStyleSheetsllegan al estilo calculado de sus elementos, así que bajo jsdom no se ven los colores, los tamaños ni eldisplay: noneovisibility: hiddendefinidos ahí. Las reglas juzgan entonces el contenido de un componente como si no tuviera estilos: el contraste se mide con los colores predeterminados, y el contenido que su CSS oculta se juzga como visible. La única excepción es el color de un enlace, que los helpers de contraste calculan a partir de las propias hojas del shadow root. Analiza en un navegador las páginas hechas con componentes web con estilos. - El
text-shadowcalculado por jsdom no es fiable en una segunda lectura del mismo elemento. Confirmado en jsdom 29.1.1: leer un valor calculado detext-shadowpor segunda vez en el mismo elemento (mediante cualquier propiedad de acceso, desde cualquierCSSStyleDeclarationrecién solicitado para ese elemento, sin importar el caché) devuelve de forma silenciosa un valor distinto e incorrecto de "sin sombra" en lugar del valor realmente declarado. La primera lectura siempre es correcta. Este motor lo evita internamente leyendo eltext-shadowde cada elemento exactamente una vez por ejecución y guardando el resultado en caché (consulta__textShadowInfoElensrc/core/contrast-helpers.js), así que un único análisis no se ve afectado. Solo aparece si tú mismo leesgetComputedStyle(el).textShadowmás de una vez sobre el mismo elemento analizado por jsdom. Un navegador real no tiene este error. - Un segundo análisis del mismo DOM de jsdom puede perder un estilo que sobrescribe una propiedad abreviada. jsdom conserva el estilo calculado de un elemento una vez leído, hasta el siguiente cambio del DOM, y la copia que devuelve de esa caché pierde una propiedad desarrollada que sobrescribe una abreviada de una regla menos específica: con
*{background:#888}ystyle="background-image:url(hero.png)", la primera lectura da la imagen y todas las siguientesnone. Un análisis conserva el estilo que leyó primero, así que no se ve afectado, pero un segundo análisis del mismo DOM sin cambios puede no ver la imagen y aprobar un texto por el que el primero preguntó (contrast-computable). Analiza un parseo nuevo, o cambia el DOM entre análisis, lo que vacía la caché de jsdom;@surea11y/core/testinglo hace en su propio segundo análisis. Un navegador real no tiene este error. - Bajo jsdom, el tiempo de análisis crece con el cuadrado de la profundidad del DOM, no con el número de elementos. jsdom resuelve el CSS heredado recorriendo la cadena de ancestros de un elemento en cada llamada a
getComputedStyle, así que una llamada por elemento cuesta O(número de elementos × profundidad). Medido en jsdom 29.1.1 sin código del motor involucrado: 4000 elementos en una sola cadena tardan 8,3s solo engetComputedStyle, frente a 0,25s para esos mismos 4000 como hermanos. El motor lee los ancestros de cada elemento hasta la raíz, una vez por análisis, aunque las reglas de contraste miden cada texto frente a cada fondo que tiene encima; la mayor parte del tiempo sigue siendo de jsdom. jsdom además resuelve un valor heredado recorriendo recursivamente los elementos de encima, lo que agota la pila a unos mil niveles de profundidad: el motor lee una propiedad de un elemento a más de 100 niveles de profundidad resolviendo primero los elementos de encima, así que todo análisis lee el mismo valor. Afecta arunDomRulesInPagey a todo lo construido sobre él, incluido@surea11y/test-matchers; un navegador real calcula el estilo heredado de forma nativa y no tiene esta forma de coste. Los frameworks de componentes anidan habitualmente entre 100 y 300 niveles, lo cual sigue siendo cómodamente rápido; se vuelve perceptible a partir de aproximadamente 1000. - Bajo jsdom, el tiempo de análisis también crece con el tamaño de la hoja de estilo. jsdom calcula un estilo comparando cada regla CSS con el elemento. El motor no modifica el DOM durante un análisis, así que cada elemento se calcula una sola vez, pero esa vez cuesta O(reglas): en una página de noticias guardada (7916 elementos, 2683 reglas CSS, jsdom 29.1.1), el primer cálculo de 5735 elementos tarda unos 224 s de un análisis de 243 s. Ese es el coste de jsdom; un navegador real calcula la misma página en una fracción de segundo. jsdom además deja sin sustituir las propiedades personalizadas, así que un color definido con
var()no se puede leer ahí: se pregunta por el texto o el fondo (contrast-computable) en lugar de medirlo. Usa un navegador real para páginas grandes de producción. - Bajo jsdom, el CSS en capas y condicional se aplica a lo que oculta un elemento, con los medios de jsdom. El
getComputedStylede jsdom no lee ninguna regla dentro de@layer,@supportso@container, así quehiddenysr-onlyde una página con Tailwind v4 no ocultaban nada ahí. Desde core 1.16.0 el motor aplica él mismo esas reglas a las propiedades que lee para saber si un elemento se muestra, pero toma@mediacomo jsdom: solo se aplicanallyscreen, así que una regla bajo(width >= 48rem)no cuenta dentro de una capa más que fuera de ella, yhidden md:blockde Tailwind sigue oculto con jsdom donde un navegador de escritorio lo muestra.@supportsse cumple para cualquier prueba de propiedad y para unselector()que se pueda analizar,@containerno se aplica, y un valorrevert,revert-layerovar()deja el de jsdom. Las demás propiedades, entre ellas los colores, siguen siendo las de jsdom. Analiza en un navegador una página que cambia lo que muestra según el viewport. - Bajo jsdom, el color de un enlace se calcula a partir de las reglas de la propia página. jsdom ordena su propio
:link { color: #0000EE }con las reglas de la página solo por especificidad, así que ela { color: #fff }de una página pierde frente a él, mientras que un navegador aplica la regla de la página. Bajo jsdom, el motor calcula el color de un enlace, y del texto que contiene, a partir de las reglas de la página como lo hace la cascada de un navegador; un enlace que ninguna regla de la página colorea conserva el color de enlace del navegador, y un enlace se considera no visitado. Donde eso no se puede hacer (una hoja de estilo que no se puede leer, unvar(), una regla dentro de@supports,@layer,@containero anidada en otra), se pregunta por el texto (contrast-computable,FOREGROUND_UNPARSABLE) en lugar de medirlo en el azul de jsdom. En un navegador se lee el color calculado tal cual. - Bajo jsdom, el texto se juzga por su
font-sizecalculado. En un navegador, las reglas de contraste yp-as-headingjuzgan el texto por el tamaño al que se dibuja, incluidos elzoomde CSS, una transformación SVG yfont-size-adjust, este último a partir de las métricas de la propia fuente (font-size: 24px; font-size-adjust: 0.3dibuja Arial a unos 14px, que no es texto grande). jsdom no maqueta nada ni lee ninguna fuente, así que ahí elfont-sizese toma tal cual. Analiza en un navegador una página que escala así su texto. - En un navegador real, algunas reglas modifican la página mientras dura su comprobación, y los propios scripts de la página pueden reaccionar. Para medir lo que dibuja el navegador,
css-hidden-focusenfoca elementos como lo haría el teclado y desactiva las transiciones en el elemento que mide;aria-hidden-focusenfoca un elemento dentro de un subárbolaria-hiddenpara ver si la página retira el foco de inmediato (un centinela de trampa de foco);text-spacing-content-lossañade una hoja de estilo con el espaciado de WCAG 1.4.12; las reglas de contraste añaden un frame vacío fuera de la pantalla para leer los colores propios del navegador para un texto provisional o un<select>desplegable. El análisis también lleva cada animación en curso a un punto fijo mientras se ejecutan las reglas (una finita a su final, una infinita a su inicio), así que un fundido de entrada se mide ya asentado. Todo esto se restablece antes de que termine la regla o el análisis: el foco, la posición de desplazamiento, los atributos style, las animaciones, la hoja de estilo añadida y el frame. Lo que no se puede restablecer es lo que hagan los scripts de la página en respuesta, ya que enfocar un elemento dispara sus manejadoresfocusyfocusin: un carrusel puede pasar a la diapositiva enfocada, moviendoaria-hiddencon ella, o activar un botón. Los hallazgos de ese análisis son los de la página tal como se encontró:aria-hidden-focuslee qué subárboles están ocultos, y el marcado que informa, antes de enfocar nada. Pero la página queda como la dejaron sus scripts, así que un segundo análisis de la misma página puede encontrar otra cosa: si analizas una página que sigues usando, o analizas una página dos veces, recárgala entre medias. Como el espaciado se vuelve a quitar, una captura de pantalla hecha tras el análisis muestra como si cupiera el texto quetext-spacing-content-lossencontró cortado; ese hallazgo nombra el estado en que se hizo (data.details.evidenceState: 'text-spacing'), yevidenceState()de@surea11y/corevuelve a poner la página en ese estado para la imagen, como hacen los.screenshots()de los bindings. - Una hoja de estilo de otro origen no se puede leer desde la página. El script de una página no puede enumerar las reglas de una hoja que sirve otro origen (una CDN, un servicio de fuentes), así que
css-orientation-lockpregunta por ella (STYLESHEETS_NOT_READABLE), ycss-hidden-focusycss-focus-indicator-suppressedno pueden ver una regla de foco en ella. Pasa su texto enengineOptions.styleSheetTexts:preloadStyleSheets()descarga las que el servidor comparte (CORS), y una herramienta que controla el navegador puede descargar las demás desde fuera de la página (consultaINTEGRATION.md, en GitHub). - Marcado estático frente a estado del DOM en vivo o posterior a la hidratación. La lógica de las reglas en sí es agnóstica respecto al origen del DOM: evalúa cualquier DOM que reciba, ya sea HTML estático analizado por jsdom (Patrón 1) o una pestaña de navegador real ya cargada e hidratada (Patrón 2, consulta
INTEGRATION.md). Pero la CLI (npx @surea11y/cli scan <url>) obtiene específicamente solo HTML estático, sin ejecución de JS. Consulta la documentación de la CLI. Para un widget hidratado por un framework de JS cuyo marcado renderizado en el servidor entrega intencionadamente un estado antes de que el JS del cliente lo sincronice (por ejemplo,<input type="checkbox" aria-checked="true">entregado antes de que el JS del cliente establezca la propiedad nativacheckedpara que coincida durante la hidratación, un patrón extremadamente común y totalmente legítimo), un análisis de la CLI solo ve el marcado previo a la hidratación. Un análisis que se ejecuta dentro de una pestaña de navegador real ya cargada ve en cambio el estado posterior a la hidratación, así que ambos pueden discrepar exactamente en esta clase de elemento por razones que no tienen nada que ver con la corrección de la regla. Por esoaria-checked-state-mismatchestá limitado amanual/cantTellen lugar de unfailestricto. Si necesitas precisión de DOM en vivo para comprobaciones sensibles a la hidratación, ejecuta la librería directamente contra una página ya cargada mediante el Patrón 2, no la CLI de obtención estática. - El diseño se mide en un único viewport, con las fuentes que tiene la página en ese momento. Las reglas que miden lo que dibuja el navegador,
text-spacing-content-loss,target-size-minimumyskip-link, valen para el viewport en el que se analizó la página: una página adaptable puede recortar texto, o encoger un objetivo, con un ancho y no con otro, y un texto medido antes de que cargue su fuente web tiene otro ancho. El resultado registra ambas cosas enengine.environment, y los hallazgos de esas reglas repiten su viewport e indican lo que se midió (cuánto sobrepasó el texto el borde, qué tamaño tiene un objetivo y a qué distancia está su vecino), de modo que un hallazgo que difiere entre ejecuciones se puede atribuir a las condiciones. Analiza con cada ancho que te importe, y espera a que la página se estabilice antes de analizar:waitForPageReady()espera al evento load, a las fuentes web y a las imágenes (consultaINTEGRATION.md, que muestra cómo llamarla antes de un análisis), y el resultado registra enengine.environmentsi las fuentes o las imágenes aún estaban cargando. - En un navegador, el contraste se mide frente a lo que se pinta bajo el texto, tal como está la página en ese momento. Se pregunta por el texto sobre una imagen, un degradado o una pintura que solo lo cubre en parte (
contrast-computable, código de motivoBACKGROUND_OVERLAP) en lugar de medirlo. El texto que una caja conoverflow: hiddenmantiene totalmente fuera de la vista, como una diapositiva de carrusel fuera de su pista, se deja fuera hasta que está a la vista, así que analiza la página en ese estado para comprobarlo. Las cajas fijas y sticky sobre el texto se ignoran. Bajo jsdom, el texto se mide solo frente a sus ancestros. Analizada en colores forzados (alto contraste de Windows, o unforced-colors: activeemulado), la página se mantiene en sus propios colores mientras se mide, así que los resultados son los de colores forzados desactivados, salvo que el propio CSS de la página reaccione a los colores forzados (una media queryforced-colors, o un color del sistema comoGrayText), lo que el análisis indica enengine.environment.forcedColorsCss; donde no se puede (una Content Security Policy que bloquea el<style>del motor en un navegador que no puede adoptar una hoja de estilo), los colores medidos son en parte los del navegador, y cada ocurrencia de contraste lo indica (details.forcedColors). Cómo funciona la medición, paso a paso:LAYOUT_MEASUREMENT.md, en GitHub. - El texto de una página sin fondo se mide sobre el lienzo del esquema de color de la página, donde ese lienzo se conoce. El navegador pinta su lienzo detrás de una página que no pinta ningún fondo opaco, con el color
Canvasdel esquema de color en que se dibuja la página, que decide elcolor-schemede la propia página: blanco para una página que no declara ninguno, prefiera lo que prefiera el usuario, y el lienzo oscuro para una que se dibuja en oscuro. Las reglas de contraste miden ahí el texto en cualquiera de los dos modos. No se mide, ycontrast-computablepregunta por él (BACKGROUND_NOT_OPAQUE_AT_ROOT): el texto dentro de un frame, cuyo lienzo puede dejar ver la página que lo rodea, y el texto en colores forzados, donde el lienzo es la paleta del usuario. - Bajo jsdom, el texto se mide con los colores del esquema en que se dibuja la página donde se pueden calcular, y se pregunta por él donde no. jsdom calcula los colores del esquema claro y no aplica ninguna regla
prefers-color-scheme. En una página que se dibuja en oscuro (color-scheme: darken la raíz o en<meta name="color-scheme">), el texto que colorea el esquema toma los colores del esquema oscuro (los de Chromium), y un color de texto dado comolight-dark(),CanvasText,LinkTextoCanvasse calcula para el oscuro. Por el texto cuyos colores en ese esquema no se pueden calcular se pregunta (contrast-computable,COLOR_SCHEME_NOT_RENDERED;link-in-text-block,COLOR_NOT_COMPUTABLE) en lugar de medirlo con los colores claros. En un navegador, los colores son los que dibuja. - En una página que admite claro y oscuro, el esquema que el análisis no ve solo se mide donde los colores del texto no pueden cambiar con él. Un análisis ve el esquema que prefiere el navegador (jsdom: claro). El texto sobre el lienzo se mide también en el otro esquema, salvo donde una regla
prefers-color-scheme, un valorlight-dark()o un color del sistema que define la página alcanza el color del texto o un fondo detrás de él, o donde una hoja de estilo no se puede leer; tampoco el texto dibujado por un contorno, una sombra,::first-lineo un pseudoelemento, ni el texto medido sobre otra pintura.data.details.colorSchemesCheckedenumera los esquemas medidos; vuelve a analizar con el navegador en el otro esquema para comprobarlo. - El texto generado solo se mide en un navegador, y solo donde se puede leer. El texto que escribe el
contentde un::beforeo un::aftery el::markerde un elemento de lista se miden con el color y la fuente del pseudoelemento; se pregunta en su lugar (contrast-computable) por el contenido que no se puede leer (un contador en un@counter-stylepropio de la página) y por un::beforeo::afterposicionado fuera del flujo sin fondo propio. Los marcadores solo se buscan en<li>. Bajo jsdom, que no calcula estilos de pseudoelementos, el texto generado se deja fuera. - El texto y el texto provisional de un campo de formulario se miden tal como los dibuja el campo, con huecos. No se mide el texto que dibuja un navegador en un campo de fecha u hora vacío, ni el botón y el nombre de archivo de un input de archivo. Un
<select>desplegable se mide tal como lo dibuja el control cerrado, con su opción mostrada en los colores del propio select; otra de sus opciones con colores propios solo se muestra en la lista que dibuja la plataforma al abrirse, y escantTell(SELECT_OPTION_COLOR). Un texto provisional por debajo de su relación de contraste falla (PLACEHOLDER_BELOW_THRESHOLD) cuando la página eligió sus colores, cumple la relación cuando todos son los del propio navegador (técnica G148), y escantTellcuando eso no se puede saber. Bajo jsdom, los textos provisionales no se miden en absoluto. Detalles:LAYOUT_MEASUREMENT.md. target-size-minimumcalcula la región de impacto a partir del diseño, no mediante pruebas de impacto (hit-testing). Los objetivos por debajo del pliegue o dentro de una caja con desplazamiento se miden enteros, como si estuvieran desplazados a la vista; las cajas fijas y sticky sobre un objetivo se ignoran; las transformaciones 3D, la mayoría de las formas declip-pathy el recorte redondeado de un ancestro se miden como una caja delimitadora; y los enlaces separados solo por separadores ("Edit | Delete") que se amontonan entre sí se preguntan en lugar de quedar exentos. Bajo jsdom, la regla esnotApplicable. Detalles:LAYOUT_MEASUREMENT.md.- El contraste se calcula en sRGB. Los colores dados en
oklch(),oklab(),lab(),lch()ocolor()(también la forma calculada decolor-mix()y de los colores relativos) se convierten a sRGB, tal como los muestra una pantalla estándar; uno fuera de sRGB, como un rojodisplay-p3saturado, se recorta a su borde, que es lo que Chromium dibuja en una pantalla sRGB. Una pantalla de gama amplia puede mostrar ese color más saturado de lo que supone la relación calculada. Un color de fondo que el motor no puede convertir, uncolor()en un espacio que no conoce, no se trata como transparente:contrast-computablepregunta por el texto con el código de motivoBACKGROUND_UNPARSABLE, y las demás reglas de contraste lo dejan fuera.
Comprobaciones no implementadas: decisiones que no pueden automatizarse de forma segura
Estos no cuentan con una heurística comparablemente segura para el nivel de exigencia de este motor (fail debe seguir reservado exclusivamente para violaciones deterministas). Construirlas de todos modos capturaría casi nada (demasiado estrictas para ser útiles) o arriesgaría falsos positivos reales (demasiado amplias para confiar en ellas):
- "¿Es significativo este texto de encabezado o etiqueta?": los encabezados y etiquetas reales son enormemente variados y legítimamente cortos ("FAQ", "Nombre", "Resumen" son todos válidos), así que nada permite decidir a partir del marcado si un encabezado describe la sección que tiene debajo o si una etiqueta describe el campo que tiene al lado. Lo que sí se puede decidir es que algunas cadenas no pueden describir nada:
heading-qualityyform-control-label-qualitymarcan marcadores de posición olvidados, campos de plantilla numerados, nombres de archivo y URLs contra listas curadas de coincidencia exacta, el mismo compromiso de precisión sobre exhaustividad que hacelink-name-quality. Ambas son reglasmanuallimitadas acantTell: señalan un candidato para revisión, nunca afirman que el texto sea incorrecto. - "¿Describe este mensaje de error el problema?": qué dispara un error de validación y su contenido casi siempre dependen de JS o de una librería de validación, invisibles para un análisis estático de entrada; no es solo un problema de diseño de heurísticas.
- Subcomprobaciones detalladas de medios basados en tiempo (WCAG 1.2.x tiene 13 reglas ACT sin regla equivalente aquí; consulta la sección de carencias de
ACT_RULE_MAPPING.md): el propio contenido de audio/vídeo es fundamentalmente imposible de verificar a partir de marcado estático; los dos casos más amplios y seguros están cubiertos (media-alternative-transcript-evidence,video-caption), los más específicos no lo están, de forma deliberada. - Análisis del contenido de imágenes de texto (WCAG 1.4.5/1.4.9): si una imagen contiene texto está en sus píxeles, lo que requeriría una comprensión de imágenes equivalente a OCR.
image-of-textsolo pregunta donde el marcado muestra una señal: un<input type="image">, y un<img>que muestra una data URL SVG cuyo marcado contiene<text>. Una foto de una frase, un banner con el texto incrustado o una imagen de fondo CSS con texto no dan ninguna señal, y no se pregunta por ellos. El 1.4.9, que no admite excepciones, no tiene regla. - Controles activados por movimiento (WCAG 2.5.4): caso de nicho, baja incidencia real; no priorizado, no estructuralmente imposible.
- Los dos casos límite de ACT en
img-alt-decorative: un<img>cuyo estado actual de solicitud de red no está "completamente disponible" (aún cargando o roto), y un<canvas>totalmente transparente (sin nada realmente dibujado en él), están ambos exentos de la propia aplicabilidad de ACT. Ninguno de los dos se puede decidir a partir de un análisis estático del DOM (sin decodificación de imágenes, sin lectura de píxeles del canvas), así que ambos se dejan dentro del alcance en lugar de eximirse: un falso positivo poco frecuente que un revisor humano descarta de un vistazo, más seguro que subinformar en silencio un elemento realmente excluido o no decorativo.
Qué significa esto en la práctica
Casi nada de lo anterior es exclusivo de surea11y. Cualquier herramienta que juzga un único estado del DOM comparte los límites de Fuera de lo que un análisis puede ver, y la mayoría de las herramientas automatizadas también dejan las decisiones de criterio a una persona, aunque difieren en cómo se ejecutan, qué comprueban y cómo informan de lo que no pueden decidir. La razón para exponerlo explícitamente aquí: un pass de este motor (o de cualquier herramienta automatizada) nunca sustituye la revisión manual que el propio WCAG exige para los criterios anteriores. Consulta WCAG_CONFORMANCE.md para saber exactamente qué afirma y qué no afirma el pass de una regla, y por qué un criterio de WCAG cuyas reglas se cumplen todas sigue en cantTell.