Las herramientas clásicas de automatización web (Cypress, Selenium heredado) históricamente interceptaban la red inyectando scripts de JavaScript en el contexto del documento mediante monkey-patching sobre las instancias globales de window.fetch y XMLHttpRequest. Este enfoque en el runtime de la página adolece de limitaciones arquitectónicas insalvables:
window.fetch no puede interceptar peticiones disparadas por Service Workers, Web Workers, etiquetas HTML <img src="...">, <link rel="stylesheet">, recursos de tipografía @font-face o llamadas de telemetría con navigator.sendBeacon.ECONNRESET), timeouts de conexión TCP, fallos de resolución DNS o cortes de red a nivel de sistema operativo.+-----------------------------------------------------------------------------+
| Enfoque heredado: Monkey Patching en el JavaScript del DOM |
| |
| DOM Window -> [window.fetch Override] -> Retorna Promise.resolve() |
| (Service Workers, Fuentes, Etiquetas img, Beacons — NO SE INTERCEPTAN) |
+-----------------------------------------------------------------------------+
+-----------------------------------------------------------------------------+
| Playwright 2026: Intercepción de red a nivel de motor |
| |
| DOM / Worker / Service Worker / Tags |
| │ |
| ▼ |
| [Socket nativo del navegador (Chromium Network Stack / WebKit LibWebSockets)]
| │ |
| ▼ (Evento CDP: Fetch.requestPaused / WebSocket Frame) |
| [Interceptores de ruta de Playwright: page.route() / context.route()] |
| │ |
| ├──► route.fulfill() ──► Inyecta respuesta HTTP sintética al instante|
| ├──► route.abort() ──► Corta el socket físico (Connection Reset) |
| ├──► route.continue() ──► Reenvía la petición a la red externa física |
| └──► route.fetch() ──► Ejecuta petición real con mutación previa |
+-----------------------------------------------------------------------------+Playwright opera directamente al nivel del stack de red del núcleo del navegador a través del Chrome DevTools Protocol (dominio Fetch) en Chromium y mediante hooks equivalentes en Firefox y WebKit. La petición se suspende antes de salir a la red física, garantizando una cobertura total sobre cualquier tipo de tráfico.
Al invocar page.route() o context.route(), Playwright registra un filtro en la canalización de red.
+--------------------------------+
| El navegador inicia una |
| petición GET /api/v1/user |
+--------------------------------+
│
▼
+--------------------------------+
| Coincide el filtro de ruta |
| page.route('**/api/v1/**', r) |
+--------------------------------+
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
+-------------------+ +------------------+ +------------------+
| route.fulfill() | | route.abort() | | route.continue() |
+-------------------+ +------------------+ +------------------+
| Sintetiza la | | Corta el socket a| | Deja pasar el |
| respuesta en Node:| | nivel de driver | | paquete al |
| status, cabeceras,| | de red | | servidor real |
| payload JSON | | (Network Error) | | sin alteraciones |
+-------------------+ +------------------+ +------------------+
│ │ │
▼ ▼ ▼
+-------------------------------------------------------------+
| El motor de renderizado recibe el resultado |
+-------------------------------------------------------------+Route#Cada interceptor registrado recibe una instancia de Route junto con la Request original:
interface Route {
/** Devuelve una respuesta HTTP sintética al cliente */
fulfill(options?: {
status?: number;
headers?: Record<string, string>;
contentType?: string;
body?: string | Buffer;
json?: unknown;
response?: APIResponse;
}): Promise<void>;
/** Envía la petición al servidor real, permitiendo mutar cabeceras, método o body */
continue(options?: {
url?: string;
method?: string;
headers?: Record<string, string>;
postData?: string | Buffer;
}): Promise<void>;
/** Cancela la petición simulando un fallo a nivel de transporte */
abort(errorCode?:
| 'aborted'
| 'accessdenied'
| 'addressunreachable'
| 'blockedbyclient'
| 'connectionaborted'
| 'connectionclosed'
| 'connectionfailed'
| 'connectionrefused'
| 'connectionreset'
| 'internetdisconnected'
| 'nameNotResolved'
| 'timedout'
): Promise<void>;
/** Ejecuta una petición HTTP real desde el proceso Node.js para inspeccionar o alterar la respuesta del servidor */
fetch(options?: {
url?: string;
method?: string;
headers?: Record<string, string>;
postData?: string | Buffer;
}): Promise<APIResponse>;
}En las pruebas automatizadas de extremo a extremo (E2E), procesar cobros reales en pasarelas como Stripe es inviable. La intercepción permite evaluar de forma determinista la respuesta visual ante caídas de la pasarela:
import { test, expect } from '@playwright/test';
test('el usuario visualiza un banner explicativo cuando falla la pasarela de pagos', async ({ page }) => {
// 1. CRÍTICO: Registrar el interceptor ANTES de navegar a la pantalla
await page.route('**/api/v1/payments/charge', async (route) => {
// Validamos el método de la petición
expect(route.request().method()).toBe('POST');
// Respondemos con un error 500 controlado con el esquema de la API
await route.fulfill({
status: 500,
contentType: 'application/json',
json: {
success: false,
errorCode: 'GATEWAY_TIMEOUT',
message: 'La pasarela de pago no está disponible temporalmente. No se ha realizado ningún cobro.',
},
headers: {
'x-mocked-by': 'playwright-e2e',
},
});
});
await page.goto('/checkout/payment');
// Completar formulario y solicitar cobro
await page.getByRole('button', { name: 'Pagar 99,00 €' }).click();
// Validar que la interfaz gestiona el error sin romperse en una pantalla en blanco
const errorAlert = page.getByRole('alert');
await expect(errorAlert).toBeVisible();
await expect(errorAlert).toContainText('La pasarela de pago no está disponible temporalmente');
// Asegurar que el botón de pago vuelve a estar activo para reintentar
await expect(page.getByRole('button', { name: 'Pagar 99,00 €' })).toBeEnabled();
});route.fetch()#A menudo es necesario validar la interfaz con datos reales del backend pero alterando un flag puntual en tránsito (como activar un feature flag experimental o permisos de administrador), sin manipular la base de datos persistente.
import { test, expect } from '@playwright/test';
test('muestra el panel de asistente AI si el feature flag experimental está activo', async ({ page }) => {
await page.route('**/api/v1/user/features', async (route) => {
// 1. Ejecutar petición HTTP real al servidor desde Playwright
const response = await route.fetch();
const originalJson = (await response.json()) as { features: Record<string, boolean> };
// 2. Modificar el payload en memoria
const modifiedJson = {
...originalJson,
features: {
...originalJson.features,
experimentalAiCopilot: true, // Forzar activación
},
};
// 3. Devolver la respuesta mutada al navegador preservando el status y cabeceras reales
await route.fulfill({
response,
json: modifiedJson,
});
});
await page.goto('/editor');
// Verificar la presencia de la barra flotante de AI Copilot
await expect(page.getByTestId('ai-copilot-panel')).toBeVisible();
});Comprobación del almacenamiento local en IndexedDB y el comportamiento del Service Worker ante cortes abruptos de conexión:
import { test, expect } from '@playwright/test';
test('la aplicación guarda el borrador en IndexedDB al desconectarse la red', async ({
page,
context,
}) => {
await page.goto('/articles/new');
await page.getByLabel('Título del artículo').fill('Arquitectura de Sistemas Distribuidos 2026');
await page.getByRole('textbox', { name: 'Contenido' }).fill('Texto del borrador del artículo...');
// 1. Simular desconexión física de red a nivel de contexto
await context.setOffline(true);
// Guardar borrador
await page.getByRole('button', { name: 'Guardar borrador' }).click();
// 2. Comprobar mensaje de trabajo sin conexión
await expect(page.getByText('Modo offline: borrador guardado localmente')).toBeVisible();
// Recargar en offline (el Service Worker debe servir la shell en caché)
await page.reload();
// 3. Comprobar que los datos persistieron tras la recarga
await expect(page.getByLabel('Título del artículo')).toHaveValue('Arquitectura de Sistemas Distribuidos 2026');
// 4. Restaurar la conexión
await context.setOffline(false);
await expect(page.getByText('Sincronizado con la nube')).toBeVisible({ timeout: 5_000 });
});Los scripts externos de telemetría (Google Tag Manager, Datadog RUM, Hotjar, píxeles de marketing) ralentizan las pruebas entre un 30% y un 50% e introducen ruido en CI. Bloquear estas herramientas acelera la ejecución, y el método routeFromHAR permite simular respuestas completas de backend a partir de archivos HAR.
import { test, expect } from '@playwright/test';
import path from 'path';
test.describe('Catálogo con reproducción determinista mediante archivo HAR', () => {
test.beforeEach(async ({ context }) => {
// 1. Bloqueo global de trackers y analíticas a nivel de contexto
await context.route(
/(googletagmanager|google-analytics|hotjar|segment|datadoghq)\.com/,
(route) => route.abort('blockedbyclient')
);
});
test('renderizado ultraveloz del catálogo desde archivo de red local', async ({ page }) => {
// 2. Reproducir respuestas desde el archivo HAR
// update: false limita a solo lectura; notFound: 'abort' descarta peticiones no archivadas
await page.routeFromHAR(path.join(__dirname, '../__fixtures__/catalog-network.har'), {
url: '**/api/v1/catalog/**',
update: false,
notFound: 'abort',
});
await page.goto('/shop');
// Comprobar elementos (las respuestas se sirven desde el HAR local en 1-2 ms)
const items = page.getByRole('listitem');
await expect(items).toHaveCount(12);
});
});Si se invoca page.route() con posterioridad a page.goto('/'), las peticiones iniciales de arranque (carga de configuración, sesión de usuario o estado inicial) viajarán al servidor real antes de que el interceptor esté activo. El mock solo capturará interacciones posteriores, introduciendo condiciones de carrera.
// ❌ INCORRECTO: La navegación ya comenzó; el mock llega tarde
await page.goto('/dashboard');
await page.route('**/api/user', async (route) => { ... });
// ✅ CORRECTO: Registrar interceptores SIEMPRE ANTES de navegar
await page.route('**/api/user', async (route) => { ... });
await page.goto('/dashboard');Si la función en page.route(url, handler) no invoca route.fulfill(), route.continue() ni route.abort(), la petición del navegador queda en estado pendiente de forma indefinida. La interfaz del cliente se bloquea esperando la respuesta y el test expira por timeout.
// ❌ INCORRECTO: Petición colgada si no es método POST
await page.route('**/api/data', async (route) => {
if (route.request().method() === 'POST') {
await route.fulfill({ status: 200, json: { ok: true } });
}
// Falta la rama else: ¡las peticiones GET quedarán congeladas para siempre!
});
// ✅ CORRECTO: Todas las rutas de ejecución resuelven la petición
await page.route('**/api/data', async (route) => {
if (route.request().method() === 'POST') {
await route.fulfill({ status: 200, json: { ok: true } });
} else {
await route.continue();
}
});El patrón **/api/users no coincidirá con https://domain.com/api/users?page=2&limit=10 porque los parámetros de consulta rompen los patrones de barra simple.
Solución: Utilizar comodines como **/api/users* o expresiones regulares como /api\/users/.
Sustituir la totalidad de las llamadas de red convierte las pruebas de extremo a extremo en pruebas de componentes lentas en el navegador. Las divergencias de contratos entre el frontend y el backend pasarán inadvertidas a producción. Solución: Mockear únicamente servicios externos inestables (pasarelas de pago, SMS, CRMs) y casos de fallo (500, 503, Offline), ejecutando los flujos nucleares de negocio contra una API real de pruebas.
La invocación de page.route() asocia un patrón de URL con una función asíncrona en Playwright.
El código cliente ejecuta fetch(). La petición llega al stack de red nativo del motor.
El navegador retiene el envío y emite el evento Fetch.requestPaused por WebSocket a Playwright.
El código del test analiza cabeceras o cuerpo y decide ejecutar fulfill(), abort() o continue().
Playwright envía la orden al navegador y la promesa en el JavaScript cliente se resuelve o rechaza.