Самый частый источник нестабильных тестов — тайминги: тест проверяет элемент раньше, чем тот появился на странице. Классическое «лечение» — добавить паузу — только ухудшает ситуацию. Playwright решает проблему иначе: его утверждения и действия умеют ждать самостоятельно.
Почему sleep не работает
Интерфейс асинхронен: после клика данные загружаются, элементы появляются с задержкой. Тест, который сразу проверяет результат, вступает в гонку с браузером — иногда успевает, иногда нет. Тест начинает падать «через раз».
Первый порыв — поставить паузу:
await page.waitForTimeout(1000); // подождём секунду...
await expect(page.getByText("Заказ оформлен")).toBeVisible();
Проблема двойная. На быстрой машине теряешь секунду зря — результат давно готов. На медленной или под нагрузкой секунды может не хватить — и тест снова нестабильный. Фиксированная пауза не привязана к реальному состоянию приложения.
Web-first утверждения
Playwright решает это иначе. Утверждение на локаторе повторяется автоматически до тех пор, пока не выполнится или не истечёт таймаут. Это не «проверить прямо сейчас», а «дождаться, что станет так».
await expect(page.getByText("Заказ оформлен")).toBeVisible();
await expect(page.getByRole("row")).toHaveCount(3);
await expect(page.getByLabel("Итого")).toHaveText("4990 ₽");
toBeVisible() будет перепроверять видимость элемента снова и снова, пока он не появится или не истечёт таймаут (по умолчанию 5 секунд). Не нужно ни паузы, ни ручного ожидания — утверждение само синхронизируется с интерфейсом.
Это и называется web-first: проверки знают про асинхронную природу браузера.
Часто используемые web-first утверждения:
| Утверждение | Что проверяет |
|---|---|
toBeVisible() | элемент виден на странице |
toBeHidden() | элемент скрыт или отсутствует |
toHaveText(text) | текстовое содержимое совпадает |
toHaveValue(value) | значение поля ввода совпадает |
toHaveCount(n) | количество найденных элементов |
toBeEnabled() / toBeDisabled() | кнопка активна или заблокирована |
toBeChecked() | чекбокс отмечен |
Авто-ожидание действий
Не только утверждения — действия тоже ждут. Перед тем как выполнить клик, Playwright проверяет, что элемент виден, не перекрыт другим элементом, не заблокирован и готов принять взаимодействие.
await page.getByRole("button", { name: "Оплатить" }).click();
Не нужно явно проверять «появилась ли кнопка» перед кликом — click() сделает это сам. Это убирает горы проверок-ожиданий, которые приходилось писать вручную в старых инструментах автоматизации.
Тот же принцип работает у fill(), selectOption(), check() и других действий.
Когда нужно явное ожидание
Авто-ожидание не покрывает всё. Иногда нужно дождаться чего-то, что не выражается через состояние элемента, — например, конкретного сетевого ответа.
await Promise.all([
page.waitForResponse((r) => r.url().includes("/api/orders") && r.ok()),
page.getByRole("button", { name: "Оформить" }).click(),
]);
Здесь тест ждёт именно условия (пришёл нужный ответ с кодом 200), а не фиксированного времени. Promise.all связывает клик и ожидание ответа вместе — это исключает гонку между отправкой запроса и началом ожидания.
Похожий приём — waitForURL(), когда нужно убедиться, что браузер перешёл на нужную страницу:
await page.getByRole("button", { name: "Войти" }).click();
await page.waitForURL("**/dashboard");
Настройка таймаута
По умолчанию web-first утверждения ждут 5 секунд. Это можно изменить глобально в конфигурации или для конкретного утверждения:
// для одного утверждения
await expect(page.getByText("Отчёт готов")).toBeVisible({ timeout: 15_000 });
Увеличивать таймаут стоит осторожно: если тест стабильно ждёт 10 секунд — возможно, проблема в самом приложении, а не в тесте.
Коротко
- Фиксированные паузы (
sleep,waitForTimeout) делают тесты медленными и всё равно нестабильными — их не должно быть в e2e. - Web-first утверждения (
expect(locator).toXxx()) повторяются автоматически, пока условие не выполнится или не истечёт таймаут. - Действия (
click,fillи др.) тоже ждут готовности элемента — явная проверка перед действием не нужна. - Для ожидания условий, не связанных с DOM, используй
waitForResponse(),waitForURL()и подобные — ждут конкретного события, а не времени. - Если возник соблазн поставить паузу — значит, надо выразить ожидание через условие.
Что почитать дальше
- Нестабильные тесты: причины и лечение — откуда берётся нестабильность и как с ней работать системно.
- Данные и аутентификация — как подготовить данные и сессию так, чтобы тесты не зависели друг от друга.
- Сетевые запросы и моки — перехват и подмена ответов для контроля внешних зависимостей.