Logo de Area 73

e2e: tests end-to-end donde tú decides cuánta IA entra


testingai-codingartificial-intelligencebest-practices

La idea en una frase

No hay que elegir entre tests deterministas y tests con IA: se elige cuánta IA lleva cada paso, y el modelo se paga una vez.

Es la propuesta de e2e, de TesterArmy: un framework de testing end-to-end para web y móvil (TypeScript, Apache-2.0) en el que describes un objetivo en lenguaje natural y un agente maneja la app para alcanzarlo, y luego compruebas el resultado con locators y assertions en el mismo test. Todo lo que sigue sale de su README y de su documentación; no lo he probado.

Tres tipos de paso, un solo test

Un test de e2e combina tres tipos de paso, y solo dos llaman al modelo:

Paso API ¿Llama al modelo?
Goal agent.act Sí, salvo que el caché lo reproduzca
Assertion agent.assert, agent.waitFor, agent.extract Sí
Locator screen, expect No
Los tres tipos de paso de e2e, de más agéntico a más determinista: goal con agent.act, que llama al modelo salvo replay; assertion con agent.assert, que llama al modelo; y locator con screen y expect, que no lo llamamás agénticomás deterministaGoalAssertionLocatormodelo, salvo replaymodelosin modelolos tres caben en el mismo testagent.actagent.assertscreen · expect

El ejemplo de su README:

test('a member upgrades to Pro', async ({ app, agent, screen }) => {
await app.open('/settings/billing');
await agent.act('upgrade the workspace to the Pro plan');
await agent.assert('the invoice preview shows a prorated amount');
await expect(screen.getByRole('status')).toContainText('Pro');
});

Concepto: la primera línea es un agente, la última es el testing de siempre, determinista y gratis. La IA solo entra donde la pones tú.

Acciones:

  • Usa agent.act para los flujos (qué hay que conseguir) y un locator para el resultado exacto (qué debe quedar en pantalla).
  • Locators en este orden de preferencia: rol y nombre, luego label, y el test id en último lugar.
  • Para valores que cambian en cada ejecución (un email con timestamp), unique(): así el paso se puede reproducir.

El replay cache: pagar el modelo una vez

Es lo que resuelve el problema económico del testing con IA. Un paso de agente que una comprobación posterior verifica queda grabado, y la siguiente ejecución lo reproduce sin llamar al modelo hasta que la app cambia. El resumen de cada ejecución lo cuenta:

AI 4.1k tokens · 2 model calls · anthropic/claude-sonnet-4.5
Cache 4 replayed · 1 handed off · 1 missed
Ciclo del replay cache: en la primera ejecución agent.act llama al modelo, un check posterior pasa y se graban las acciones y su efecto. En las siguientes se repiten las acciones y se comprueba el efecto; si se ve, no hay llamadas al modelo; si no, el agente sigue desde la pantalla actual1.ª ejecuciónsiguientes ejecucionesun check pasaexpect · assert · waitForse grabaacciones + efectoreplayrepite las acciones¿se ve el efecto?ruta + controlessí: 0 llamadasno: el agentesigue desde ahíagent.actllama al modelo

No es «grabar y rezar». Al reproducir, el runner:

  • Busca cada control grabado por rol, nombre, test id y contexto, y se para si la coincidencia es ambigua.
  • Comprueba la ruta final y lo que el paso cambió: qué controles aparecieron (con su estado: marcado, expandido…) y cuáles desaparecieron.
  • Exige que al menos un cambio ocurra durante el replay: un resultado que ya estaba en pantalla antes de las acciones que deberían producirlo no prueba nada.
  • Si algo no cuadra, no falla: degrada. El agente sigue desde la pantalla actual con el registro de lo que se ejecutó y por qué se paró (el handed off del resumen).

Detalles que demuestran que lo han pensado:

  • Ids, tokens y timestamps en la URL se leen como placeholder: /orders/42?t=1727780000 y /orders/7?t=1727780999 son la misma ruta. ?mode=safe y ?mode=unsafe, no.
  • El texto que cambia en cada ejecución (fechas, duraciones, ids) no se usa para comparar.
  • Solo se graba si pasa una verificación posterior: un locator matcher, agent.assert o agent.waitFor. Un expect(value).toBe() sobre un valor suelto o un agent.extract no cuentan.

Acción: pon siempre un check del resultado justo después de cada agent.act. Sin él, el paso nunca se graba y pagas el modelo en cada ejecución de CI.

Fallar no es lo mismo que no saber

agent.assert distingue dos resultados que casi ninguna herramienta separa:

  • ASSERTION_FAILED: la afirmación es falsa.
  • ASSERTION_INCONCLUSIVE: no hay evidencia suficiente en pantalla (el dato está en otra página, aún carga o solo está en los píxeles).

Un test que no puede ver el resultado no debería decir que la app está rota.

Las otras dos assertions:

  • agent.waitFor espera una condición y solo llama al modelo cuando la pantalla cambia.
  • agent.extract lee datos de la pantalla a un tipo (Zod, o cualquier Standard Schema), que luego compruebas con un expect normal.

Con vision: true el juicio incluye una captura enmascarada. Avisan de que las imágenes añaden tokens, y tras rellenar un secreto no se envían más capturas en ese paso.

Modelos de decisión en vez de un chat

