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 от лишних трат и ложных срабатываний.

Идеальный пайплайн:

  1. Auth Setup: Сначала авторизуемся и сохраняем состояние (storageState).

  2. Health Check: Пингуем окружение и API (используя токен из Auth).

  3. 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 мы разделяем ответственность:

  1. Для действий (Движение): Используйте getByTestId. Нам не важно, написано на кнопке «Купить» или «Add to Cart». Тест не должен падать от смены перевода. Например, page.getByTestId('add-to-cart-button')

  2. Для проверок (Доступность и Смысл): Если вы проверяете заголовок или текст ошибки — используйте getByRole или локализацию. Если заголовок должен быть «Личный кабинет», а тест нашел data-testid="page-title" и успокоился, хотя тайтл пустой — это ложноположительный результат. Например, expect(page.getByRole('heading')).toHaveText(ru.personal_cabinet)

Ситуация

Рекомендация

Кнопка входа

page.getByTestId('login-submit').click()

Счётчик товаров

expect(loc).toHaveText('3')

Заголовок страницы

expect(page.getByRole('heading')).toHaveText(ru.title)

Поле ввода Email

page.getByLabel('Email').fill('...')

ВАЖНО:

Зачем это всё?

Потому что переучивать команду и переписывать сотни тестов с «плохих» локаторов на правильные в 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 (ждет)

Видимость

await loc.isVisible()

await expect(loc).toBeVisible()

Текст

await loc.textContent() == '...'

await expect(loc).toHaveText('Купить')

Количество

await loc.count()

await expect(loc).toHaveCount(3)

Атрибут

 await loc.getAttribute('href')

await expect(loc).toHaveAttribute('href', '/qa')

Чекбокс/Радио

await loc.isChecked()

await expect(loc).toBeChecked()

Состояние

await loc.isEnabled()

await expect(loc).toBeEnabled()

ЗАМЕТКА:

Почему это важно: Методы слева делают ровно один запрос к 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-ом.

Практические советы:

  1. Обязательно сопровождайте комментарием: Если вы реально вынуждены применить force: true (например, для скрытого <input type="file">), напишите комментарий, почему.

  2. Ищите причину: Если тест падает без force, значит страница нестабильна или элемент реально перекрыт. Лучше узнать причину, но понимаю, что на горящих сроках это не всегда возможно.

  3. 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)

page.$(), page.$$()

getByRole(), getByLabel(), getByTestId()

Используют ленивый поиск и автоматический перезапуск при ассертах.

Ожидание

 waitForSelector(), waitForTimeout()

Не нужно (встроено в действия)

Playwright сам ждет готовности (Actionability) перед кликом/вводом.

Видимость

isVisible(), toBeTruthy()

await expect(loc).toBeVisible()

Ассерт опрашивает DOM до наступления события или таймаута.

Навигация

waitForNavigation()

await expect(page).toHaveURL()

toHaveURL имеет встроенный polling, исключая Race Condition.

Отладка

console.log('HERE')

Trace Viewer

Машина времени с логами, сетью и слепками DOM прямо из CI.


Итог: Шпаргалка частых флаков

Симптом

Вероятная причина

Решение

Клик есть, эффекта нет

Гидратация / Элемент перекрыт

Ожидание маркера готовности или force: true, если элемент перекрыт

Таймаут в CI, проходит локально

Медленная сеть / Аналитика

Заблокировать мусор (Правило 4), fulfill(200)

Селектор не найден после релиза

Хрупкий CSS / Текст изменился

data-testid или getByRole с регуляркой


Что дальше?

Мы заложили фундамент стабильности. Но флаки часто прячутся глубже: в асинхронности и архитектуре данных. В следующей части мы перейдем к масштабированию: зачем нужны фикстуры, как изолировать данные в параллельных воркерах и почему ваш проект — это «живая документация».

BDR-челлендж

Попробуйте применить эти 5 правил на своём проекте прямо сегодня. Заблокируйте лишнюю сеть, замените пару хрупких CSS-локаторов на getByRole и проверьте, насколько стабильнее стал прогон в CI.

Делитесь в комментариях, какие из этих правил кажутся вам наиболее спорными?


Полезные ссылки

Весь этот подход и архитектурные решения в коде реализованы в Reference Implementation на GitHub. Вы можете склонировать его, запустить тесты и своими глазами увидеть, как BDR превращает код в живую документацию.