Reproduzir um erro 500 da API de produtos ou uma gateway lenta que dispara um spinner de carregamento não é fácil contra um backend real. O mesmo vale para uma falha de rede que testa se a mensagem de erro é útil. page.route() intercepta qualquer requisição que sua aplicação faz e permite retornar uma resposta simulada com route.fulfill(), repassar com route.continue(), ou simular falha de rede com route.abort().

Por que interceptar requisições de rede?

Testar estados difíceis de reproduzir:
  • O servidor retorna 500: o que a UI mostra?
  • A API demora 10 segundos: existe um spinner de carregamento?
  • O gateway de pagamento está fora: o checkout exibe um erro útil?
Tornar os testes mais rápidos e confiáveis:
  • Mock do backend para que os testes não dependam de dados reais
  • Bloqueio de analytics, anúncios e scripts de rastreamento que atrasam o carregamento
  • Sem risco de bater em rate limits de APIs externas
Testar sem um backend:
  • Desenvolver testes de UI antes de a API existir
  • Testar edge cases difíceis de acionar em um sistema real

Mock básico de rotas

page.route() intercepta requisições que correspondem a um padrão de URL:

test('shows loading state', async ({ page }) => {
  // Intercepta e atrasa a API de produtos
  await page.route('/api/products', async (route) => {
    await new Promise(resolve => setTimeout(resolve, 3000));  // 3 segundos de delay
    await route.continue();  // Depois deixa a requisição real passar
  });
  
  await page.goto('/products');
  
  // O spinner deve estar visível durante o delay
  await expect(page.getByTestId('loading-spinner')).toBeVisible();
  
  // Aguarda os produtos carregarem
  await expect(page.getByTestId('product-card').first()).toBeVisible();
});

Retornando respostas simuladas

Em vez de repassar para o servidor real, retorne dados fictícios:

const mockUsers = [
  { id: 1, name: 'Alice', email: 'alice@test.com', role: 'admin' },
  { id: 2, name: 'Bob', email: 'bob@test.com', role: 'member' },
];

test('users table shows all users', 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');
});

Testando estados de erro

test('shows error when API fails', 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('shows empty state when no products', 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('shows network error message', async ({ page }) => {
  await page.route('/api/products', async (route) => {
    await route.abort('failed');  // Simula falha de rede
  });
  
  await page.goto('/products');
  
  await expect(page.getByTestId('network-error')).toBeVisible();
});

Padrões de URL

page.route() aceita glob patterns e regex:

// URL exata
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 as rotas de API

// Regex
await page.route(/\/api\/users\/\d+/, handler);  // /api/users/123

// Glob com query string
await page.route('/api/products?*', handler);  // /api/products?page=1&limit=10

Interceptando e modificando requisições

Leia a requisição real antes de decidir o que fazer:

test('uses correct auth header', async ({ page }) => {
  let capturedAuthHeader = '';
  
  await page.route('/api/users', async (route) => {
    // Captura o header real da requisição
    capturedAuthHeader = route.request().headers()['authorization'] || '';
    
    await route.fulfill({
      status: 200,
      body: JSON.stringify([]),
    });
  });
  
  // Dispara a requisição
  await page.goto('/admin/users');
  
  expect(capturedAuthHeader).toMatch(/Bearer .+/);
});

test('sends correct request body', 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');
});

Modificando respostas reais

Intercepte uma requisição real e altere a resposta antes de ela chegar ao app:

test('handles extra user roles gracefully', async ({ page }) => {
  await page.route('/api/users/1', async (route) => {
    // Deixa a requisição real passar
    const response = await route.fetch();
    const body = await response.json();
    
    // Modifica a resposta
    body.role = 'super-admin';  // Esse role pode não existir no banco de teste
    
    await route.fulfill({
      status: 200,
      body: JSON.stringify(body),
    });
  });
  
  await page.goto('/users/1');
  
  // Testa como a UI lida com valores de role inesperados
  await expect(page.getByTestId('role-badge')).toBeVisible();
});

Bloqueando requisições de terceiros

Scripts de tracking, analytics e anúncios atrasam os testes. Bloqueie todos de uma vez no beforeEach:

test.beforeEach(async ({ page }) => {
  // Bloqueia scripts comuns de terceiros
  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());
});

Bloquear imagens pode afetar testes visuais ou assertions sensíveis ao layout.

Usando page.waitForRequest e page.waitForResponse

Aguarde atividade de rede específica acontecer:

test('form submission sends correct data', async ({ page }) => {
  await page.goto('/login');
  
  // Registre o waitForRequest ANTES da ação que vai disparar a requisição
  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('page reloads after save', async ({ page }) => {
  await page.goto('/profile');
  
  // Aguarda a chamada de salvar
  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');
});

Sempre registre waitForRequest e waitForResponse antes da ação que dispara a requisição. Registrar depois cria uma race condition: a requisição pode terminar antes de o Playwright começar a aguardar.

Padrões de dados simulados realistas

Factory functions evitam dados duplicados e facilitam criar variações:

// 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('admin table handles 100 users', 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');
  
  // Verifica se a paginação exibe o total correto
  await expect(page.getByTestId('total-count')).toContainText('100');
  await expect(page.getByTestId('user-row')).toHaveCount(10);  // Primeira página
});

Resumo

| Método | O que faz |

|--------|-----------|

| page.route(url, handler) | Intercepta requisições que correspondem à URL |

| route.fulfill({...}) | Retorna uma resposta simulada |

| route.continue() | Deixa a requisição real passar |

| route.abort('failed') | Simula falha de rede |

| route.fetch() | Faz a requisição real e captura a resposta |

| page.waitForRequest(filter) | Aguarda uma requisição específica acontecer |

| page.waitForResponse(filter) | Aguarda uma resposta específica |

A interceptação de rede é um dos recursos mais úteis do Playwright para testar edge cases. Use para testar estados de carregamento, erros, listas vazias e falhas de rede. Esses são os cenários mais difíceis de reproduzir de forma consistente contra um backend real.

→ Veja também: Interceptação de Rede, Mocking e Stubbing no Playwright | Testes de API com Playwright: Além da Interface | Estratégias de Espera no Playwright: Sem sleep()