Тест нажимает кнопку «Оплатить», получает ошибку на первой попытке и проходит на повторной. В отчёте остаётся статус flaky, а в pull request появляется короткое предложение: поднять timeout и идти дальше. Проблема в том, что этим действием смешиваются четыре разных объекта: selector для действия, условие готовности интерфейса, retry тест-раннера и evidence из конкретного запуска. Пока они смешаны, команда лечит паузу, а не контракт.
Цена такой правки не сводится к лишним секундам CI. Реальная ошибка может стать «шумом»: повторный запуск проходит, скрывает первый отказ и откладывает разбор. Обратная цена тоже заметна: бесконечный trace на каждый тест раздувает артефакты, но не отвечает, какой результат должен увидеть пользователь. Здесь нужен короткий порядок: сначала назвать факт после действия, затем выбрать наблюдение, только потом решать, нужен ли retry и какой evidence сохранять.
Четыре объекта, которые нельзя называть одним ожиданием
Selector отвечает на вопрос «куда направить действие». Для Playwright 1.37.0 locator с role и name — это способ найти пользовательский элемент. Перед click() Playwright проверяет, что элемент attached, visible, stable, receives events и enabled. Эти проверки полезны: они не дают кликнуть в скрытый или перекрытый control. Но они не знают, завершилась ли оплата, сохранился ли профиль или пришёл ли пользовательский статус.
Readiness condition отвечает на другой вопрос: какой наблюдаемый продуктовый факт должен появиться после действия. Это может быть текст в role=status, появление записи в таблице или смена доступного пользователю состояния. Условие не должно быть «страница немного успокоилась» или «кнопка стала disabled», если бизнес-операция ещё не подтверждена. Retry запускает тест повторно после failure. Evidence — trace, snapshot, action log или другой артефакт отдельной попытки. Ни один из них не является синонимом selector или готовности.
| Слой | На какой вопрос отвечает | Что может подтвердить | Чего не подтверждает | Первое действие |
|---|---|---|---|---|
| Selector | Как найти control? | locator разрешается ровно в один ожидаемый элемент | что операция завершилась | использовать role/name или явный test id |
| Readiness condition | Какой факт увидел пользователь после действия? | конкретный status, текст, запись или состояние | что locator был уникален до click | сформулировать ожидаемый продуктовый результат |
| Retry | Что сделал runner после failure? | первая попытка не прошла, следующая прошла или нет | почему попытки различаются | сохранить статус первой и повторной попытки |
| Evidence | Что можно изучить в одном запуске? | действия, snapshots, log и network log, если они записаны | корневую причину без дополнительной проверки | связать артефакт с номером попытки |
| Timeout | Каков верхний предел ожидания? | когда ожидание остановится ошибкой | какой факт надо дождаться | не менять, пока не назван readiness contract |
Сначала формулируем результат после click
Практический контракт выглядит короче, чем кажется. После нажатия нужно назвать одно состояние, которое экран обязан показать. Например: «платёж подтверждён», а не «кнопка уже недоступна». Если интерфейс не имеет такого состояния, это не повод ждать произвольную паузу. Это повод вместе с владельцем экрана выбрать доступный сигнал: status region, итоговую строку, новый route или ответственный элемент в UI. Контракт должен быть проверяемым пользователем, а не внутренним CSS-классом без смысла вне реализации.
Ниже — проектный образец. getByRole() выбирает кнопку, а toHaveText() ждёт текст результата. Web-first assertion в Playwright умеет повторять проверку до timeout; это не означает, что любой текст подходит. Значение confirmed здесь специально условное: в своём продукте его заменяют на реальный, устойчивый и доступный пользователю результат. Фрагмент не доказывает, что страница, backend или browser уже проверены.
// Иллюстрация контракта: locator выбирает действие, expect проверяет результат.
import { expect, test } from '@playwright/test';
test('подтверждает оплату', async ({ page }) => {
await page.getByRole('button', { name: 'Оплатить' }).click();
await expect(page.getByTestId('payment-state')).toHaveText('confirmed');
});
// getByRole — selector. toHaveText — readiness condition.
// Текст и test id должны соответствовать контракту конкретного продукта.
Sleep или network idle без связи с результатом делают запуск длиннее, но не объясняют, что делать при ошибке экрана после успешного запроса. Actionability делает действие допустимым, readiness делает завершение сценария наблюдаемым. Тогда timeout — параметр договора, а не способ спрятать расхождение.
Учебный fixture: только synthetic попытки в памяти
Пакет содержит исполнимую модель, но не e2e-запуск. Она создаёт две synthetic попытки: первая не достигает payment-state=confirmed, вторая достигает его после retry. Selector в обеих уникален, поэтому fixture отделяет readiness от selector и retry. Он не открывает URL, не читает тест, не запускает Playwright и не производит trace.zip или video.
node web/scripts/upgrade-2023-08.mjs --verify-fixture
# Команда детерминированно создаёт marked synthetic attempts только в памяти.
# Она не открывает браузер, не читает проект, не запускает Playwright,
# не записывает trace/video, не измеряет duration и не проверяет compatibility.
# PASS проверяет разделение selector, readiness, retry и synthetic evidence.
# PASS не означает, что настоящий тест flaky, что причина найдена или что UI готов.
PASS проверяет шестнадцать утверждений: вход помечен как memory-only, браузер не запускается, первая и повторная попытки различаются ровно теми полями, которые заданы в модели, а увеличение timeout отклоняется учебным планом. Отдельная отрицательная ветка делает selector множественным и получает другую классификацию. Это важно: даже одинаковый финальный статус «flaky» не даёт права без проверки переписать locator, readiness condition и retry policy одной правкой.
Маршрут: симптом → причина → проверка → действие
- Симптом. Первый запуск failed, retry passed, а отчёт пометил тест flaky. Сохраните номер попытки, исходный текст ошибки и ссылку на evidence, если ваш runner его записал. Не называйте это доказанной причиной.
- Причина. Обычно в одном ожидании оказались selector, техническая готовность control и результат операции. Иногда к ним добавляют общий timeout, поэтому ошибка становится длиннее, но не яснее.
- Проверка selector. Убедитесь, что locator на обеих попытках разрешается ровно в один пользовательский элемент. Если нет, это отдельная задача: сузить role/name, scope или test id; не менять пока бизнес-assert.
- Проверка readiness. Выпишите факт, который обязан увидеть пользователь после действия. Сверьте, что assertion ждёт именно его, а не disabled button, исчезновение spinner или окончание произвольной паузы.
- Проверка retry и evidence. Сопоставьте initial и retry как два запуска. Для Playwright 1.37.0 retry выполняется в новом worker; trace с
on-first-retry, если он настроен, относится к первой повторной попытке. Он помогает задать вопрос, но не возвращает автоматически evidence первого отказа. - Действие. Внесите минимальную правку в один слой: locator, semantic assertion, изоляцию данных или явный проектный контракт. Не повышайте timeout, пока не можете назвать, что именно должно стать ready.
- Rollback. До merge запишите прежний assertion и критерий возврата. Если новый сигнал оказался неверным, откатите только test diff и повторите разбор; не стирайте историю первой неудачной попытки комментарием «пофиксили flaky».
Retry полезен как граница сбора evidence, а не как индульгенция
В Playwright retries выключены по умолчанию. При включении runner повторно запускает упавший тест; документация v1.37.0 называет flaky тот случай, когда первая попытка не прошла, а повторная прошла. Это удобный сигнал для очереди разбора, но не диагноз. Повтор может дать другой worker и чистое состояние, а значит скрыть утечку тестовых данных, зависимость от порядка или неявную готовность экрана. Он не доказывает, что первая ошибка была случайной, внешний сервис был медленным или selector корректен.
Конфигурация trace: on-first-retry привязывает артефакт к первому повтору, а не ко всей истории. Это evidence retry, не объяснение initial failure. Если нужен контекст первого отказа, проекту требуется отдельный способ его сохранить с учётом чувствительности данных и цены артефактов.
// Иллюстрация для Playwright v1.37.0; этот фрагмент не запускается fixture.
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 1 : 0,
use: { trace: 'on-first-retry' },
});
// retry сохраняет evidence первого повторного запуска.
// Он не заменяет явное условие готовности после click.
Ограничения и следующий проверяемый шаг
Заметка не измеряет flake rate, не сравнивает браузеры, не обещает устойчивость любого getByRole() и не предлагает общий timeout. Trace фиксирует один запуск, а причина может лежать в приложении, сети, окружении или данных. Рамка ограничена Playwright v1.37.0 от 10 августа 2023; другую версию проверяют по её документации.
Следующий шаг — взять один тест со статусом flaky и заполнить короткую карточку из пяти строк: selector, readiness condition, initial outcome, retry outcome и доступный evidence. Затем выбрать один слой для изменения и указать rollback. Если карточку нельзя заполнить без догадок, не увеличивайте timeout. Сначала добавьте недостающий наблюдаемый результат или изоляцию данных. Так retry перестаёт превращать ошибку в шум и становится точкой, где начинается инженерный разбор.
Проверяемые источники
- Playwright v1.37.0: официальный GitHub release, 10 августа 2023 — версионный срез, опубликованный до конца августа 2023. Он фиксирует выпуск, но не подтверждает версию, браузеры или настройку конкретного проекта.
- Playwright v1.37.0: Auto-waiting, исходник официальной документации по тегу — для click описаны проверки attached, visible, stable, receives events и enabled. Эти проверки готовят действие с элементом, но не доказывают бизнес-результат после действия.
- Playwright v1.37.0: Retries, исходник официальной документации по тегу — описывает повторный запуск после сбоя, новый worker и статусы passed, flaky, failed. Статус flaky — классификация результата запусков, а не причина сбоя.
- Playwright v1.37.0: Trace Viewer, исходник официальной документации по тегу — описывает действия, snapshots, action log, source и network log; режим on-first-retry записывает trace при первом retry. Такой артефакт — evidence одного запуска, а не доказательство корневой причины.
- Playwright v1.37.0: Best Practices, исходник официальной документации по тегу — рекомендует изоляцию тестов, пользовательские locator и web-first assertions. Рекомендация не заменяет проверку реального контракта конкретного экрана.