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?
- 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
- 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=10Interceptando 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());
});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');
});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()