El paquete @e2e-dev/decision ejecuta agent.act y agent.assert con un modelo de decisión en lugar de un LLM. Para cada acción responde a una sola pregunta: qué operación y sobre qué elemento, eligiendo entre las opciones que salen del árbol semántico de la pantalla. Nunca genera texto libre.

  • Cuando la operación es escribir, un modelo de texto pequeño redacta el valor. Lo que devuelve un modelo nunca se convierte en un selector, una URL ni una tecla, y los campos de contraseña no le llegan.
  • Sirve cualquier proveedor que responda preguntas de elección con distribuciones de probabilidad; su ejemplo usa typeSafeAi.decisionModel('jev-latest').
  • Dos umbrales opcionales, minProbability y minConfidence, bloquean acciones dudosas y convierten assertions débiles en inconclusas. Ellos mismos advierten de que el umbral es una decisión tuya: mídelo con tus tests antes de tocarlo.

Es la idea de Jev aplicada a un caso concreto: muchas decisiones pequeñas, tipadas y con probabilidad, en vez de un modelo que conversa.

Bug bashes: solo bugs con un test que falla

La parte más ambiciosa. Un bug bash lanza varias sesiones de exploración a la vez, cada una sobre un área de la app, y solo reporta como bug lo que un test de repro confirma.

Pipeline de un bug bash: el agente lee las rutas y el diff, escribe de 5 a 10 charters, lanza cuatro sesiones de explore a la vez, descarta los hallazgos con causa conocida y escribe un test de repro para el resto. Si el test falla, es un bug confirmado; si no, se descartarutas + diffde la rama5–10 chartersun área, una posturaexplore ×4cada uno, su personadescartarcausa conocidatest de reproafirma lo esperado¿falla?ASSERTION_FAILEDsínobug confirmadocon test y pasosfuerano se reporta

Concepto:

  1. El agente lee las rutas de la app y, en una rama, el diff.
  2. Escribe de 5 a 10 charters: un objetivo de una frase sobre un área, con una postura (usuario nuevo, números y copy, input de borde, estado tras recargar, caminos de error). Cada uno corre con su propia persona, cuatro a la vez.
  3. Descarta lo que tiene una causa conocida: el entorno local, el diseño previsto, los datos semilla o los límites del propio explorador (un enlace que abre otra pestaña, por ejemplo).
  4. Escribe un test de repro por hallazgo, que afirma el comportamiento esperado. El hallazgo es un bug solo si ese test falla.

Acciones:

  • Los tests de repro llevan el tag bugbash: sácalos del run que bloquea el merge con e2e run --exclude-tag bugbash.
  • Cuando se arregla el bug, el test pasa: quítale el tag y se queda como test de regresión.
  • El vídeo del explorador no se enmascara: si escribió una contraseña, revísalo antes de compartirlo.

Hecho para agentes de código

  • Servidor MCP con sesiones en vivo, varias a la vez, para que un agente compruebe locators contra la app.
  • Una skill (npx skills add tester-army/e2e) con temas como bug-bash.
  • La documentación entera viaja en el paquete npm, en node_modules/e2e/docs, para que los agentes la lean sin conexión.

Cautelas

  • Muy joven (creado en julio de 2026) y camino de la 1.0: los autores avisan de que las APIs y la configuración pueden cambiar entre versiones menores.
  • No aísla el código del test: corre con tus permisos. Para PRs no confiables hace falta un sandbox externo sin secretos ni tokens de escritura.
  • Envía telemetría anónima por defecto (comandos, motores, dónde fallan las ejecuciones; no contenido ni credenciales). Se desactiva con E2E_TELEMETRY_DISABLED=1.
  • Plataformas: web con Playwright (Chromium, Firefox, WebKit) y móvil con agent-device (simuladores de iOS y emuladores de Android), con la misma API. Agnóstico a la plataforma, no al lenguaje: es TypeScript.

Por dónde empezar

Hay guías de migración desde Playwright, Cypress, Selenium, Detox y Maestro, todas incrementales:

  • La de Playwright propone correr e2e al lado de Playwright y migrar fichero a fichero.
  • La de Cypress conserva tus atributos data-cy (web({ testIdAttribute: 'data-cy' })).

Lo razonable: elegir un flujo que hoy sea frágil (muchos selectores, cambia a menudo), reescribirlo con un agent.act y un check determinista detrás, y mirar en el resumen de CI cuántos pasos se reproducen gratis y cuántos acaban en el agente.

Mi opinión

Me quedo con tres cosas:

  1. El grado de IA se decide paso a paso. Es la forma correcta de plantearlo: el agente para llegar, el locator para comprobar. No hace falta apostar el test entero a un modelo.
  2. El replay cache es lo que lo hace viable en CI. Pagar el modelo solo cuando la app cambia, y degradar al agente en vez de fallar, convierte el coste de cada ejecución en el coste de cada cambio. Y la regla de que un efecto que ya estaba en pantalla no prueba nada es exactamente el tipo de detalle que separa una herramienta seria de una demo.
  3. INCONCLUSIVE y los bugs con repro son honestidad aplicada. Ni el test ni el bug bash reportan lo que no pueden demostrar. Es lo que más me gustaría ver en cualquier herramienta que use un modelo para juzgar.

Lo probaría con un flujo frágil y un ojo en la telemetría y en el aislamiento, pero no lo pondría todavía como base de toda la suite de un producto: la 1.0 aún no ha llegado.

Bibliografía: