RustPruebasE2EChromiumCDPAccesibilidad

Tu banco de pruebas E2E mide el códec, no el producto

Publicado el 2026-08-10 · Xiliux

Tu banco de pruebas de extremo a extremo está en verde. Arranca el binario, le pide una pantalla, y comprueba que el HTML trae lo que tiene que traer. Cien pruebas, cero rojas.

Y la pantalla está rota.

No es una historia inventada: es lo que pasa en cuanto una parte de la interfaz la termina de construir el navegador. Un fragmento correcto insertado en el sitio equivocado devuelve exactamente el mismo 200 con el mismo cuerpo. Un <script type="module"> bloqueado por la política de seguridad de contenido, servido con el MIME equivocado, o que revienta al importar: el mismo 200, con el mismo cuerpo. Un botón que confirma dos pulsaciones y que en realidad actúa a la primera —porque su oyente se registró después que el de la librería— responde igual que uno sano.

Lo que se está midiendo ahí es el códec, no el producto. Se comprueba que el servidor sabe serializar HTML. Sobre si eso se convierte en una pantalla usable, el banco no dice nada, y no dice nada en verde, que es la peor forma de no decir nada.

El truco de la cookie

La respuesta obvia es dibujar la pantalla en un navegador de verdad. Y ahí aparece la pared: casi todo lo interesante está detrás de un acceso.

chromium --dump-dom no admite mandar una cookie. No existe --header. Así que la tentación es abrir una ruta que autentique por GET, solo para las pruebas —un agujero real a cambio de una prueba— o inyectar la cookie por la puerta de atrás.

Hay una tercera vía, y es mejor que las dos: entrar por el formulario, como entra una persona. Se rellenan los campos, se envía, y se espera a salir de la pantalla de acceso. Cuesta lo mismo, y de regalo el acceso queda medido: una cookie mal marcada —Secure en una instalación que corre por HTTP en localhost, un SameSite equivocado— se ve en la prueba en vez de en la máquina del cliente.

Para eso hay que hablar con el navegador, no solo lanzarlo.

Una tubería, no un puerto

El protocolo de DevTools de Chrome se suele usar por WebSocket: se arranca el navegador con --remote-debugging-port, se descubre el puerto, se espera a que escuche, se abre un WebSocket. Tres cosas que pueden fallar y una dependencia nueva en el árbol.

--remote-debugging-pipe hace lo mismo por los descriptores 3 y 4 del propio proceso. Nada que descubrir, nada a lo que esperar, y —esto importa más de lo que parece— dos pruebas en paralelo no se pisan, que es como tu runner de pruebas las ejecuta por defecto. El protocolo es JSON separado por bytes nulos. Se lee de un descriptor y se escribe en el otro.

En Rust, poner los descriptores en su sitio es un dup2 entre el fork y el exec:

cmd.pre_exec(move || {
    if libc::dup2(r_nav, 3) < 0 || libc::dup2(w_nav, 4) < 0 {
        return Err(std::io::Error::last_os_error());
    }
    Ok(())
});

El detalle que cuesta una tarde

Ese fragmento tiene una trampa que no da error.

El hijo necesita sus extremos de las tuberías en los descriptores 3 y 4. Si los tuyos siguen ocupando descriptores bajos cuando llega el dup2, se machacan. El resultado no es un fallo: es un arnés que se lee a sí mismo. Mandas un comando y esperas una respuesta que no llega nunca, hasta que salta el techo de la prueba con un mensaje que no dice nada útil.

La solución son dos líneas, y solo se encuentran sabiendo lo que se busca: apartar los propios extremos a descriptores altos con un dup antes de tocar el 3 y el 4.

Y una segunda, del mismo tipo: cerrar en el padre los extremos del hijo justo después de arrancarlo. Si se quedan abiertos, la tubería no da fin de fichero cuando el navegador muere — y una lectura se cuelga hasta el techo en vez de decirte que el navegador se murió.

Lo que el navegador sabe y tu HTML no

Una vez tienes el canal, aparece algo que no esperabas: el navegador te presta lo que ya calcula para sí mismo.

