
Los tests E2E tienen un enemigo silencioso: la fragilidad de los selectores. Cambias una clase CSS, traduces un botón o mueves un campo de formulario y la suite entera se rompe. En JMO Labs decidimos que los tests deberían repararse solos. Este artículo explica cómo construimos un pipeline de self-healing con IA que planifica, ejecuta, diagnostica, repara y verifica tests E2E de forma autónoma. Hablamos de esto con más detalle en el diseño de APIs para agentes de IA.
Si no conoces JMO Labs, en el post anterior explicamos su arquitectura general. Hoy nos centramos en la parte más ambiciosa: el sistema de autocuración.
El flujo tiene cinco fases encadenadas, cada una con su propio modelo de IA:
Veamos cada fase en detalle.
El usuario escribe algo como: “Navega al login, introduce test@example.com y contraseña 1234, haz clic en Entrar y verifica que aparece el dashboard”. El planificador convierte esto en un JSON con pasos concretos.
Pero antes de planificar, el sistema extrae el contexto real de la página: encabezados, formularios con sus campos (label, id, placeholder), y los primeros 50 elementos interactivos con sus atributos (role, aria-label, data-testid, texto visible). Este contexto se envía al modelo junto con la especificación para que los selectores sean lo más precisos posible.
// El prompt incluye el contexto real de la página
const userMessage = `URL: ${url}
Especificación funcional:
${spec}
Contexto de la página:
- Título: ${pageContext.title}
- Formularios: ${JSON.stringify(pageContext.forms)}
- Elementos interactivos: ${JSON.stringify(pageContext.elements.slice(0, 50))}
Genera un plan JSON con esta estructura:
{
"name": "Nombre breve del test",
"steps": [
{ "id": 1, "action": "navigate", "selector": "...", "value": "..." }
]
}`;El plan se valida estructuralmente antes de ejecutarse: cada paso debe tener una acción válida (navigate, click, type, select, scroll, wait, screenshot_only) y los campos requeridos según la acción. Si la validación falla, el planificador reintenta una vez.
El prompt instruye al modelo a preferir selectores por este orden:
role:name, por ejemplo button:Enviar, link:Inicio (el más resiliente)aria-label, como [aria-label="Search"]data-testid, como [data-testid="submit-btn"]id, como #login-form.btn-primary (último recurso, el más frágil)Esta jerarquía hace que los tests generados sean naturalmente resistentes a cambios de diseño. Un cambio de clase CSS no rompe un selector basado en el role del elemento.
Cada paso del plan se ejecuta buscando el elemento objetivo con 9 estrategias en orden de especificidad. Si la primera no encuentra el elemento, prueba la siguiente. Y si ninguna funciona en el primer intento, reintenta 3 veces con timeouts crecientes (3s, 6s, 9s).
async function smartLocator(page, step, attempt = 0) {
const timeout = 3000 + (attempt * 3000); // 3s → 6s → 9s
const strategies = [
// 1. CSS directo
() => page.locator(selector).first(),
// 2. role:name (ej: "button:Enviar")
() => page.getByRole(role, { name }),
// 3. getByLabel (campos de formulario)
() => page.getByLabel(selector, { exact: false }).first(),
// 4. getByPlaceholder (inputs)
() => page.getByPlaceholder(selector, { exact: false }).first(),
// 5. getByRole con roles comunes
() => page.getByRole("button", { name: selector }),
// 6. getByText (texto visible)
() => page.getByText(selector, { exact: false }).first(),
// 7. aria-label parcial
() => page.locator(`[aria-label*="${selector}" i]`).first(),
// 8. data-testid parcial
() => page.locator(`[data-testid*="${selector}" i]`).first(),
// 9. title parcial
() => page.locator(`[title*="${selector}" i]`).first(),
];
for (const strategy of strategies) {
const el = await strategy();
if (await el.isVisible({ timeout })) return el;
}
return null;
}Entre reintentos, el sistema hace dos cosas inteligentes:
Cada vez que un selector funciona, el sistema lo guarda en una caché persistente en SQLite asociada al patrón de URL. La próxima vez que se ejecute un test contra esa URL, el localizador consulta la caché antes de probar las 9 estrategias.
// Antes de probar estrategias, consulta la caché
const cached = getCachedSelector(urlPattern, step.selector);
if (cached && cached.success_count > cached.fail_count) {
const el = page.locator(cached.working_selector).first();
if (await el.isVisible({ timeout: 3000 })) {
incrementSelectorSuccess(urlPattern, step.selector);
return el; // Encontrado en caché
}
}
// Si la caché falla, prueba las 9 estrategias...
// Si tiene éxito, guarda en caché:
upsertSelector(urlPattern, original, working, description, strategy);La caché usa un sistema de contadores de éxito y fallo. Solo se usa un selector cacheado si success_count > fail_count. Si un selector cacheado deja de funcionar, su contador de fallos sube y eventualmente se descarta. Los selectores no usados en 90 días se eliminan automáticamente.
El resultado: los tests se aceleran con cada ejecución porque la caché evita recorrer las 9 estrategias para elementos ya conocidos.
Cuando las 9 estrategias fallan en los 3 intentos y la caché no ayuda, entra el healer. Este módulo toma una captura de pantalla de la página actual y la envía a un modelo de IA con capacidad de visión junto con el selector que falló.
const result = await provider.askWithImage(
HEALER_SYSTEM, // Instrucciones: "Analiza el screenshot y sugiere selectores"
HEALER_USER(step), // "El selector X no funciona para la acción Y"
screenshotBase64 // Captura actual de la página
);
// El modelo responde con:
// { selector: "nuevo CSS", fallbacks: ["alt1", "alt2"], reasoning: "..." }
// Prueba el selector sugerido
const locator = page.locator(result.selector);
if (await locator.count() > 0) {
// Funciona → guardar en caché con strategy: "ai-healed"
upsertSelector(urlPattern, original, result.selector, description, "ai-healed");
return locator;
}El healer también proporciona selectores de fallback que se prueban si el principal no funciona. Si alguno de los selectores sugeridos encuentra el elemento, se guarda en la caché con la estrategia ai-healed para futuras ejecuciones.
A veces el problema no es el selector, sino el estado de la página. Un modal bloqueando la interfaz, una redirección inesperada al login, un error 500 o contenido que no ha cargado. Para estos casos, el sistema de recovery analiza la captura y el error para diagnosticar qué pasó y proponer una acción correctiva.
const recovery = await attemptRecovery(
page, provider, failedStep, errorMessage, screenshotBase64
);
// El modelo analiza y responde con:
// {
// diagnosis: "Un banner de cookies está bloqueando el botón",
// recoveryAction: { action: "dismiss", selector: "#cookie-accept" },
// shouldRetryOriginal: true,
// shouldSkipStep: false
// }
// Ejecuta la acción de recuperación
if (recovery.recoveryAction) {
await executeAction(page, recovery.recoveryAction);
}
// Reintenta el paso original si el modelo lo sugiere
if (recovery.shouldRetryOriginal) {
await executeAction(page, failedStep);
}Las acciones de recovery disponibles son: dismiss (cerrar obstáculo), scroll, wait, click (hacer clic en otro elemento primero) y navigate. Si el modelo determina que el estado es irrecuperable (página de error, login wall), recomienda saltar el paso en vez de fallar el test entero.
Después de ejecutar cada paso (con o sin healing/recovery), el verificador toma una captura de pantalla y la analiza con IA de visión para confirmar que el resultado es el esperado.
const verification = await verifyStep(
provider, step, screenshotBase64,
{
urlChanged: currentUrl !== previousUrl,
consoleErrors: errorsThisStep,
networkErrors: failuresThisStep,
}
);
// Respuesta: { status: "pass" | "fail" | "warn", explanation: "..." }
// "pass" → el resultado esperado es visible
// "fail" → el resultado NO está presente
// "warn" → no se puede determinar con certezaEl verificador recibe contexto adicional: si la URL cambió entre pasos, si hubo errores de consola o fallos de red durante la ejecución. Esto le permite distinguir entre cambios esperados (navegación) e inesperados (redirección de error).
Antes de que el planificador analice la página y durante los reintentos del ejecutor, un módulo especializado descarta automáticamente banners de cookies, modales y popups. Conoce 28 patrones de consentimiento de cookies (OneTrust, Cookiebot, Didomi, Tarteaucitron...) y textos de aceptación en 7 idiomas.
// Ejemplo de los 28 selectores conocidos (extracto)
const COOKIE_SELECTORS = [
"#onetrust-accept-btn-handler",
"#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
"#didomi-notice-agree-button",
".cc-accept-all",
"[data-cookie-accept]",
// ... 23 más
];
// Si ningún selector funciona, prueba textos en 7 idiomas:
// "Aceptar", "Accept", "Accepter", "Akzeptieren", "Aceitar", "Accetta"...Si todo lo anterior falla, pulsa Escape como último recurso. El sistema ejecuta hasta 3 rondas de descarte para manejar popups apilados.
Todo el pipeline emite eventos por Server-Sent Events (SSE) que el frontend consume en tiempo real. El usuario ve cada paso ejecutarse, cada healing aplicarse y cada verificación resolverse.
// Eventos emitidos durante la ejecución:
emit("step_start", { stepId, description, action });
emit("selector_healed", { stepId, originalSelector, usedSelector, strategy });
emit("step_recovery", { stepId, diagnosis, action, retried, skipped });
emit("step_complete", { stepId, status, explanation, screenshot });
emit("live_frame", { filename, frame, timestamp });
emit("ai_complete", { report, steps, usage });Además, durante toda la ejecución E2E se capturan screenshots en tiempo real cada 500 ms que se envían como frames por SSE. El frontend los muestra como un vídeo en directo del navegador headless. Es como tener una ventana abierta a lo que Playwright está haciendo en ese momento.
El pipeline completo crea un ciclo de mejora continua:
Cada ejecución deja la caché más rica. Los tests de hoy son más rápidos y resistentes que los de ayer porque el sistema recuerda qué selectores funcionan en cada dominio.
La fragilidad de los tests E2E no es un problema técnico, es un problema de diseño. Si tu sistema asume que los selectores siempre funcionarán, cualquier cambio en la interfaz lo rompe. Si asume que fallarán y tiene mecanismos para adaptarse, se vuelve resiliente. Construye tests que esperen el caos.
Si quieres probar el sistema de self-healing en acción, lanza un test E2E en e2e.josemanuelortega.dev con cualquier especificación en lenguaje natural. Verás a la IA planificar, ejecutar y verificar cada paso en tiempo real.

Ejecutar los mismos tests una y otra vez deja de encontrar defectos nuevos. Así funciona la paradoja del pesticida y estas son las estrategias para combatirla.
Los scripts E2E necesitan datos sensibles —tokens de API, credenciales, URLs privadas— sin que aparezcan en el código. En JMO Labs hemos añadido variables de script con modo privado: se inyectan automáticamente, se enmascaran en los logs y se acceden con una sintaxis limpia.

Playwright no es solo para tests E2E. En JMO Labs lo usamos como motor completo: 9 fases de comprobación, localizador de 9 estrategias con self-healing, grabación de vídeo, testing responsive con viewports reales y accesibilidad con axe-core.