Playwright Python — справочник На главный сайт

Основы мокирования в Playwright

Мокирование (перехват сетевых запросов) — одна из ключевых возможностей Playwright. Она позволяет перехватывать HTTP-запросы, которые делает страница, и подменять ответы, блокировать запросы или модифицировать их на лету. Это незаменимо для тестирования сценариев с недоступными API, для ускорения тестов и для проверки поведения страницы в особых условиях.

Важно: Перехват работает на уровне протокола CDP (Chrome DevTools Protocol), что даёт полный контроль над сетевыми запросами. В отличие от сервис-воркеров или прокси, перехват в Playwright прозрачен для приложения и работает на любом сайте.

page.route() — базовый перехват запросов

Метод page.route() регистрирует обработчик для запросов, соответствующих указанному URL-паттерну. Паттерн может быть строкой (с glob-символами) или регулярным выражением.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()

    # Перехват всех запросов к API
    page.route("https://api.example.com/**", lambda route: route.fulfill(
        status=200,
        content_type="application/json",
        body='{"status": "ok"}'
    ))

    page.goto("https://example.com")

Glob-паттерны для URL

Playwright поддерживает glob-паттерны для фильтрации URL. Это удобный способ перехватить группу запросов без написания сложных регулярных выражений.

# Все CSS-файлы
page.route("**/*.css", lambda route: route.abort())

# Все изображения
page.route("**/*.{png,jpg,jpeg,gif,svg}", lambda route: route.abort())

# Все запросы к определённому поддомену
page.route("https://analytics.example.com/**", lambda route: route.abort())

# Все POST-запросы к API
page.route("**/api/**", lambda route: route.fulfill(...))

# Комбинация: все шрифты и иконки
page.route("**/*.{woff,woff2,ttf,eot,ico}", lambda route: route.abort())

# Все запросы, кроме основного документа
page.route("**/*", lambda route: route.continue_() if route.request.resource_type == "document" else route.abort())

route.fulfill() — подмена ответа

Метод route.fulfill() позволяет вернуть произвольный ответ вместо реального запроса к серверу. Можно указать статус, заголовки, тело (строкой, байтами, JSON или из файла).

# Простая подмена с JSON-ответом
page.route("**/api/users", lambda route: route.fulfill(
    status=200,
    content_type="application/json",
    body='[{"id": 1, "name": "Иван"}, {"id": 2, "name": "Мария"}]'
))

# Подмена с использованием json-параметра
page.route("**/api/user", lambda route: route.fulfill(
    status=200,
    content_type="application/json",
    json={"id": 1, "name": "Тестовый пользователь", "role": "admin"}
))

# Подмена с ответом из файла
page.route("**/api/data", lambda route: route.fulfill(
    status=200,
    path="mocks/response.json"
))

# Подмена с кастомными заголовками
page.route("**/api/users", lambda route: route.fulfill(
    status=200,
    headers={"X-Custom-Header": "mock-value", "Access-Control-Allow-Origin": "*"},
    body='[]'
))

# Имитация ошибки сервера (500)
page.route("**/api/error", lambda route: route.fulfill(
    status=500,
    content_type="application/json",
    body='{"error": "Internal Server Error", "code": "SERVER_ERROR"}'
))

# Имитация таймаута (медленный ответ)
import time
page.route("**/api/slow", lambda route: (
    time.sleep(10),
    route.fulfill(status=200, body='"slow response"')
))

route.abort() — блокировка запроса

Метод route.abort() отменяет запрос с указанной причиной ошибки. Это полезно для блокировки нежелательных ресурсов (реклама, аналитика, шрифты).

# Блокировка с причиной "aborted" (по умолчанию)
page.route("**/*.css", lambda route: route.abort())

# Различные причины ошибки
page.route("https://ads.example.com/**", lambda route: route.abort("connectionrefused"))
page.route("https://tracker.example.com/**", lambda route: route.abort("accessdenied"))
page.route("https://cdn.example.com/**", lambda route: route.abort("internetdisconnected"))

# Блокировка только скриптов
page.route("**/*.js", lambda route: route.abort("aborted"))

# Блокировка изображений для ускорения тестов
page.route("**/*.{png,jpg,jpeg,gif,svg,webp}", lambda route: route.abort())

# Блокировка внешних ресурсов, но разрешение локальных
page.route("**/*", lambda route: route.continue_()
    if route.request.url.startswith("http://localhost")
    else route.abort())

Важно: Причины ошибок для abort: "aborted", "accessdenied", "addressunreachable", "blockedbyclient", "blockedbyresponse", "connectionaborted", "connectionclosed", "connectionfailed", "connectionrefused", "connectionreset", "internetdisconnected", "namenotresolved", "timedout", "failed".

route.continue_() — модификация запроса

