
En el post anterior contamos cómo usamos Playwright para automatizar capturas de pantalla en Ofusca. Pero hay un proyecto donde Playwright no es una herramienta auxiliar, sino el motor completo. En JMO Labs, nuestra plataforma de testing web, Playwright lanza navegadores, ejecuta 9 comprobaciones de calidad en fases, graba vídeo de cada test y hasta se autocorrige con IA cuando un selector falla. En el mismo contexto, en variables de entorno en scripts E2E cubrimos el otro lado del problema.
Este artículo es un recorrido técnico por cómo construimos un sistema de testing completo sobre la API de Playwright, desde la arquitectura del orquestador hasta el localizador de 9 estrategias que hace que los tests E2E sean resistentes a cambios en la interfaz. Hablamos de esto con más detalle en el diseño de APIs para agentes de IA.
JMO Labs es una plataforma fullstack de testing y análisis de calidad web. Ofrece tres modos:
Todo corre en un único contenedor Docker con Chromium preinstalado, un backend Express y un frontend React. Y en el centro de todo, Playwright.
El problema de ejecutar múltiples comprobaciones sobre una misma página es que unas pueden interferir con otras. Un check de responsive cambia el viewport. Un check de interactividad hace clic en botones. Si todo corre en paralelo, los resultados son impredecibles.
La solución es un orquestador que ejecuta las comprobaciones en 9 fases ordenadas por nivel de intrusividad:
// Fases de ejecución — de solo lectura a mutación del DOM
const phases = [
{ name: "render", parallel: false }, // Fase 1: captura inicial
{ name: "console-errors", parallel: false }, // Fase 2: re-navega para capturar consola
{ group: ["performance", "security", "seo"], parallel: true }, // Fase 3: solo lectura
{ name: "accessibility", parallel: false }, // Fase 4: inyecta axe-core
{ name: "responsive", parallel: false }, // Fase 5: cambia viewport
{ name: "links", parallel: false }, // Fase 6: peticiones HTTP externas
{ name: "interactive", parallel: false }, // Fase 7: clics, formularios
{ name: "web-vitals", parallel: false }, // Fase 8: PerformanceObserver
{ name: "lighthouse", parallel: false }, // Fase 9: auditoría completa (CDP)
];Las tres primeras fases de solo lectura (rendimiento, seguridad, SEO) se ejecutan en paralelo porque no mutan el DOM. A partir de la fase 4, cada comprobación corre en solitario para evitar interferencias.
El orquestador también gestiona la concurrencia entre tests: un máximo configurable de navegadores simultáneos (por defecto 3) evita saturar la memoria del servidor.
Si el mismo usuario lanza el mismo test con las mismas comprobaciones seleccionadas, el orquestador devuelve los resultados cacheados sin abrir un navegador. La clave de caché es un hash de la URL + los checks seleccionados, con un TTL configurable.
function cacheKey(url, checks) {
const normalized = checks.slice().sort().join(",");
return `${url}::${normalized}`;
}
// Si existe resultado reciente, lo retransmitimos por SSE sin ejecutar nada
const cached = getFromCache(cacheKey(url, selectedChecks));
if (cached && !options.forceRerun) {
emitCachedResults(cached, stream);
return;
}El modo E2E es donde Playwright y la inteligencia artificial se combinan. El flujo tiene tres fases:
Por ejemplo, una especificación como “Navega al login, introduce el email test@example.com y la contraseña 1234, haz clic en Entrar y verifica que aparece el dashboard” se convierte en un plan ejecutable:
{
"steps": [
{ "action": "navigate", "value": "/login" },
{ "action": "fill", "selector": "input[type=email]", "value": "test@example.com" },
{ "action": "fill", "selector": "input[type=password]", "value": "1234" },
{ "action": "click", "selector": "button:Entrar" },
{ "action": "wait", "value": 2000 },
{ "action": "screenshot_only" }
]
}El mayor desafío de los tests E2E automatizados es la fragilidad de los selectores. Un cambio de clase CSS, un texto traducido o un atributo renombrado rompen el test entero. Nuestra solución: un localizador que intenta 9 estrategias en cascada antes de dar un elemento por no encontrado.
async function smartLocator(page, selector) {
// 1. CSS directo
let el = page.locator(selector);
if (await el.count()) return el;
// 2. role:name (ej: "button:Enviar")
if (selector.includes(":")) {
const [role, name] = selector.split(":");
el = page.getByRole(role, { name });
if (await el.count()) return el;
}
// 3. getByLabel (campos de formulario)
el = page.getByLabel(selector);
if (await el.count()) return el;
// 4. getByPlaceholder (inputs)
el = page.getByPlaceholder(selector);
if (await el.count()) return el;
// 5. getByRole con roles comunes
for (const role of ["button", "link", "textbox", "heading"]) {
el = page.getByRole(role, { name: selector });
if (await el.count()) return el;
}
// 6. getByText (texto visible)
el = page.getByText(selector);
if (await el.count()) return el;
// 7. aria-label parcial
el = page.locator(`[aria-label*="${selector}"]`);
if (await el.count()) return el;
// 8. data-testid parcial
el = page.locator(`[data-testid*="${selector}"]`);
if (await el.count()) return el;
// 9. title parcial
el = page.locator(`[title*="${selector}"]`);
if (await el.count()) return el;
return null; // las 9 estrategias fallaron
}Pero aquí no acaba. Si las 9 estrategias fallan, el sistema entra en modo self-healing:
El resultado: tests que se reparan solos cuando la interfaz cambia. La tasa de éxito sube con cada ejecución porque la caché de selectores aprende qué funciona.
El check de responsive no usa tamaños inventados. Los viewports están calibrados con dispositivos reales de 2025:
const defaultViewports = [
{ name: "mobile", width: 402, height: 874, label: "iPhone 17" },
{ name: "tablet", width: 820, height: 1180, label: "iPad Air 11\" M3" },
{ name: "desktop", width: 1440, height: 932, label: "MacBook Air 15\" M4" },
];Para cada viewport, Playwright:
page.setViewportSize().La comparación visual es especialmente útil en desarrollo continuo: cada cambio de CSS se valida automáticamente contra el baseline aprobado.
El check de accesibilidad aprovecha que Playwright tiene control total del contexto del navegador para inyectar axe-core directamente en la página bajo prueba:
async function runAccessibilityCheck(page) {
// Inyecta axe-core en la página
const axePath = require.resolve("axe-core/axe.min.js");
const axeScript = readFileSync(axePath, "utf-8");
await page.evaluate(axeScript);
// Ejecuta el análisis
const results = await page.evaluate(() => axe.run());
// Agrupa violaciones por impacto
const violations = results.violations.map((v) => ({
impact: v.impact, // critical | serious | moderate | minor
description: v.description,
nodes: v.nodes.length,
help: v.helpUrl,
}));
return {
status: violations.some((v) => v.impact === "critical") ? "fail" : "pass",
violations,
passes: results.passes.length,
};
}El criterio es estricto. Cualquier violación crítica marca el check como fallido. Las violaciones moderadas y menores se reportan como advertencias para que el equipo las priorice.
Cada test en JMO Labs se graba en vídeo. Playwright soporta grabación nativa de WebM que activamos al crear el contexto del navegador:
const context = await browser.newContext({
recordVideo: { dir: videoTmpDir, size: viewport },
viewport,
});
const page = await context.newPage();
// ... ejecutar comprobaciones ...
// Finalizar grabación
await context.close();
const videoPath = await page.video().path();Además, durante la ejecución enviamos capturas en tiempo real cada 500 ms por Server-Sent Events (SSE). El frontend las muestra como una vista en directo del navegador que está ejecutando el test. Es como ver a Playwright trabajar en tiempo real.
Todo encaja en un flujo que va desde la petición del usuario hasta el informe PDF:
POST /api/test.
Y el historial de tests ejecutados, con filtrado por URL, modo, duración y número de checks superados:
Después de construir una plataforma de testing completa sobre Playwright, estas son las lecciones más valiosas:
Playwright es mucho más que un framework de testing E2E. Es una API de automatización del navegador lo suficientemente potente como para construir productos completos sobre ella. JMO Labs es la prueba de que, con la arquitectura adecuada, puedes convertir un navegador headless en una plataforma de análisis de calidad web.
Si quieres probar JMO Labs, está disponible en e2e.josemanuelortega.dev. Lanza un Quick Scan contra cualquier URL y verás a Playwright en acción.

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.

Los tests E2E se rompen con cada cambio de interfaz. En JMO Labs construimos un pipeline de 5 fases con IA que planifica, ejecuta, repara selectores, diagnostica fallos y verifica resultados de forma autónoma. La caché de selectores hace que cada ejecución sea más rápida que la anterior.