Проверка в E2E должна выдерживать асинхронность интерфейса, и именно этим web-first assertion отличается от обычного сравнения. Разовое чтение состояния - взять текст элемента и сравнить со строкой - попадает в конкретный момент, и если данные ещё не пришли, тест падает не потому, что поведение неверно, а потому, что проверка опоздала или поспешила. Web-first assertion устроена иначе: она повторяет наблюдение до тех пор, пока условие не выполнится или не истечёт timeout.
Форма таких проверок узнаваема: await expect(локатор или страница) и матчер, описывающий ожидаемое состояние. expect(page).toHaveURL с регулярным выражением проверяет адрес, toContainText и toHaveText - текст элемента, toBeVisible и toBeHidden - видимость, toBeEnabled - доступность кнопки. Каждый матчер повторяется автоматически, поэтому тесту не нужно вручную ждать появления или исчезновения элемента: ожидание встроено в само утверждение.
Ключевое свойство - безопасное повторение. Матчер не читает состояние один раз, а опрашивает страницу заново, пока не увидит ожидаемое. Для асинхронного UI это и есть надёжность: сообщение об успехе, пришедшее через триста миллисекунд после клика, будет поймано, а не пропущено. Поэтому web-first assertions заменяют и ручные ожидания, и разовые проверки - они совмещают ожидание и утверждение в одном шаге.
Полезно один раз собрать рабочий набор матчеров, чтобы описывать наблюдаемое точно, а не приблизительно. Ниже - типичные проверки одного сценария оформления заказа: адрес по шаблону, текст уведомления, значение итога, скрытый диалог, активная кнопка. К этому набору возвращаются, когда нужно выразить ожидаемое состояние на публичной границе, а не лезть во внутренности приложения.
Для состояния, которое не отражено в UI напрямую, есть expect.poll. Он периодически выполняет произвольную функцию - запрос к API, чтение из внешнего источника - и проверяет её результат тем же матчером, повторяя до успеха или timeout. Это web-first подход, распространённый на не-DOM состояние: когда готовность выражена не элементом на странице, а ответом сервиса, который нужно опрашивать.
Отдельный инструмент - soft assertions. Обычная проверка при провале останавливает тест, и это правильно для критических условий. Но иногда нужно собрать сразу несколько расхождений - например, проверить, что на странице корректны все поля карточки, - и тогда soft-вариант позволяет продолжить и показать все проблемы разом. Это удобно для отчёта, но у приёма есть граница, которую нельзя переступать.
Граница проходит по разрушительности следующего шага. Soft assertion уместна, пока после расхождения сценарий продолжает безопасные проверки. Но если критическое утверждение провалилось, а следующий шаг - необратимое действие (оплата, удаление, отправка), продолжать нельзя: тест либо испортит данные, либо будет действовать в заведомо сломанном состоянии. Критический провал должен останавливать сценарий, а не накапливаться мягко.
Типичные провалы проверок сводятся к потере повторяемости. Разовое чтение textContent с ручным сравнением вместо web-first матчера - классический источник флака на асинхронном UI. expect.poll там, где готовность выражена элементом на странице, - лишнее усложнение. И soft assertions, за которыми сценарий продолжает разрушительные действия после критического провала. Описывайте наблюдаемое повторяющимся матчером и останавливайтесь на критическом расхождении.
await expect(page).toHaveURL(/\/orders\/\d+$/)
await expect(page.getByRole('alert')).toContainText('Заказ создан')
await expect(page.getByTestId('total')).toHaveText('1 490 ₽')
await expect(page.getByRole('dialog')).toBeHidden()
await expect(downloadButton).toBeEnabled()