Метод route.continue_() позволяет пропустить запрос, изменив его параметры: URL, метод, заголовки или тело. Запрос будет отправлен на сервер с изменениями.

# Продолжить запрос без изменений
page.route("**/api/**", lambda route: route.continue_())

# Изменить заголовки запроса
page.route("**/api/**", lambda route: route.continue_(
    headers={
        "Authorization": "Bearer mock-token",
        "X-Test": "true"
    }
))

# Изменить URL запроса (перенаправление)
page.route("**/api/v1/**", lambda route: route.continue_(
    url=route.request.url.replace("/api/v1/", "/api/v2/")
))

# Изменить метод запроса
page.route("**/api/delete/**", lambda route: route.continue_(
    method="GET"
))

# Изменить тело POST-запроса
page.route("**/api/users", lambda route: route.continue_(
    post_data='{"name": "Мокированный", "role": "admin"}'
))

# Комбинация: изменить заголовки и добавить параметр в URL
page.route("**/api/search", lambda route: route.continue_(
    url=route.request.url + "&mock=true",
    headers={"X-Mock": "true"}
))

Совет: Используйте route.continue_(), когда нужно изменить запрос, но не подменять ответ полностью. Например, для добавления тестовых заголовков, подмены URL или изменения тела запроса.

route.fetch() — выполнение реального запроса из обработчика

Метод route.fetch() выполняет реальный HTTP-запрос к серверу (как если бы перехвата не было). Это полезно, когда нужно выполнить реальный запрос, но при необходимости изменить его результат.

import json

def handle_route(route):
    # Выполнить реальный запрос к серверу
    response = route.fetch()

    # Модифицировать ответ
    data = response.json()
    data["mocked"] = True
    data["original_length"] = len(data)

    # Вернуть модифицированный ответ
    route.fulfill(
        status=response.status,
        headers=response.headers,
        body=json.dumps(data)
    )

page.route("**/api/users", handle_route)


# Пример: добавить элемент в список ответа
def add_item_to_response(route):
    response = route.fetch()
    data = response.json()
    data["items"].append({"id": 999, "name": "Подставной элемент"})
    route.fulfill(status=response.status, json=data)

page.route("**/api/items", add_item_to_response)

page.unroute() — удаление обработчика

Метод page.unroute() удаляет зарегистрированный обработчик для указанного паттерна. Если обработчик не был указан, удаляются все обработчики для данного паттерна.

# Удаление конкретного обработчика
def handler(route):
    route.fulfill(status=200, body="mock")

page.route("**/api/data", handler)
# ... тест с моком ...
page.unroute("**/api/data", handler)

# Удаление всех обработчиков для паттерна
page.unroute("**/api/**")

# Удаление всех обработчиков вообще
page.unroute("**/*")

# Контекстный менеджер для временного мока
page.route("**/api/test", lambda route: route.fulfill(body="temp"))
# ... выполнение теста с моком ...
page.unroute("**/api/test")
# Теперь запросы снова идут на реальный сервер

context.route() — перехват для всего контекста

Метод context.route() работает аналогично page.route(), но перехватывает запросы во всех страницах данного браузерного контекста. Это полезно для установки глобальных моков.

# Перехват на уровне контекста (работает для всех страниц)
context.route("**/analytics/**", lambda route: route.abort())

# Глобальная подмена API для всех страниц контекста
context.route("**/api/config", lambda route: route.fulfill(
    json={"feature_enabled": True, "max_items": 50}
))

# Удаление обработчика контекста
context.unroute("**/analytics/**")

# Все страницы в этом контексте не загружают шрифты
context.route("**/*.woff2", lambda route: route.abort())

page.route_from_har() — ответы из HAR-файла

Playwright может перехватывать запросы и возвращать ответы, сохранённые в HAR-файле (HTTP Archive). Это мощный инструмент для воспроизведения тестов с заранее записанными данными.

# Использование HAR-файла для мокирования
page.route_from_har("test-data/har/api-response.har")

# Использование с фильтрацией URL
page.route_from_har(
    "test-data/har/example.har",
    url="**/api/**"  # только API-запросы из HAR
)

# Обновление HAR-файла (update = True)
page.route_from_har(
    "test-data/har/example.har",
    update=True  # перезаписать HAR новыми данными
)

# Запись HAR-файла при первом запуске
context = browser.new_context(record_har_path="test-data/har/example.har")
page = context.new_page()
page.goto("https://example.com")
# После завершения HAR-файл будет содержать все запросы
context.close()

# Использование записанного HAR в тестах
page.route_from_har("test-data/har/example.har")
page.goto("https://example.com")
# Все запросы будут взяты из HAR, а не выполнены реально

Примечание: HAR-файлы (HTTP Archive) содержат полную информацию о запросах и ответах. Playwright может как записывать HAR (через опцию record_har_path в контексте), так и воспроизводить из него ответы. Это идеально для тестов, которые должны работать офлайн или с фиксированными данными.

