500 от API продуктов, медленный шлюз который вызывает спиннер загрузки, сетевой сбой проверяющий полезность сообщения об ошибке: ничего из этого нельзя стабильно воспроизвести против реального бэкенда по запросу. page.route() перехватывает любой запрос приложения и позволяет вернуть route.fulfill() с любым статусом и телом, задержать и пропустить через route.continue(), или симулировать сетевой сбой через route.abort(). Это руководство разбирает основные паттерны: JSON-фикстуры, тестирование состояний ошибок, модификацию реальных ответов через route.fetch(), блокировку сторонних скриптов и waitForRequest/waitForResponse для проверки что именно приложение отправляет.

Зачем перехватывать сетевые запросы

Тестирование состояний которые сложно воспроизвести

Что покажет UI когда сервер возвращает 500? Появится ли спиннер загрузки если API отвечает 10 секунд? Покажет ли чекаут понятную ошибку когда платёжный шлюз недоступен?

Ускорение и повышение надёжности тестов

Замокировать бэкенд чтобы тесты не зависели от реальных данных, заблокировать аналитику, рекламу и трекинг-скрипты замедляющие загрузку, избежать rate limit на внешних API.

Тестирование без бэкенда

Разрабатывать UI-тесты до того как API построен и тестировать крайние случаи которые сложно вызвать в реальной системе.

Базовое мокирование маршрутов

page.route() перехватывает запросы совпадающие с URL-паттерном:

test('shows loading state', async ({ page }) => {
  // Перехватываем и задерживаем API продуктов
  await page.route('/api/products', async (route) => {
    await new Promise(resolve => setTimeout(resolve, 3000));  // 3 секунды задержки
    await route.continue();  // Потом пропускаем реальный запрос
  });
  
  await page.goto('/products');
  
  // Спиннер должен быть виден во время задержки
  await expect(page.getByTestId('loading-spinner')).toBeVisible();
  
  // Ждём загрузки продуктов
  await expect(page.getByTestId('product-card').first()).toBeVisible();
});

Возврат мокированных ответов

Вместо пересылки на реальный сервер возвращаем фейковые данные:

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');
});

Тестирование состояний ошибок

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');  // Симулируем сетевой сбой
  });
  
  await page.goto('/products');
  
  await expect(page.getByTestId('network-error')).toBeVisible();
});

URL-паттерны

page.route() поддерживает glob-паттерны и regex:

// Точный URL
await page.route('/api/users', handler);

// Wildcard
await page.route('/api/users/*', handler);  // /api/users/1, /api/users/abc
await page.route('/api/**', handler);        // Все API-маршруты

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

// Glob со строкой запроса
await page.route('/api/products?*', handler);  // /api/products?page=1&limit=10

Перехват и модификация запросов

Читаем реальный запрос перед тем как решить что делать:

test('uses correct auth header', async ({ page }) => {
  let capturedAuthHeader = '';
  
  await page.route('/api/users', async (route) => {
    // Захватываем реальный заголовок запроса
    capturedAuthHeader = route.request().headers()['authorization'] || '';
    
    await route.fulfill({
      status: 200,
      body: JSON.stringify([]),
    });
  });
  
  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');
});

Модификация реальных ответов

Перехватываем реальный запрос и изменяем его ответ:

test('handles extra user roles gracefully', async ({ page }) => {
  await page.route('/api/users/1', async (route) => {
    // Пропускаем реальный запрос
    const response = await route.fetch();
    const body = await response.json();
    
    // Модифицируем ответ
    body.role = 'super-admin';  // Такой роли может не быть в тестовой БД
    
    await route.fulfill({
      status: 200,
      body: JSON.stringify(body),
    });
  });
  
  await page.goto('/users/1');
  
  // Проверяем как UI обрабатывает неожиданные значения роли
  await expect(page.getByTestId('role-badge')).toBeVisible();
});

Блокировка сторонних запросов

Ускоряем тесты блокируя трекинг, аналитику и рекламу:

test.beforeEach(async ({ page }) => {
  // Блокируем распространённые сторонние скрипты
  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());
});

Осторожно: блокировка изображений может повлиять на визуальные тесты или ассёрты чувствительные к вёрстке.

waitForRequest и waitForResponse

Ожидание конкретной сетевой активности:

test('form submission sends correct data', async ({ page }) => {
  await page.goto('/login');
  
  // Начинаем ждать запрос ДО действия которое его вызовет
  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');
  
  // Ждём API-вызов сохранения
  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');
});

Паттерны реалистичных мок-данных

// 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');
  
  // Проверяем что пагинация показывает правильное количество
  await expect(page.getByTestId('total-count')).toContainText('100');
  await expect(page.getByTestId('user-row')).toHaveCount(10);  // Первая страница
});

Шпаргалка

| Метод | Что делает |

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

| page.route(url, handler) | Перехватывает запросы совпадающие с URL |

| route.fulfill({...}) | Возвращает мокированный ответ |

| route.continue() | Пропускает реальный запрос |

| route.abort('failed') | Симулирует сетевой сбой |

| route.fetch() | Выполняет реальный запрос, получает ответ |

| page.waitForRequest(filter) | Ждёт конкретный запрос |

| page.waitForResponse(filter) | Ждёт конкретный ответ |

→ See also: Перехват сети, моки и стабы в Playwright | API-тестирование в Playwright: выходим за рамки UI | Стратегии ожидания в Playwright: без sleep()