Un 500 de la API de productos, un gateway lento que dispara un spinner de carga, un fallo de red que testea si el mensaje de error es útil: ninguno de estos escenarios es fácil de reproducir contra un backend real a demanda. page.route() intercepta cualquier petición que hace tu aplicación y te permite devolver route.fulfill() con cualquier status y body, retrasar y dejar pasar con route.continue(), o simular un fallo de red con route.abort(). Esta guía cubre los patrones principales: fixtures JSON, testing de estados de error, modificar respuestas reales con route.fetch(), bloquear scripts de terceros, y waitForRequest/waitForResponse para verificar qué envía realmente la app.
Por qué interceptar peticiones de red
Testear estados difíciles de reproducir
El servidor devuelve 500 (¿qué muestra la UI?), la API tarda 10 segundos (¿hay un spinner de carga?) o el gateway de pago está caído (¿el checkout muestra un error útil?).
Hacer los tests más rápidos y confiables
Mockea el backend para que los tests no dependan de datos reales, bloquea analytics, ads y scripts de tracking que ralentizan la carga de página, y evita rate limits en APIs externas.
Testear sin un backend
Desarrolla tests de UI antes de que la API esté construida y testea casos extremos difíciles de disparar en un sistema real.
Mocking básico de rutas
page.route() intercepta peticiones que coinciden con un patrón de URL:
test('muestra estado de carga', async ({ page }) => {
// Interceptar y retrasar la API de productos
await page.route('/api/products', async (route) => {
await new Promise(resolve => setTimeout(resolve, 3000)); // 3 segundos de delay
await route.continue(); // Luego dejar pasar la petición real
});
await page.goto('/products');
// El spinner debería ser visible durante el delay
await expect(page.getByTestId('loading-spinner')).toBeVisible();
// Esperar a que carguen los productos
await expect(page.getByTestId('product-card').first()).toBeVisible();
});Devolver respuestas mockeadas
En lugar de reenviar al servidor real, devuelve datos falsos:
const mockUsers = [
{ id: 1, name: 'Alice', email: 'alice@test.com', role: 'admin' },
{ id: 2, name: 'Bob', email: 'bob@test.com', role: 'member' },
];
test('la tabla de usuarios muestra todos los usuarios', async ({ page }) => {
await page.route('/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockUsers),
});
});
await page.goto('/admin/users');
const rows = page.getByTestId('user-row');
await expect(rows).toHaveCount(2);
await expect(rows.first()).toContainText('Alice');
await expect(rows.last()).toContainText('Bob');
});Testear estados de error
test('muestra error cuando la API falla', async ({ page }) => {
await page.route('/api/products', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/products');
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message')).toContainText('Something went wrong');
await expect(page.getByTestId('retry-button')).toBeVisible();
});
test('muestra estado vacío cuando no hay productos', async ({ page }) => {
await page.route('/api/products', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
});
await page.goto('/products');
await expect(page.getByTestId('empty-state')).toBeVisible();
await expect(page.getByTestId('empty-state')).toContainText('No products found');
});
test('muestra mensaje de error de red', async ({ page }) => {
await page.route('/api/products', async (route) => {
await route.abort('failed'); // Simular fallo de red
});
await page.goto('/products');
await expect(page.getByTestId('network-error')).toBeVisible();
});Patrones de URL
page.route() soporta patrones glob y regex:
// URL exacta
await page.route('/api/users', handler);
// Wildcard
await page.route('/api/users/*', handler); // /api/users/1, /api/users/abc
await page.route('/api/**', handler); // Todas las rutas de la API
// Regex
await page.route(/\/api\/users\/\d+/, handler); // /api/users/123
// Glob con query string
await page.route('/api/products?*', handler); // /api/products?page=1&limit=10Interceptar y modificar peticiones
Lee la petición real antes de decidir qué hacer:
test('usa el header de auth correcto', async ({ page }) => {
let capturedAuthHeader = '';
await page.route('/api/users', async (route) => {
// Capturar el header real de la petición
capturedAuthHeader = route.request().headers()['authorization'] || '';
await route.fulfill({
status: 200,
body: JSON.stringify([]),
});
});
// Hacer algo que dispare la petición
await page.goto('/admin/users');
expect(capturedAuthHeader).toMatch(/Bearer .+/);
});
test('envía el body de petición correcto', async ({ page }) => {
let capturedBody = '';
await page.route('/api/auth/login', async (route) => {
capturedBody = route.request().postData() || '';
await route.fulfill({
status: 200,
body: JSON.stringify({ token: 'fake-token', user: { id: 1 } }),
});
});
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@test.com');
await page.fill('[data-testid="password"]', 'ValidPass1');
await page.click('[data-testid="submit"]');
const body = JSON.parse(capturedBody);
expect(body.email).toBe('user@test.com');
expect(body.password).toBe('ValidPass1');
});Modificar respuestas reales
Intercepta una petición real y modifica su respuesta:
test('maneja roles de usuario adicionales correctamente', async ({ page }) => {
await page.route('/api/users/1', async (route) => {
// Dejar pasar la petición real
const response = await route.fetch();
const body = await response.json();
// Modificar la respuesta
body.role = 'super-admin'; // Este rol puede no existir en la DB de test
await route.fulfill({
status: 200,
body: JSON.stringify(body),
});
});
await page.goto('/users/1');
// Testear cómo maneja la UI valores de rol inesperados
await expect(page.getByTestId('role-badge')).toBeVisible();
});Bloquear peticiones de terceros
Acelera los tests bloqueando tracking, analytics y ads:
test.beforeEach(async ({ page }) => {
// Bloquear scripts comunes de terceros
await page.route('**/*.{png,jpg,jpeg,gif,svg,ico,woff,woff2}', route => route.abort());
await page.route('**/google-analytics.com/**', route => route.abort());
await page.route('**/googletagmanager.com/**', route => route.abort());
await page.route('**/hotjar.com/**', route => route.abort());
await page.route('**/intercom.io/**', route => route.abort());
await page.route('**/sentry.io/**', route => route.abort());
});Usar waitForRequest y waitForResponse
Esperá a que ocurra actividad de red específica:
test('el envío del formulario envía los datos correctos', async ({ page }) => {
await page.goto('/login');
// Empezar a esperar la petición ANTES de la acción que la dispara
const requestPromise = page.waitForRequest(req =>
req.url().includes('/api/auth/login') && req.method() === 'POST'
);
await page.fill('[data-testid="email"]', 'user@test.com');
await page.fill('[data-testid="password"]', 'ValidPass1');
await page.click('[data-testid="submit"]');
const request = await requestPromise;
const body = JSON.parse(request.postData() || '{}');
expect(body.email).toBe('user@test.com');
});
test('la página recarga después de guardar', async ({ page }) => {
await page.goto('/profile');
// Esperar la llamada a la API de guardado
const responsePromise = page.waitForResponse(resp =>
resp.url().includes('/api/users') && resp.status() === 200
);
await page.fill('[data-testid="name"]', 'New Name');
await page.click('[data-testid="save"]');
const response = await responsePromise;
const body = await response.json();
expect(body.name).toBe('New Name');
});Patrones de datos mock realistas
// data/mocks/users.ts
export const mockUser = (overrides = {}) => ({
id: Math.floor(Math.random() * 10000),
email: `user_${Date.now()}@test.com`,
name: 'Test User',
role: 'member',
createdAt: new Date().toISOString(),
...overrides,
});
export const mockPaginatedResponse = <T>(items: T[], page = 1, limit = 10) => ({
data: items,
page,
limit,
total: items.length,
totalPages: Math.ceil(items.length / limit),
});test('la tabla de admin maneja 100 usuarios', async ({ page }) => {
const users = Array.from({ length: 100 }, (_, i) =>
mockUser({ id: i + 1, name: `User ${i + 1}` })
);
await page.route('/api/users', async (route) => {
const url = new URL(route.request().url());
const pageNum = parseInt(url.searchParams.get('page') || '1');
const limit = parseInt(url.searchParams.get('limit') || '10');
const start = (pageNum - 1) * limit;
const pageUsers = users.slice(start, start + limit);
await route.fulfill({
status: 200,
body: JSON.stringify(mockPaginatedResponse(pageUsers, pageNum, limit)),
});
});
await page.goto('/admin/users');
// Verificar que la paginación muestra el conteo correcto
await expect(page.getByTestId('total-count')).toContainText('100');
await expect(page.getByTestId('user-row')).toHaveCount(10); // Primera página
});Referencia de métodos
| Método | Qué hace |
|--------|----------|
| page.route(url, handler) | Intercepta peticiones que coinciden con la URL |
| route.fulfill({...}) | Devuelve una respuesta mockeada |
| route.continue() | Deja pasar la petición real |
| route.abort('failed') | Simula un fallo de red |
| route.fetch() | Realiza la petición real y obtiene la respuesta |
| page.waitForRequest(filter) | Espera a que ocurra una petición específica |
| page.waitForResponse(filter) | Espera a que ocurra una respuesta específica |