У e2e-теста часто один большой timeout и один текст ошибки, хотя внутри живут четыре независимых контракта. Locator должен найти ровно тот control, auto-wait должен сделать действие допустимым, продуктовый assertion должен дождаться результата, а retry должен сохранить факт повторного запуска. Когда все четыре слоя названы словом «ожидание», падение становится непонятным: инженер видит TimeoutError, но не знает, кнопка не нашлась, была перекрыта, результат не наступил или retry изменил исходные условия. Цена ошибки — замедлить CI и оставить flaky-тест без причины.
После первой удобной правки команда повышает timeout, затем добавляет retry. Часть ошибок превращается в длинные flaky, CI медленнее, а trace не связан с гипотезой. Вместо этого один тест раскладывают на четыре контракта: у каждого свой вопрос, evidence, владелец изменения и rollback.
Контракт действия не равен контракту результата
В Playwright 1.37.0 auto-wait перед click() проверяет набор actionability conditions. Для click это attached, visible, stable, receives events и enabled. Этот механизм решает узкую задачу: не отправить действие в элемент, который исчез, невидим, движется, перекрыт или disabled. Он не может узнать смысл вашей операции. Кнопка может быть полностью ready для click, но сервер вернёт отказ, клиент покажет validation error или асинхронное подтверждение не появится.
Поэтому после действия нужен второй контракт — readiness. Он принадлежит сценарию, а не библиотеке. Для оплаты это может быть подтверждённый статус; для сохранения профиля — видимое сообщение и обновлённое значение; для импорта — запись с терминальным состоянием. Условие должно быть достаточно узким, чтобы отделить success от промежуточного состояния, и достаточно пользовательским, чтобы пережить смену внутренней разметки. Если product UI не предоставляет такого сигнала, тест обнаруживает не только техническую проблему, но и недостающую наблюдаемость интерфейса.
| Контракт | Владелец смысла | Пример проверки | Негативный сигнал | Недопустимая подмена |
|---|---|---|---|---|
| Selector | автор теста и контракт доступной разметки | role/button с name или test id в нужном scope | 0 или несколько совпадений | считать уникальный selector подтверждением операции |
| Actionability | библиотека и состояние DOM перед действием | visible, stable, receives events, enabled для click | элемент не готов принять действие | ждать бизнес-успех только потому, что click допустим |
| Readiness | продуктовый сценарий и UI-контракт | status, итоговая строка, terminal state | после действия нужный факт не наблюдается | проверять disabled/spinner вместо результата |
| Retry | test runner и политика CI | номер попытки, outcome, новый worker | failed → passed или failed → failed | объявлять retry объяснением причины |
| Trace evidence | артефакт конкретной попытки | actions, snapshots, log, source, network log при записи | evidence отсутствует или относится к другому запуску | выдавать артефакт за root-cause analysis |
Auto-wait полезен, но у него есть предел
Auto-wait заменяет ручной sleep ожиданием технической допустимости target. Это снижает зависимость от отрисовки, но не делает интерфейс «готовым»: готов только target для одного действия. Синхронизация с сервером и итоговый статус лежат за границей actionability.
Уникальный locator с role/name находит пользовательский объект, но не является селектором сетевого ответа и не подтверждает update store. Assertion должен отвечать на вопрос «что увидит человек?», а не цепляться за внутренний CSS, который сломается при рефакторинге.
// Иллюстрация контракта: 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 должны соответствовать контракту конкретного продукта.
Фрагмент не запускается вместе с fixture и не даёт готовый селектор для чужого приложения. Его задача — показать роль строк: getByRole() указывает действие, toHaveText() описывает постусловие. В более сложном сценарии readiness может состоять из нескольких согласованных фактов, но это не повод прятать их под waitForTimeout(). Лучше назвать один терминальный state, а дополнительные поля проверять отдельными assertions с понятным сообщением об ошибке.
Retry меняет исполнение, но не причинность
Документация Playwright 1.37.0 описывает retry через worker process: после failure runner отбрасывает worker с браузером, а при включённом retry новый worker начинает с повторной попытки упавшего теста. Это важная техническая деталь для разбора. Повтор получает изолированный контекст исполнения и может не унаследовать причину, которая была в первой попытке. Значит, переход failed → passed — наблюдение о двух запусках, а не доказательство случайности, исправления или совместимости браузера.
Flaky — повод открыть карточку: фиксированные входы, expected readiness, selector, initial outcome и evidence retry. Общая запись или порядок набора могут исчезнуть в чистом retry context. Тогда правят данные или изоляцию, а не selector и timeout.
Trace — evidence с известной областью действия
Trace Viewer версии 1.37.0 умеет показывать список действий, snapshots, action log, source и network log для записанного trace. Это хороший материал для вопроса «что происходило в этой попытке?». Например, можно заметить, что assertion начался до появления ожидаемого state, или что click пришёлся в другой control. Но нельзя перескочить от одного кадра к слову «причина». Trace не знает ожиданий бизнеса, а сетевой лог не доказывает, что серверная операция эквивалентна пользовательскому успеху без контракта UI.
Режим on-first-retry записывает trace при первом retry, поэтому evidence относится к повторной попытке. Он не восстановит initial failure. План диагностики сначала называет вопрос к artefact: selector, порядок action, видимый state или событие одного запуска.
Маршрут: симптом → причина → проверка → действие
- Симптом. Тест заканчивается timeout либо статусом flaky. Сначала запишите фазу: поиск locator, click, assertion результата или повторный запуск. Одно слово «упал» слишком мало для изменения.
- Причина. Определите, какой контракт нарушен: cardinality selector, actionability control, readiness condition, изоляция данных или политика retry. Не выводите причину из длительности, если нет отдельного измерения.
- Проверка selector и actionability. Сверьте user-facing locator и cardinality. Перекрытый или disabled элемент — ещё не проверка результата операции.
- Проверка readiness. Запишите terminal state и наблюдаемый элемент. Замените assertion побочного индикатора на web-first assertion пользовательского факта.
- Проверка retry. Сопоставьте initial и retry как разные worker contexts. Выясните, какие данные, настройки или cleanup не были частью контракта первой попытки.
- Проверка evidence. Если проект записал trace, проверьте, к какой попытке он относится и какой вопрос может сузить. Не заявляйте, что просмотрели trace, пока артефакт не предоставлен.
- Действие и rollback. Сделайте один малый diff и сохраните прежний assert либо config diff. Если contract не подтверждается, верните его и соберите evidence, а не поднимайте timeout.
Учебная модель удерживает границы видимыми
Исполнимая часть sidecar не использует Playwright API. Она создаёт в памяти два marked synthetic outcome: initial failure с достигнутым selector и недостигнутым readiness, затем passed на first retry. Обе попытки обязаны нести одну и ту же synthetic retry-policy: иначе их нельзя сравнивать как один учебный сценарий. Функция возвращает классификацию retry-masked-missing-readiness-contract-in-synthetic-input. Длинное имя намеренно: оно не говорит «это настоящий flake», а сообщает, какую форму увидела модель. Вторая ветка делает selector множественным и получает отдельный результат; этим fixture не позволяет склеить два вида проблемы.
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 готов.
Учебный plan принимает только readiness condition, совпадающий с condition из evidence; он не может подменить payment-state=confirmed произвольным удобным признаком. Timeout и selector change без evidence отклоняются. Это ограничение модели, не правило для всех проектов. Rollback возвращает неприменённый synthetic plan и сообщает, что test code и runner config не менялись.
Ограничения модели и следующий шаг
Модель не запускает browser, не получает trace/video, не читает исходник, не знает CI, не измеряет duration и не выбирает domain condition. Она не оценивает реальную стабильность. Рамка — Playwright v1.37.0 от 10 августа 2023; другую версию сверяют отдельно.
Следующий шаг — написать для одного flaky теста компактный контракт из четырёх строк: user-facing locator, expected readiness, retry policy и evidence policy. Добавьте owner UI-сигнала и способ rollback тестовой правки. Если не удаётся сформулировать readiness без CSS или sleep, остановитесь и обсудите с владельцем экрана, какое состояние должен видеть пользователь. Этот разговор обычно дешевле, чем ещё один круг 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. Рекомендация не заменяет проверку реального контракта конкретного экрана.