El árbol de accesibilidad, por ejemplo. Es tentador auditar accesibilidad con un grep sobre las plantillas: contar aria-label, comprobar que cada campo tenga su <label>. Eso mide la fuente, no lo que llega. Un <label for="x"> cuyo id no existe se ve perfecto en el código y no nombra nada. Un aria-label vacío tapa al nombre que sí había. Un control que la CSP dejó sin dibujar no está.

Calcular el «nombre accesible» a mano es un algoritmo largo, con reglas distintas por papel, y una versión propia se equivoca justo donde importa: diciendo que hay nombre donde el lector de pantalla no lo encuentra. Chrome ya lo calcula. Se pide y ya está.

Falla ruidosamente

Un último detalle, y es el que separa un banco que sirve de uno decorativo.

Si Chromium no está instalado, la tentación es saltarse la prueba. No lo hagas. Un banco que se salta solo es indistinguible de uno que pasa, y ése es exactamente el verde que todo esto existe para no producir. Si no hay navegador, que entre en pánico y lo diga.

Lo mismo con el plazo: si el navegador se cuelga, mátalo y distingue «murió por plazo» de «salió con error». No son lo mismo, y confundirlos hace que un cuelgue se lea como un fallo de tu pantalla.

La librería

Todo esto está empaquetado en mirilla, en Rust, con dos dependencias: libc —por pipe, dup y dup2— y serde_json, porque el protocolo es JSON. Nada más. Un arnés de pruebas que arrastra un árbol de paquetes es un arnés que hay que auditar.

let mut nav = Navegador::abrir(&buscar_chromium());
nav.entrar("http://127.0.0.1:8080", "/entrar", "ada@x.co", "su-clave");
nav.ir("http://127.0.0.1:8080/panel");

assert_eq!(nav.texto("location.pathname"), "/panel");

No nació como librería. Vivía dentro de un producto, se copió a un segundo sin generalizar —a propósito: extraer una pieza compartida antes de que exista el segundo consumidor es adivinar la costura— y solo entonces se midió el código de las dos copias. De unas trescientas líneas, la única diferencia real era la ruta del formulario de acceso. Por eso es hoy su único parámetro, y no hay ninguna otra abstracción: lo demás todavía no se ha repetido.

Entra como dependencia de desarrollo, así que no toca el binario que se entrega.

Preguntas frecuentes

¿No basta con comprobar el HTML que devuelve el servidor?

Solo si el navegador no añade nada. En cuanto hay intercambios parciales o módulos de JavaScript, un fragmento correcto insertado en el sitio equivocado devuelve el mismo 200 con el mismo cuerpo — y un módulo bloqueado por la política de seguridad de contenido, también. Eso comprueba que el servidor sabe serializar HTML, no que la pantalla funcione.

¿Por qué una tubería y no el WebSocket de siempre?

`--remote-debugging-port` obliga a descubrir el puerto, esperar a que escuche y abrir un WebSocket —tres cosas que pueden fallar y una dependencia más—. `--remote-debugging-pipe` habla por los descriptores 3 y 4 del proceso: nada que descubrir, nada a lo que esperar, y dos pruebas en paralelo no se pisan.

¿Por qué entrar por el formulario en vez de inyectar la cookie?

Porque inyectarla mide una pantalla y salta el acceso. Entrando mide las dos cosas, y una cookie mal marcada —`Secure` en una instalación que corre por HTTP en localhost, un `SameSite` equivocado— se ve en la prueba en vez de en la máquina del cliente. La alternativa, abrir una ruta que autentique por GET solo para las pruebas, es un agujero real a cambio de una prueba.

¿Qué pasa si el navegador no está instalado?

Entra en pánico y lo dice. Saltarse la prueba sería peor que no tenerla: un banco que se salta solo es indistinguible de uno que pasa, y ése es exactamente el verde que esto existe para no producir.

¿Sirve para auditar accesibilidad?

Sí, y mejor que un grep sobre las plantillas. Devuelve el árbol de accesibilidad que calcula el navegador, con el nombre ya resuelto. Un `<label for="x">` cuyo `id` no existe se ve perfecto en la fuente y no nombra nada; ahí sale como lo que es.

← Ver más artículosCotizar un proyecto