Длинный E2E-сценарий - вход, наполнение корзины, оформление, оплата, проверка заказа - в отчёте выглядит как плоская стена действий, и когда он падает, непонятно, на каком этапе. Playwright даёт два инструмента навести здесь порядок: шаги, которые группируют действия в осмысленные этапы, и аннотации, которые управляют тем, как и когда тест выполняется. Вместе они превращают набор из свалки сценариев в организованную и диагностируемую систему.
Шаг создаётся через test.step с именем и телом. Он не меняет логику теста, но группирует действия внутри себя в один именованный этап, который виден в trace и в отчёте отдельным блоком. Длинный сценарий, разбитый на шаги "войти", "добавить товар", "оформить", "оплатить", читается как история с оглавлением, а при падении отчёт сразу показывает, на каком именно этапе оно случилось - без раскопок по всему телу теста.
Ценность шагов особенно видна в диагностике. Trace Viewer и HTML-отчёт отображают шаги как сворачиваемые блоки с их длительностью, поэтому по упавшему тесту сразу ясно, где он споткнулся и сколько занял каждый этап. Это дешёвая инвестиция в читаемость: несколько step вокруг логических этапов сценария экономят минуты разбора при каждом будущем падении, а стоят одной строки обёртки.
Полезно один раз увидеть шаги и аннотации в коде рядом. Ниже - сценарий, разбитый на именованные step, и примеры аннотаций: условный skip, пометка медленного теста и тег в заголовке. К этой форме возвращаются, когда сценарий вырастает настолько, что по нему уже не видно этапов, или когда тест надо временно исключить, пометить или отнести к набору.
Аннотации управляют выполнением теста явно и честно. test.skip с условием пропускает тест там, где он неприменим - например, на определённом браузере или окружении. test.fixme помечает известно сломанный сценарий, который чинят, а не удаляют. test.slow говорит раннеру, что тест объективно долгий, и увеличивает его лимит - это правильный способ дать время одной тяжёлой операции, в отличие от задранного глобального timeout из главы про конфиг.
Теги дают гибкую фильтрацию набора без перекладывания файлов. Тест помечают тегом - через опцию в его определении или прямо в заголовке, - а затем гоняют выборочно: --grep '@smoke' запускает только помеченные, --grep-invert исключает их. Так один и тот же набор служит и быстрым smoke на каждый пуш, и полным regression по расписанию, а деление проходит по тегам, а не по структуре каталогов.
Есть и уровень группы - test.describe.configure. Он задаёт режим и параметры для всей группы: например, перевести describe в serial или назначить ему собственные retries. Это точечная настройка поведения там, где она действительно нужна, вместо глобального изменения конфига ради одной группы сценариев. Важно применять её осознанно - serial-режим, как мы видели, блокирует параллельность и оправдан лишь для неделимого процесса.
Типичные провалы этого уровня - потеря организации и злоупотребление аннотациями. Гигантский тест без единого step, по падению которого непонятно, где сломалось. test.skip без условия и без причины, тихо выключающий сценарий навсегда вместо временного исключения с пояснением. И теги, расставленные бессистемно, по которым нельзя собрать осмысленный набор. Дробите сценарий на шаги, помечайте пропуски условием и причиной, а теги держите как продуманную карту наборов.
test('оформление заказа', { tag: '@smoke' }, async ({ page }) => {
await test.step('войти', async () => {
await page.goto('/login')
// ...
})
await test.step('оформить и оплатить', async () => {
// ...
await expect(page.getByRole('status')).toContainText('Заказ оплачен')
})
})
test.skip(({ browserName }) => browserName === 'webkit', 'нет поддержки в WebKit')
test.slow() // объективно долгий сценарий - увеличить лимит только ему