Прямой ответ
Playwright — браузерная автоматизация (Chromium/Firefox/WebKit одним API) с встроенным auto-waiting (действия ждут появления/кликабельности элемента — sleep-ы не нужны в принципе — половина flaky-тестов прошлого поколения исчезает архитектурно). Ключевые практики надёжности: (1) локаторы по пользовательскому смыслу — get_by_role("button", name="Оплатить"), get_by_test_id("checkout-form") (роль+текст/testid — стабильны к перекладке вёрстки; CSS/XPath по структуре DOM — хрупкость на каждый рефакторинг — договорённость с фронтендом о data-testid на ключевых элементах — контракт); (2) assertions с ожиданием — expect(locator).to_be_visible() / to_have_text(...) (ретраятся до таймаута — проверка «стало», не «сейчас» — классический flaky «не успело отрендериться» закрыт); (3) изолированные тесты — каждый тест со своим браузерным контекстом (чистые cookies/storage — фикстура page делает это из коробки; переиспользование залогиненной сессии — через storage_state, не через «залогиниться в первом тесте, остальные по инерции»); (4) сеть под контролем — page.route() перехватывает/мокает API (тест UI без бэкенда — быстрый и детерминированный; с бэкендом — testcontainers поднимает реальный стек) — стратегия по цели теста; (5) параллельность — pytest-xdist + workers в конфиге (e2e медленные — параллель обязателен; тесты независимы — п.3 — параллелятся безопасно); (6) CI — официальный docker-образ (браузеры внутри — CI-матрица), трейсы/скриншоты/видео при провале (trace on-first-retry — разбор падения по трейсу — минуты вместо гаданий), ретраи 1–2 в CI (компенсация средового шума — но flaky-тест лечат, а не ретраят вечно). Границы честности: e2e — дорогое медленное покрытие критических путей (регистрация/оплата/ключевой сценарий — десятки тестов, не сотни); основная масса проверок — юнит/интеграционные (пирамида) — «все тесты через браузер» — пирамида наоборот, разоряющая команду.
Локаторы, ожидания, структура теста
Анатомия: page.get_by_role(...) возвращает локатор (ленивое описание элемента — резолвится в момент действия/проверки); действия (click/fill/check) — с авто-ожиданием actionable-состояния (видим, стабилен, не disabled — гонки «кликнул до отрисовки» уходят); приоритет локаторов: роль+имя (доступность-семантика — button/link/heading — заодно мотивирует фронтенд держать a11y), label (формы — get_by_label("Email")), text, test_id (нет роли/текста — data-testid — соглашение команды: testid не меняется при редизайне, в отличие от классов); анти-паттерны: page.locator("div > div:nth-child(3)") (структура DOM — деталь реализации), XPath-цепочки (то же + нечитаемость), page.wait_for_timeout(3000) (sleep — источник и flaky, и медлительности — вместо него expect(...) и явные условия). Ожидания: web-first assertions (expect(locator).to_have_count(3), to_be_enabled(), URL-ожидания expect(page).to_have_url(...)) ретраятся до таймаута (дефолт 5 с — настраивается); «проверить мгновенное состояние без ожидания» — почти всегда ошибка (UI асинхронен по природе — realtime-специфика — ожидания ещё важнее). Структура: arrange (данные/состояние — через API напрямую: тест «оформление заказа» начинает с созданного через API заказа — не с десяти кликов подготовки — быстрее и устойчивее; подготовка кликами — только если сам путь и есть предмет теста), act (действие пользователя), assert (результат на UI + опционально побочные эффекты через API/БД-проверку).
Page Object, фикстуры и данные
Page Object — инкапсуляция страниц: класс CheckoutPage(page) с локаторами и действиями (def pay(self, card): self.page.get_by_role("button", name="Оплатить").click(); ...) — тесты читаются сценариями (checkout.pay(TEST_CARD)), изменения вёрстки правятся в одном месте; граница: PO — для переиспользуемых потоков (логин/корзина/админка — десятки тестов поверх), не для каждого экрана формально (PO ради PO — церемония без пользы — «толстые PO на 50 методов» — признак, что тесты дублируют друг друга). Фикстуры (pytest-интеграция — фикстуры родные): page/browser — встроенные (scope-функция — изоляция); свои — логин-состояние (storage_state — залогиненный контекст переиспользуется — «логин» происходит один раз на сессию через API, не в каждом тесте), тестовые данные (фабрики заказов/пользователей через API — каждая фикстура убирает за собой — teardown через API; «общий тестовый юзер на все тесты» — гонки при параллели — свой юзер на тест), сервер приложения (session-фикстура поднимает app — или внешний стейдж-URL из env — стратегия команды). Данные и окружение: e2e против изолированного стейджа с тестовой БД (контейнеры локально, отдельный стенд в CI) — не против прода (платежи/письма — sandbox-режимы (email — локальный SMTP-перехватчик, платежи — тестовые ключи)); моки сети (page.route("**/api/orders", fulfill=json)) — для UI-состояний, трудных вживую (ошибка 500 от API, медленная сеть — route + задержка — проверка спиннеров/ошибок); правило разделения: бизнес-пути — реальный бэкенд; edge-кейсы интерфейса — моки.
CI, отладка, flaky-гигиена
CI-интеграция: контейнер mcr.microsoft.com/playwright/python (браузеры+зависимости — свой docker-слой кэшируется — CI-оптимизация); запуск — параллельный (workers по числу ядер раннера; шардирование на большие наборы — несколько джоб по частям), headed-режим не нужен (headless + видео при провале), артефакты — traces/скриншоты/видео в artifacts (Actions-артефакты); трейс — главный инструмент разбора (trace viewer — покадровая лента: DOM/сеть/консоль/действия — «почему упало» видно буквально); --retries=2 в CI + --retries=0 локально (ретрай — компенсация шума среды, локально flaky виден сразу). Flaky-гигиена (e2e-репутация живёт здесь): flaky = тест с результатом без изменения кода — источники: sleep-ожидания (→ web-first assertions), общие данные между тестами (→ изоляция/фабрики), гонки параллели (→ свой юзер/заказ на тест), внешние зависимости (→ моки/песочницы), таймауты впритык (→ адекватные ожидания); процесс: quarantine-список (flaky-тест выключается из блокирующих проверок с тикетом — чинится или удаляется; «ретраим 5 раз, иногда проходит» — не лечение); метрика здоровья — % зелёных прогонов без ретраев (видна в CI-статистике — падение — сигнал деградации набора). Масштаб набора: e2e-пирамида в числах — юнит-сотни (секунды), интеграционные — десятки (минуты — API/БД), e2e — 20–50 критических путей (5–15 минут параллельно) — «200 e2e на все формы» — ночные прогоны и очередь на фиксы; критерий отбора e2e-кейса — «пользовательский путь, поломка которого = инцидент» (логин, оплата, ключевой CRUD, критичные интеграционные сценарии); остальное — уровни ниже (дешевле/быстрее/точнее локализуют баг).
Практический сценарий: e2e для чекаута за неделю
Исходник: интернет-магазин, релизы еженедельно, регресс руками 2 часа (и всё равно «оплата сломалась» в прошлом релизе прошла в прод — проверили «не тот путь»). Внедрение (неделя, один разработчик): (1) стенд — docker-compose приложения с тестовой БД (стек) + фикстура session-подъёма, тестовые данные — фабрики через API (заказ/товар/юзер — изоляция на тест); (2) Page Object: CatalogPage/ProductPage/CartPage/CheckoutPage (~60 строк каждая — локаторы по ролям + testid на 12 ключевых элементах — договор с фронтендом); (3) 14 тестов критических путей: регистрация+логин, поиск→товар→корзина→оплата (успех), оплата-отклонение карты (мок платёжного API через page.route — отказ и 500 — UI-ошибки проверены без реальной платёжки), восстановление пароля (письмо — локальный SMTP (перехват) — код из ящика в тесте), админ-сценарий (создание товара → виден в каталоге); (4) CI — джоб на каждый PR (playwright-образ, 4 workers — 6 минут; трейсы артефактами — retries=1); (5) flaky-мониторинг: за первый месяц — 2 flaky (общий тестовый товар в двух админ-тестах — гонка параллели — свои фабрики; таймаут на медленном CI-раннере — ожидание загрузки поднято) — оба вылечены за день, с тех пор 99.4% прогонов зелёные без ретраев. Итог: регресс-чек перед релизом — 6 минут вместо 2 часов, «сломанная оплата» ловится на PR (поймали дважды за квартал — оба раза баги интеграции после рефакторинга корзины); поддержка набора — ~2 часа в месяц (правка локаторов после двух редизайнов — PO окупился: правки в 4 файлах, не в 14 тестах).
FAQ
Playwright против Selenium? Selenium — ветеран (протокол WebDriver, язык-агент шире, грид-инфраструктуры зрелые), но без встроенных ожиданий/авто-ретраев (flaky-борьба — на вас), медленнее (WebDriver-hop), мультибраузерность через драйверы; Playwright — один движок на браузеры (CDP/свои протоколы — быстрее), auto-waiting/трейсы/моки сети/мобильная эмуляция из коробки, pytest/jest-интеграция родная; новый проект — Playwright почти всегда; легаси-Selenium с работающим набором — миграция по мере надобности (полный перенос — месяцы, окупается при активной разработке).
Нужен ли Cypress-подобный «тест раннер внутри браузера»? Playwright включает раннер-интеграции (pytest для Python, свой @playwright/test для JS — параллель/ретраи/трейсы); «внутрибраузерность» Cypress — историческое отличие, у Playwright модель «извне через CDP» — параллельность/мультивкладки/мультитабы без ограничений; для Python-команды pytest-связка (тот же раннер, что юниты — фикстуры общие) — минимальный когнитивный налог.
Как тестировать e2e, если бэкенд ещё не готов? Контрактные моки: page.route() отдаёт ответы по OpenAPI-схеме (фикстуры-примеры из контракта — тесты пишутся параллельно разработке — контракт-первый подход); при готовности бэкенда — те же тесты переключаются на реальный API (env-флаг «мок/реал» — набор один); риск расхождения мока и реализации — снимается контрактными тестами API-слоя отдельно (валидация — схемы общие для моков и сервера — single source of truth).
Стоит ли снимать видео всех тестов «на всякий случай»?
Нет: видео/трейсы каждого прогона — гигабайты артефактов и замедление CI (запись — накладные); правильно — trace on-first-retry/screenshot only-on-failure (артефакты только там, где разбираем) + видео точечно при отладке конкретных сценариев; «полная запись всегда» — из эпохи отладки без трейсов — трейс-вьюер (DOM+сеть+консоль покадрово) информативнее видео на порядок.
Источники: документация Playwright (intro, locators, test runners) — ссылки в шапке.