Dominios, comandos y eventos: el mapa del protocolo
Cómo está organizado el protocolo, los dominios que más se usan, el patrón de activación previa, y las cuatro capacidades que no tienen equivalente en la interfaz.
El protocolo tiene decenas de dominios y cientos de métodos, y recorrer esa lista no es forma de aprenderlo. La estructura, en cambio, es muy regular: dominios que agrupan métodos por área funcional, un patrón de activación que hay que conocer para que nada funcione a la primera, y unos pocos dominios que cubren casi todo lo que se hace en la práctica. Con esa estructura y el monitor del panel, encontrar el comando que necesitas deja de ser un problema.
- Explicar la organización en dominios y el patrón de activación previa.
- Identificar los dominios que cubren la mayoría de los casos de uso.
- Usar los identificadores de objeto remoto para inspeccionar valores.
- Aplicar cuatro capacidades del protocolo sin equivalente directo en la interfaz.
La estructura
Un método del protocolo se nombra como dominio y operación separados por un punto. El dominio agrupa por área: uno para el árbol del documento, otro para la red, otro para la ejecución de código, otro para el depurador, y así.
El patrón de activación previa es la primera cosa que hay que saber, porque explica por qué nada funciona al principio. La mayoría de los dominios que emiten eventos requieren que se les active explícitamente antes de recibir nada. Sin esa activación, los comandos de consulta pueden funcionar y los eventos no llegan nunca. Es el error número uno de quien empieza.
La activación tiene además un coste: un dominio activado hace que el navegador registre y envíe información que de otro modo no produciría, y eso afecta al rendimiento de lo que estés midiendo. Activar el dominio de red mientras se graba un perfil de rendimiento cambia el perfil.
Los dominios que cubren casi todo
| Dominio | Para qué | Ejemplos de operación |
|---|---|---|
| Runtime | Ejecutar código y manejar objetos | Evaluar una expresión, llamar a una función sobre un objeto |
| Page | Ciclo de vida de la página | Navegar, recargar, capturar imagen, añadir script de arranque |
| DOM | El árbol del documento | Obtener el documento, consultar por selector, describir un nodo |
| CSS | Estilos | Estilos calculados, reglas que aplican, cobertura |
| Network | Peticiones | Activar, emular condiciones, obtener el cuerpo de una respuesta |
| Fetch | Interceptar peticiones | Pausar, modificar, cumplir con una respuesta propia |
| Debugger | Puntos de parada | Poner y quitar, pausar, evaluar en un marco |
| Emulation | Condiciones del dispositivo | Métricas del dispositivo, ralentización de CPU, características de medios |
| Performance | Métricas del navegador | Obtener contadores |
| Tracing | Trazas de bajo nivel | Iniciar y detener la captura |
| HeapProfiler | Memoria | Tomar una instantánea, forzar recolección |
| Target | Destinos y sesiones | Listar, adjuntar, crear |
De esa lista, los cuatro primeros y el de emulación cubren la práctica totalidad de la automatización habitual.
Un detalle que confunde al principio: los objetos de JavaScript no viajan por el protocolo. Cuando evalúas una expresión que devuelve un objeto, lo que recibes es un identificador de objeto remoto, y para inspeccionarlo hay que pedir sus propiedades con otro comando. Si lo que quieres es el valor serializado, hay que pedirlo explícitamente, y entonces solo funciona con valores que se puedan serializar.
Cuatro capacidades sin equivalente en la interfaz
Uno: ejecutar código antes que cualquier otra cosa de la página. Hay un comando que registra un script para que se ejecute en cada documento nuevo, antes de cualquier script de la página. Es la forma de instrumentar el arranque, de sustituir APIs antes de que nadie las use, o de fijar condiciones deterministas.
// Instrumentar el arranque de cualquier pagina antes de que ejecute nada
await cliente.enviar('Page.addScriptToEvaluateOnNewDocument', {
source: `
// Fijar el tiempo para hacer deterministas las pruebas
const AHORA = new Date('2026-08-06T09:00:00Z').getTime();
const FechaOriginal = Date;
globalThis.Date = class extends FechaOriginal {
constructor(...args) { return args.length ? new FechaOriginal(...args) : new FechaOriginal(AHORA); }
static now() { return AHORA; }
};
// Fijar el generador aleatorio
let semilla = 42;
Math.random = () => { semilla = (semilla * 1103515245 + 12345) % 2147483648; return semilla / 2147483648; };
// Registrar todos los errores desde el primer instante
globalThis.__errores = [];
addEventListener('error', e => globalThis.__errores.push({ tipo: 'error', mensaje: e.message, fuente: e.filename }));
addEventListener('unhandledrejection', e => globalThis.__errores.push({ tipo: 'promesa', mensaje: String(e.reason) }));
`
});
Fijar el tiempo y el generador aleatorio es lo que convierte una prueba visual inestable en una determinista, y es imposible de conseguir desde la propia página porque el código de la aplicación ya ha capturado las referencias originales.
Dos: interceptar y modificar peticiones. El dominio de interceptación permite pausar una petición antes de que salga, modificar su URL, sus cabeceras o su cuerpo, o responderla directamente sin que llegue a la red.
// Simular respuestas de API sin tocar el codigo ni levantar un servidor
await cliente.enviar('Fetch.enable', {
patterns: [{ urlPattern: '*/api/*', requestStage: 'Request' }]
});
cliente.al('Fetch.requestPaused', async ({ requestId, request }) => {
if (request.url.includes('/api/usuarios')) {
const cuerpo = JSON.stringify([{ id: 1, nombre: 'Prueba' }]);
await cliente.enviar('Fetch.fulfillRequest', {
requestId,
responseCode: 200,
responseHeaders: [{ name: 'Content-Type', value: 'application/json' }],
body: Buffer.from(cuerpo).toString('base64')
});
return;
}
if (request.url.includes('/api/lento')) {
await new Promise(r => setTimeout(r, 5000)); // simular latencia extrema
}
await cliente.enviar('Fetch.continueRequest', { requestId });
});
Esa capacidad convierte cualquier escenario de error de servidor en algo reproducible en un segundo: respuestas de error, respuestas malformadas, latencias extremas, respuestas vacías.
Tres: emular condiciones que no están en ningún menú. Ralentización de CPU con valores arbitrarios, métricas de dispositivo con cualquier combinación, características de medios concretas, zonas horarias, idiomas, geolocalización.
Cuatro: controlar la recolección de basura. Forzar una recolección desde código es lo que hace posible automatizar la detección de fugas.
// Deteccion automatizada de fugas: el ciclo de los tres snapshots, en CI
async function detectarFuga(cliente, ciclo, vueltas = 10) {
await cliente.enviar('HeapProfiler.enable');
const medirMonton = async () => {
await cliente.enviar('HeapProfiler.collectGarbage');
const { result } = await cliente.enviar('Runtime.evaluate', {
expression: 'performance.memory ? performance.memory.usedJSHeapSize : 0',
returnByValue: true
});
return result.value;
};
await ciclo(); // calentamiento: inicializa lo perezoso
const base = await medirMonton();
for (let i = 0; i < vueltas; i++) await ciclo();
const despues = await medirMonton();
const crecimientoMB = (despues - base) / 1048576;
const porCiclo = crecimientoMB / vueltas;
console.log('Crecimiento total:', crecimientoMB.toFixed(2), 'MB |',
'por ciclo:', (porCiclo * 1024).toFixed(1), 'KB');
return { crecimientoMB, porCicloKB: porCiclo * 1024 };
}
El ciclo de calentamiento antes de la medida base es lo que evita el falso positivo de la inicialización perezosa, exactamente por la misma razón que la técnica de las tres instantáneas necesita tres.
Encontrar el comando que necesitas
El método fiable no es leer la documentación entera sino este procedimiento de tres pasos.
Uno: haz la acción a mano en las DevTools con el monitor de protocolo abierto.
Dos: lee el comando que se envió, con sus parámetros exactos.
Tres: búscalo en la documentación del protocolo para conocer el resto de sus parámetros y su respuesta completa.
Ese recorrido convierte una búsqueda en un espacio enorme en una lectura dirigida, y es lo que hace viable trabajar con el protocolo sin memorizar nada.
De las cuatro capacidades de esta lección, la que más cambia la vida de un equipo es la primera combinada con la segunda, y merece una explicación de por qué. Las pruebas automatizadas de interfaz tienen mala fama merecida: son lentas, se rompen sin motivo aparente y acaban desactivadas en la mayoría de los proyectos. Pero si se analiza por qué fallan de forma intermitente, casi siempre se llega a las mismas tres fuentes de no determinismo. El tiempo: una prueba que compara una fecha mostrada, o que depende de un temporizador, o que compara una captura donde aparece la hora. El azar: cualquier identificador generado, cualquier orden que dependa de una barajada, cualquier prueba A/B. Y la red: latencias variables, respuestas que cambian, servicios de terceros que a veces fallan. Las tres se eliminan con las capacidades de esta lección: se fija la fecha y el generador aleatorio con un script de arranque, y se sustituye la red entera con interceptación. El resultado no es una mejora incremental: es la diferencia entre una batería de pruebas que la gente ignora y una en la que se confía. Y hay un beneficio adicional menos evidente. Cuando la red está interceptada, las pruebas dejan de necesitar un backend, lo que las hace órdenes de magnitud más rápidas, ejecutables en paralelo, y capaces de reproducir escenarios que con un servidor real serían dificilísimos de montar: un error de servidor, una respuesta con un campo nulo inesperado, una latencia de treinta segundos, una respuesta que llega después de que el usuario haya navegado. Esos son exactamente los casos donde las aplicaciones se rompen en producción, y son también los que ninguna prueba con un backend real cubre nunca porque nadie sabe cómo provocarlos. La conclusión es que el determinismo no es un detalle de la infraestructura de pruebas: es la condición que decide si esas pruebas van a existir dentro de un año, y el protocolo es lo que lo hace alcanzable.