TL;DR для тех, кто ценит время:
1. Используйте Dependency Projects вместо
globalSetup.2. Приоритет локаторов:
getByRole>getByLabel>getByTestId. CSS — только в крайнем случае.3. Забудьте про
isVisible()в ассертах. Только Web-first (ретраящие) проверки.4. Блокируйте маркетинговый мусор (метрики, чаты) через
page.route.5. Трейсы (Trace Viewer) — это база. Скриншоты не лечат причину.
Философия BDR: Тесты как контракт
Если фундамент кривой, никакие «умные» репортеры не спасут вас от падения тестов. В основе этого цикла статей лежит методология BDR (Behavior-Driven Living Requirements — подход, при котором автотесты превращаются в живую, структурированную документацию.
Начнем.
СОВЕТ:
Важный нюанс про стоимость:
Все описанные в этом цикле практики (Dependency Projects, правильные локаторы, web-first ассерты) внедряются за часы, а не дни.
Если на проекте планируется больше 100 тестов, заложите эти принципы в фундамент сейчас. Это обойдется в 2–4 часа работы инженера, но сэкономит десятки часов в месяц на дебаг флаков и простои CI. ROI здесь бесконечный.
ВНИМАНИЕ:
Когда бюджет горит ярче, чем тесты
Понедельник, утро. Вы запускаете полный регресс из 1000 тестов. Через 40 минут выясняется, что стейджинг «прилег» еще ночью. Итог: сотни красных алертов, бесполезно сожженные минуты CI и ноль полезной информации.
Правило №1: Chained Dependencies (Граф иммунитета)
В профессиональной архитектуре тесты не запускаются «в вакууме». Мы строим граф зависимостей, который защищает CI от лишних трат и ложных срабатываний.
Идеальный пайплайн:
Auth Setup: Сначала авторизуемся и сохраняем состояние (
storageState).Health Check: Пингуем окружение и API (используя токен из Auth).
UI Tests: Запускаем основные тесты только если первые два этапа прошли.
Почему это важно?
Если у вас упал сервер авторизации или нестабильна среда, Плейрайт не будет запускать 1000 ваших тестов. Он упадет на одном из первых двух этапов, сэкономив вам 40 минут времени CI и сотни мусорных алертов.
Как это настраивается в playwright.config.ts:
export default defineConfig({ projects: [ // 1. Проект авторизации (создаем .auth/user.json) { name: 'auth-setup', testMatch: /.*\.auth\.setup\.ts/, }, // 2. Хелсчек, который ждет завершения авторизации { name: 'healthcheck', testMatch: /.*\.health\.setup\.ts/, dependencies: ['auth-setup'], }, // 3. Основные тесты, зависящие от здоровья среды { name: 'chromium', use: { ...devices['Desktop Chrome'] }, dependencies: ['healthcheck'], }, ], });
Порядок в массиве не важен, Плейрайт сам построит граф: auth-setup -> healthcheck -> chromium. Это «безопасная гавань» для тяжелых операций: сидирования БД, миграций или получения глобального токена. Вы гарантированно избежите Race Conditions, когда 50 потоков одновременно пытаются создать одного и того же пользователя.
Почему не globalSetup?
В отличие от старого подхода с одним глобальным файлом инициализации, Dependency Projects — это полноценные тесты. Это даёт две киллер-фичи для стабильности CI:
1. Trace Viewer: Если авторизация упала в CI, вы открываете трейс и видите всё: сеть, скриншоты, консоль. В
globalSetupу вас были бы только сухие логи.2. Изоляция и Fail-fast: Вы можете запустить только один проект
npx playwright test --project=auth-setup, чтобы быстро проверить состояние. А если сетап упадет — Playwright сразу остановит зависящие от него тесты, не сжигая лишние минуты CI.
Авторизация через API
В учебных проектах (как этот) мы часто делаем логин через UI — это наглядно и просто. Но в энтерпрайз-автоматизации 99% сетапов должны идти через API.
Почему это база:
1. Скорость: Прямой
POSTзапрос к/api/loginс последующим сохранением кук вstorageStateзанимает 50-100мс. UI-логин со всеми ассетами и рендерингом — от 2 до 5 секунд. В масштабе CI это экономит сотни долларов в месяц.2. Изоляция: Вы не нагружаете UI-форму логина лишними кликами. Для проверки логина у вас должен быть один честный UI-тест, а все остальные тесты просто «забирают» готовое состояние.
Пример «быстрого» сетапа:
test('setup', async ({ request }) => { > // 1. Выполняем POST-запрос для логина > await request.post('/api/login', { data: { user, pass } }); > > // 2. Сохраняем состояние (куки автоматически подхватятся из контекста request) > await request.storageState({ path: '.auth/user.json' }); > });
ВНИМАНИЕ:
Смерть в пятницу вечером
Вы запушили «идеальный» код в 18:00. В 18:05 фронтенд-разработчик поменял
.submit-btnна.btn-primary-new. Ваш тест падает, потому что «не нашел элемент». Вы тратите вечер на правку локаторов, которые даже не связаны с логикой.
Правило №2: Локаторы «здорового» инженера
Главная ошибка начинающего инженера — использовать локатор по тексту там, где он может измениться. Главная ошибка лида — использовать data-testid там, где текст является бизнес-требованием.
В BDR мы разделяем ответственность:
Для действий (Движение): Используйте
getByTestId. Нам не важно, написано на кнопке «Купить» или «Add to Cart». Тест не должен падать от смены перевода. Например,page.getByTestId('add-to-cart-button')Для проверок (Доступность и Смысл): Если вы проверяете заголовок или текст ошибки — используйте
getByRoleили локализацию. Если заголовок должен быть «Личный кабинет», а тест нашелdata-testid="page-title"и успокоился, хотя тайтл пустой — это ложноположительный результат. Например,expect(page.getByRole('heading')).toHaveText(ru.personal_cabinet)
Ситуация | Рекомендация |
Кнопка входа |
|
Счётчик товаров |
|
Заголовок страницы |
|
Поле ввода Email |
|
ВАЖНО:
Зачем это всё?
Потому что переучивать команду и переписывать сотни тестов с «плохих» локаторов на правильные в 100 раз дороже, чем сразу приучить себя к разделению ответственности.
Правило №3: Умные ассерты (Web-first assertions)
Большинство флаков рождается из-за попытки прочитать состояние элемента «здесь и сейчас». Метод .isVisible() возвращает мгновенный результат в миллисекунду вызова. Если страница «моргнула» на 10 мс позже, ваш тест падает с ошибкой.
Антипаттерн:
const isVisible = await page.getByRole('button').isVisible(); expect(isVisible).toBeTruthy(); // НИКОГДА ТАК НЕ ДЕЛАЙТЕ
Используйте ассерты, которые ретраят проверку до победного (или таймаута). expect(locator).toBeVisible() — это не просто проверка, это встроенный цикл ожидания (Polling).
Плохие vs Хорошие ассерты: наглядный справочник
Чтобы вы всегда могли отличить «флакоопасный» ассерт от стабильного, держите шпаргалку. В ней слева — методы, которые не ждут (Snapshot), справа — их правильные аналоги со встроенным ожиданием.
Что проверяем? | Снапшот (не ждет) | Web-first assertion (ждет) |
Видимость |
|
|
Текст |
|
|
Количество |
|
|
Атрибут | |
|
Чекбокс/Радио |
|
|
Состояние |
|
|
ЗАМЕТКА:
Почему это важно: Методы слева делают ровно один запрос к DOM в момент вызова. Если страница ещё не дорисовалась или данные подгружаются, они вернут
falseилиundefined, и тест упадёт. Методы справа запускают цикл проверок (обычно каждые 100 мс) в течение таймаута: они дождутся нужного состояния и только тогда продолжат тест. Это делает ваши тесты устойчивыми к гонкам состояний (Race Conditions): они не упадут, если страница «моргнула» на 10 мс дольше обычного.
СОВЕТ:
> Исключение: Иногда
await page.isVisible()может пригодиться внутри условной логики (например, чтобы решить, кликать ли по кнопке). Но применение условной логики в тестах должно быть обоснованным и минимальным. А для финальных проверок используйте Web-first ассерты (как в таблице выше).
Правило №4: Ловушка гидратации и политика force: true
Вы видите в отчете, что Playwright кликнул по кнопке. Ошибок нет, но приложение не отреагировало. Это Hydration Error.
Почему это происходит (Архитектурная справка)
В современных подходах типа SSR (Server-Side Rendering) или SSG (Static Site Generation) сервер присылает браузеру уже готовую HTML-вёрстку. Пользователь (и Playwright) видит кнопку мгновенно. Но это «мертвая» кнопка, в ней нет JavaScript-обработчиков.
Процесс «оживления» этой вёрстки называется Hydration (гидратация). Браузер должен скачать бандл, распарсить его и «навесить» события на элементы. В этот короткий промежуток (от долей секунды до секунд на слабом железе или очень медленном интернете) страница выглядит живой, но не реагирует на клики. Плейрайт, будучи очень быстрым, часто попадает именно в это «окно смерти».
Про Qwik и будущее
Для контраста стоит упомянуть Qwik. Там придумали Resumability-подход, который практически убивает гидратацию. Странице не нужно «просыпаться», она готова к работе сразу. Если ваше приложение на Qwik, проблем с «холодными кликами» будет на порядок меньше. Но пока большинство сидит на React/Vue (Next.js/Nuxt), гидратация остается главным врагом стабильности тестов.
Как это лечить:
Ищите «маркер готовности». Это может быть исчезновение глобального лоадера или появление специфического класса в DOM (например, от библиотеки hydration-overlay).
Пример ожидания:
// Ждем, пока JS «оживит» наше приложение await page.waitForSelector('.hydrated', { state: 'attached' }); // Или более универсальный способ — ждем скрытия лоадера await expect(page.locator('#global-loader')).toBeHidden();
Политика использования force: true:
Параметр force: true отключает проверки Playwright на кликабельность (Actionability). В 99% случаев это маскировка проблемы, а не её решение.
Техническая справка: Цена за
force: trueКогда вы ставите
force: true, вы приказываете Playwright проигнорировать свой фирменный Actionability Check. Вы добровольно отказываетесь от проверки на:
Visible: Элемент может быть скрыт от пользователя.
Stable: Элемент может летать по экрану (анимации), и мы кликнем «в молоко».
Enabled: Кнопка может быть
disabled(серый цвет), но мы всё равно пошлем на неё событие клика.Receiving Events: Кнопка может быть перекрыта лоадером или рекламным баннером.
Используя
force, вы превращаете свои тесты (эмуляцию действий пользователя) в скрипт манипуляции DOM-ом.
Практические советы:
Обязательно сопровождайте комментарием: Если вы реально вынуждены применить
force: true(например, для скрытого<input type="file">), напишите комментарий, почему.Ищите причину: Если тест падает без
force, значит страница нестабильна или элемент реально перекрыт. Лучше узнать причину, но понимаю, что на горящих сроках это не всегда возможно.Dispatch Event: Помните, что
force: trueпереводит клик из режима эмуляции мыши в режим программного события браузера.
Правило №5: Сетевая гигиена — режем мусор
Ваше приложение тянет Яндекс.Метрику и чаты поддержки? Эти сервисы часто тормозят, валя ваши тесты.
Решение: Блокируйте или мокайте всё лишнее.
// Блокируем метрику, чтобы она не тормозила наши тесты await page.route(/yandex\.ru|google-analytics\.com/, (route) => { // Используем fulfill вместо abort, чтобы приложение получило статус 200 "OK" // и не «зависло» в бесконечных ретраях. route.fulfill({ status: 200, body: 'ok' }); });
ВАЖНО:
Осторожно со шрифтами: Блокировка внешних шрифтов может вызвать Layout Shift (скачок текста), из-за чего Actionability Check на стабильность (
Stable) может срабатывать дольше обычного.
ВНИМАНИЕ:
Гадание по скриншотам
Тест упал, в логах — таймаут, на скриншоте — просто страница. Что случилось за 2 секунды до падения? Какая ошибка в консоли? Какой запрос в сеть завис? Пытаться чинить сложные флаки по одному скриншоту — это как лечить зубы по фотографии.
Правило №6: Trace Viewer вместо «посмертных записок»
На скриншоте страница выглядит идеально, кнопка на месте. Но в Trace Viewer на вкладке Actionability вы увидите не просто ошибку, а «приговор» от Playwright: он прямо назовет селектор элемента, который перекрыл кнопку (например, div.loading-skeleton). Откройте вкладку Snapshots и там вы увидите красную точку «прицела» и тот самый невидимый оверлей, который поймал ваш клик.
Настройка для CI
Чтобы не захламлять артефакты гигабайтами ненужных логов, используйте стратегию «Трейс только при падении». Это сэкономит место и оставит только важные данные:
// playwright.config.ts > use: { > // Сохраняем трейс только при первом ретрае упавшего теста > trace: 'retain-on-failure', > screenshot: 'only-on-failure', > }

Выжимаем максимум из Trace Viewer
Трейс — это не просто «запись экрана», это полноценная машина времени для вашего теста.
1. Вкладка Metadata: Посмотрите, с какими параметрами запустился браузер, какое было разрешение экрана и версия Playwright. Часто ответ на вопрос «почему в CI падает» кроется именно в viewport size.
2. Кнопка Context: Откройте Console и Network прямо внутри трейса. Вы можете кликнуть на любой сетевой запрос и увидеть заголовки, Payload и Response.
3. Режим Snapshots (Action/Before/After):
Action: состояние в момент выполнения.
Before: до начала действия (например, до того как поп-ап закрылся).
After: результат (например, кнопка стала
disabled).
Это критично для поиска «мертвых» кликов. Если на Action кнопка перекрыта, вы увидите это красной точкой.
4. Интерактивный DOM: Снапшот — это не просто картинка, а живой слепок страницы. Вы можете открыть DevTools прямо внутри трейса (кликнув на снимок) и исследовать дерево элементов, стили и атрибуты. Это позволяет инспектировать «прошлое», которого больше нет в живом браузере. Вы буквально можете изучить состояние страницы в ту миллисекунду, когда происходило действие.

Культура и Линтеры: Прагматичный подход
Важно не просто писать тесты, но и создавать систему, которая не дает команде ошибаться. Но важно не перегнуть палку. Если линтер будет бить по рукам на каждый «чих», его начнут отключать.
Стратегия уровней строгости:
Error: Для «смертных грехов», которые гарантированно ломают логику или процесс (забытый
await, оставленная пауза в CI).Warn: Для «архитектурного долга». Это сигнал: «Друг, здесь можно лучше, но если у тебя Ant Design и по-другому никак, живи с этим».
// .eslintrc.js module.exports = { plugins: ['playwright'], rules: { // ERROR: Для «смертных грехов» (сборка должна падать) 'playwright/missing-playwright-await': 'error', 'playwright/no-wait-for-timeout': 'error', // WARN: Для архитектурного долга (подсвечивается в IDE) 'playwright/prefer-web-first-assertions': 'warn', 'playwright/no-force-option': 'warn', }, };
СОВЕТ:
Когда правила нужно нарушать?
Если вы работаете с тяжелыми библиотеками компонентов, где селекторы генерятся динамически, иногда
// eslint-disable-next-line— это единственный способ выжить. Главное, чтобы это было осознанное решение, а не случайный флак.
Playwright Lead QA Cheat Sheet
Чтобы вам не пришлось собирать знания по всей статье, вот единая шпаргалка. Это ваш стандарт качества.
Категория | Legacy | Playwright Way | Механика |
Поиск (Locators) |
|
| Используют ленивый поиск и автоматический перезапуск при ассертах. |
Ожидание | | Не нужно (встроено в действия) | Playwright сам ждет готовности (Actionability) перед кликом/вводом. |
Видимость |
|
| Ассерт опрашивает DOM до наступления события или таймаута. |
Навигация |
|
|
|
Отладка |
| Trace Viewer | Машина времени с логами, сетью и слепками DOM прямо из CI. |
Итог: Шпаргалка частых флаков
Симптом | Вероятная причина | Решение |
Клик есть, эффекта нет | Гидратация / Элемент перекрыт | Ожидание маркера готовности или |
Таймаут в CI, проходит локально | Медленная сеть / Аналитика | Заблокировать мусор (Правило 4), |
Селектор не найден после релиза | Хрупкий CSS / Текст изменился |
|
Что дальше?
Мы заложили фундамент стабильности. Но флаки часто прячутся глубже: в асинхронности и архитектуре данных. В следующей части мы перейдем к масштабированию: зачем нужны фикстуры, как изолировать данные в параллельных воркерах и почему ваш проект — это «живая документация».
BDR-челлендж
Попробуйте применить эти 5 правил на своём проекте прямо сегодня. Заблокируйте лишнюю сеть, замените пару хрупких CSS-локаторов на getByRole и проверьте, насколько стабильнее стал прогон в CI.
Делитесь в комментариях, какие из этих правил кажутся вам наиболее спорными?
Полезные ссылки
Весь этот подход и архитектурные решения в коде реализованы в Reference Implementation на GitHub. Вы можете склонировать его, запустить тесты и своими глазами увидеть, как BDR превращает код в живую документацию.
Playwright BDR Template — github.com/dmitryAQA/playwright-bdr-template
BDR Methodology Manifesto — github.com/dmitryAQA/bdr-methodology
