Когда UI-тест флакует, команда часто увеличивает ожидание и получает более медленный флак. Проблема не в том, что секунда «слишком короткая»: секунды не описывают момент, когда пользовательский сценарий готов. Цена — ложная уверенность в зелёном CI и потеря времени при следующем timeout, потому что исходное наблюдение так и не было сформулировано.
Ниже — полевой маршрут без обещаний о чужом продукте. Он использует локальную модель с явными состояниями и отдельный rollback. Это не собранный trace, не прогон Playwright и не статистика флаков. Его назначение скромнее: разобрать один тест так, чтобы reviewer мог проверить связь между действием, наблюдением и условием завершения.
Соберите симптом без ярлыка «медленно»
Возьмите падение и выпишите буквально: какое действие выполнено, какая текущая проверка стоит после него, какой элемент или текст она читает, какая пауза есть в коде. Не добавляйте объяснение «бекенд медленный», пока нет evidence. Часто оказывается, что assertion ждёт визуальную деталь, а не результат; либо submit можно выполнить дважды; либо ошибка спрятана за общим spinner. Эти ситуации отличаются исправлением, хотя все выглядят как timeout.
| Наблюдение | Вероятная дырка contract | Локальная проверка | Первое изменение |
|---|---|---|---|
| sleep после submit | нет именованного pending state | fixture: submit даёт submitting | добавить status ожидания |
| сразу ждём success | пропущено промежуточное владение | fixture: request id существует до ack | проверить pending до финала |
| два click дают два эффекта | нет guard | fixture: второй submit blocked | запретить transition в submitting |
| поздний ответ закрывает новую форму | нет correlation | fixture: stale ack ignored | связать ack с request id |
| после ошибки нечего повторить | нет recovery state | fixture: missing ack даёт alert | сохранить текст и назвать действие |
Разбор по одному сценарию
Представим форму с текстовым полем. После click старый тест спит и затем ищет успех. Новый разбор вводит текст, ожидает status `submitting`, подаёт контролируемое acknowledgement и только после него ждёт `saved`. Это не означает, что browser-тест обязан подделывать любой API через публичный UI: способ контроля ответа зависит от архитектуры стенда. Важно другое: контроль должен быть явным, а наблюдение — тем, которое обещает страница.
Если confirmation не приходит, contract не показывает success «на всякий случай». Он переходит в `recovery-required` и сохраняет text. В реальном продукте причина может быть server error, отмена, сеть или нарушение протокола. Учебная модель не выбирает между ними. Она отказывается от ложного вывода и требует отдельного понятного пути: повторить по согласованной политике, проверить результат или вернуться к редактированию.
Выполните fixture как проверку модели
CLI ниже не обращается к приложению. Он проверяет четырнадцать assertions локального объекта: отсутствие браузера и таймера, именованный pending state, guard повторного submit, отклонение stale acknowledgement, recovery, сохранение черновика при rollback, новую последовательность request id и корректное final acknowledgement. Поэтому PASS нельзя прикладывать как доказательство прохождения e2e. Его можно использовать как воспроизводимый ответ на вопрос «что именно наша маленькая модель гарантирует».
node web/scripts/upgrade-2022-10.mjs --verify-fixture
# ожидаемый смысл PASS:
# локальная модель различает submitting, saved и recovery-required
# а не то, что страница, сеть или Playwright были запущены
После fixture полезен ручной review кода настоящей страницы. Найдите, где создан request id, кто запрещает второй submit, где появляется pending status, что маппит transport outcome в UI и кто очищает текст. Запишите эти пять мест в PR. Если ответ «это делает useEffect где-то ниже», тест пока остаётся хрупким: будущий рефакторинг может вернуть секунду, не меняя бизнес-логику.
Порядок внедрения в реальный тест
- Зафиксируйте старый сигнал. Сохраните текст падения и текущий assertion; не начинайте с увеличения timeout.
- Назовите наблюдаемое состояние. Выберите pending, success и recovery с понятными пользователю role/name.
- Добавьте unit-level transition checks. Проверяйте guard, correlation и rollback там, где живёт state, а не через sleep.
- Настройте контролируемый вход. В тестовом окружении выберите разрешённый способ получить success/failure; задокументируйте его отдельно.
- Поставьте assertion на condition. Инструмент может retry condition до timeout, но condition должен быть именованным state, а не прошедшим временем.
- Откатите честно. Если новый contract ломает согласованный UX, удалите и UI-state, и новый assertion одним изменением. Не оставляйте data-testid без владельца.
Граница технического evidence
Документация Playwright v1.27.0 подтверждает наличие retrying assertions, но не говорит, как устроен ваш state reducer. WebDriver Working Draft задаёт remote-control protocol, но не оценивает полезность текста «сохранено». WCAG 2.1 — не сертификат доступности этой страницы. Эти источники помогают поставить границы и не подменять одну область другой.
Чтобы утверждать, что реальный флак исчез, нужны новые evidence: commit, выбранное окружение, версия браузера и раннера, результаты повторяемых запусков, известные исключения. В этой статье их нет. Поэтому корректная формулировка результата после внедрения будет узкой: «тест теперь ждёт конкретный доступный state; условия прогона приложены отдельно». Это лучше, чем красивое, но непроверяемое «стало стабильно».
Типичные неверные исправления
Не заменяйте `wait(1000)` на `wait(5000)`: вы меняете стоимость, а не сигнал. Не ловите любой текст «Успех»: он может остаться от предыдущего шага. Не делайте assertion на private loading flag, если пользователь не может увидеть связанный результат. Не отправляйте второй request автоматически без domain policy. И не скрывайте error, чтобы тест увидел только счастливую ветку: именно error и recovery показывают, что condition действительно связан с переходом.
Отдельно опасен фальшивый rollback. Если тест очищает всё окружение, но UI-state откатывается не так, как продукт, проверка может проходить на чистом листе. В учебной fixture rollback делает простую вещь: возвращает snapshot переходов до submit. В продукте восстановление может потребовать router, store, cache и server state. Их надо тестировать отдельно, не расширяя значение маленького unit fixture.
Ограничение и следующий шаг
Модель не включает реальный DOM, framework, test runner, API, timeouts, локализацию или разрешения. Роль `status` и `alert` в ней — контрактные labels, а не готовая accessibility-реализация. request id и acknowledgement — ручные values. Никакой подсчёт флаков, длительности или production impact здесь не сделан.
Следующий шаг — провести один PR через эту карту: удалить одну бессодержательную паузу, добавить одно наблюдаемое состояние, покрыть guard и stale reply unit-level тестом, затем сделать один browser assertion. В описании PR оставьте границы модели и ссылку на реальный отчёт, если он запускался. Так изменение остаётся маленьким, проверяемым и обратимым.
Историческая граница октября 2022
В статье используются только проверяемые источники, доступные к октябрю 2022 года: Playwright v1.27.0 documentation commit от 7 октября, WebDriver Working Draft от 25 октября и WCAG 2.1 Recommendation. Они не подменяют проверку конкретной страницы, а fixture не является browser run.
Проверяемые источники
- Playwright: test assertions, commit af0d2936 (7 октября 2022) — неизменяемый исходный документ версии v1.27.0, опубликованной 7 октября 2022 года. Описывает retrying assertion до условия или timeout; статья не запускает Playwright.
- W3C WebDriver, Working Draft 25 октября 2022 года — датированный Working Draft, доступный в октябре 2022 года. Он задаёт remote-control interface, но не определяет смысл прикладного состояния страницы.
- W3C WCAG 2.1, Recommendation 5 июня 2018 года — нормативная Recommendation, доступная к октябрю 2022 года. Используется только для границы: видимое состояние нуждается в доступной семантике, а не в одном визуальном эффекте.