Самый частый источник flaky-тестов — тайминги: тест проверяет элемент раньше, чем тот появился, и падает через раз. Классическое «лечение» — sleep — делает только хуже. Playwright решает проблему в корне: его утверждения и действия ждут сами. Понять авто-ожидание — значит избавиться от целого класса нестабильности.
Проблема таймингов
Интерфейс асинхронен: после клика данные грузятся, элементы появляются с задержкой. Тест, который проверяет результат мгновенно, гоняется в гонку с интерфейсом — иногда успевает, иногда нет. Это и есть флаки.
Соблазн — поставить паузу: await page.waitForTimeout(1000). Это плохо вдвойне: на быстрой машине теряешь время зря, на медленной — всё равно не хватает, и тест снова флачит. Фиксированная пауза не синхронизирована с реальностью.
Web-first assertions
Утверждения Playwright на локаторе повторяются автоматически до таймаута, пока не станут истинными. Это не «проверить сейчас», а «дождаться, что станет так».
await expect(page.getByText("Заказ оформлен")).toBeVisible();
await expect(page.getByRole("row")).toHaveCount(3);
await expect(page.getByLabel("Итого")).toHaveText("4990 ₽");
expect(locator).toBeVisible() будет перепроверять видимость, пока элемент не появится или не истечёт таймаут. Не нужно ни паузы, ни ручного ожидания — утверждение само синхронизируется с интерфейсом. Это и есть «web-first»: проверки знают про асинхронность веба.
Авто-ожидание действий
Действия тоже ждут. Перед кликом Playwright дожидается, что элемент видим, включён и готов принять клик (actionable).
await page.getByRole("button", { name: "Оплатить" }).click();
Не нужно проверять «появилась ли кнопка» перед кликом — click сделает это сам. Это убирает горы ручных проверок-ожиданий, которые писали в старых инструментах.
Почему не sleep
Из сказанного следует правило: waitForTimeout/sleep в e2e не место. Если возник соблазн поставить паузу — значит, чего-то ждёшь, и это «что-то» надо выразить условием, а не временем. Почти всегда нужное ожидание уже встроено в утверждение или действие.
Явное ожидание, когда нужно
Изредка ждут то, что не покрыто авто-ожиданием, — например, конкретный ответ сети:
await Promise.all([
page.waitForResponse((r) => r.url().includes("/api/orders") && r.ok()),
page.getByRole("button", { name: "Оформить" }).click(),
]);
Это ожидание условия (пришёл нужный ответ), а не времени — оно детерминировано. Связь действия и ожидаемого ответа через Promise.all исключает гонку.
Где это в UCP
Авто-ожидание — главный приём устойчивого e2e: утверждения и действия синхронизируются с интерфейсом сами, а sleep уходит из лексикона. Это снимает основную причину флаки и делает тесты детерминированными. Для продукт-инженера это e2e, которым можно верить: красный означает реальную поломку, а не неудачный тайминг. Остальные источники нестабильности — данные, сеть — закрывают подготовка данных и контроль сети.