Практические примеры

Пример 1: Мок API для тестирования

Полный пример теста, который мокирует API и проверяет отображение данных на странице.

from playwright.sync_api import sync_playwright, expect
import json

def test_user_list_with_mock_api():

    mock_users = [
        {"id": 1, "name": "Анна", "status": "active"},
        {"id": 2, "name": "Борис", "status": "inactive"},
        {"id": 3, "name": "Виктория", "status": "active"}
    ]

    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()

        # Перехват запроса к API и возврат мок-данных
        page.route("**/api/users", lambda route: route.fulfill(
            status=200,
            content_type="application/json",
            body=json.dumps(mock_users)
        ))

        page.goto("https://example.com/users")

        # Проверка, что данные отображаются
        expect(page.get_by_text("Анна")).to_be_visible()
        expect(page.get_by_text("Борис")).to_be_visible()
        expect(page.get_by_text("Виктория")).to_be_visible()

        browser.close()

Пример 2: Блокировка рекламы и аналитики

Блокировка нежелательных запросов для ускорения тестов и устранения внешних зависимостей.

def test_page_without_ads(page):
    # Блокировка рекламы и аналитики
    page.route("**/ads/**", lambda route: route.abort())
    page.route("**/analytics/**", lambda route: route.abort())
    page.route("**/tracker/**", lambda route: route.abort())

    # Блокировка изображений (для ускорения)
    page.route("**/*.{png,jpg,jpeg,gif}", lambda route: route.abort())

    # Навигация на страницу
    page.goto("https://example.com")

    # Проверка основного контента
    expect(page.get_by_role("heading", name="Добро пожаловать")).to_be_visible()

Пример 3: Подмена изображения на тестовое

Замена изображений на тестовые заглушки для изоляции тестов.

def test_image_mock(page):
    # Подмена всех изображений на тестовую заглушку
    page.route("**/*.{png,jpg,jpeg,gif,webp}", lambda route: route.fulfill(
        status=200,
        content_type="image/png",
        path="mocks/test-image.png"  # файл-заглушка 1x1 пиксель
    ))

    page.goto("https://example.com/gallery")

    # Все изображения загружены (заглушки)
    images = page.get_by_role("img").all()
    assert len(images) == 10

Пример 4: Мокирование ошибок сети

Тестирование поведения страницы при различных сетевых ошибках.

def test_error_scenarios(page):
    # Тест 1: ошибка 500 от API
    page.route("**/api/data", lambda route: route.fulfill(
        status=500,
        content_type="application/json",
        body='{"error": "Server Error"}'
    ))
    page.goto("https://example.com/data")
    expect(page.get_by_text("Ошибка загрузки данных")).to_be_visible()

    # Тест 2: таймаут запроса (очень медленный ответ)
    page.route("**/api/data", lambda route: (
        time.sleep(60),  # долгий ответ
        route.fulfill(status=200, body='"ok"')
    ))
    page.goto("https://example.com/data")
    expect(page.get_by_text("Таймаут соединения")).to_be_visible()

    # Тест 3: блокировка запроса
    page.route("**/api/data", lambda route: route.abort("connectionrefused"))
    page.goto("https://example.com/data")
    expect(page.get_by_text("Сервер недоступен")).to_be_visible()

Пример 5: Динамическая модификация ответа

Перехват реального ответа и добавление в него тестовых данных.

def test_inject_test_data(page):
    def modify_response(route):
        # Выполняем реальный запрос
        response = route.fetch()

        # Модифицируем JSON-ответ
        data = response.json()
        data["test_mode"] = True
        data["notification"] = "Это тестовое окружение"

        # Добавляем элемент в массив
        if "items" in data:
            data["items"].insert(0, {"id": "test-001", "name": "Тестовый элемент"})

        route.fulfill(
            status=response.status,
            headers=response.headers,
            body=json.dumps(data)
        )

    page.route("**/api/config", modify_response)
    page.goto("https://example.com")

    # Проверка, что тестовые данные отображаются
    expect(page.get_by_text("Это тестовое окружение")).to_be_visible()

Заключение

Мокирование в Playwright — мощный механизм для контроля над сетевыми запросами в тестах. Основные возможности:

  • page.route() и context.route() — перехват запросов по glob-паттернам или регулярным выражениям
  • route.fulfill() — подмена ответа с полным контролем над статусом, заголовками и телом
  • route.abort() — блокировка нежелательных запросов (реклама, аналитика, внешние ресурсы)
  • route.continue_() — модификация запроса перед отправкой на сервер
  • route.fetch() — выполнение реального запроса из обработчика с возможностью модификации ответа
  • page.route_from_har() — воспроизведение ответов из HAR-файла
  • page.unroute() — удаление обработчиков