Главный источник нестабильности E2E-тестов - неправильные ожидания. Интерфейс обновляется асинхронно: данные приходят по сети, элементы появляются после анимации, кнопки становятся активными не сразу. Наивный тест пытается угадать этот момент паузой в две секунды, и на быстрой машине она избыточна, а на загруженном CI - мала. Playwright решает это иначе: он ждёт не время, а условие, при котором действие вообще имеет смысл.
Механизм называется auto-waiting, и он встроен в каждое действие. Перед кликом Playwright автоматически проверяет набор условий actionability: элемент единственный, видимый, стабилен (перестал двигаться), способен получать события и enabled. Пока эти условия не выполнены, действие не начинается - и повторяется вплоть до timeout. Это снимает целый класс гонок, в которых тест кликал по элементу, ещё не готовому к взаимодействию.
Ту же логику несут локаторы и web-first assertions: они не читают состояние один раз, а повторяют проверку до появления нужного результата. Именно поэтому не нужно вручную ждать, "пока прогрузится": правильный локатор и правильная проверка сами дождутся. Ожидание перестаёт быть отдельным шагом сценария и становится свойством каждого действия и утверждения.
Контраст нагляден на паре примеров. Наивный подход ставит waitForTimeout(2000) и затем разово читает textContent - тест либо тормозит на ровном месте, либо падает, не дождавшись. Правильный подход пишет expect(status).toHaveText('Готово') - проверка сама повторяется, пока текст не появится, и завершается ровно тогда, когда условие выполнено, ни секундой раньше или позже. Первый способ угадывает время, второй наблюдает состояние.
Полезно один раз увидеть оба подхода рядом, чтобы разница въелась в привычку. Ниже - фиксированная пауза против ожидания наблюдаемого состояния; к этому сравнению возвращаются каждый раз, когда рука тянется вставить waitForTimeout, чтобы "стабилизировать" флакающий сценарий. Пауза не стабилизирует - она прячет причину и добавляет случайности.
Отдельно стоит предупредить про networkidle. Соблазн дождаться "тишины в сети" как признака готовности страницы понятен, но обманчив: аналитика, websockets и периодический polling держат сеть постоянно активной, и такое ожидание либо не наступит, либо наступит не там. networkidle не universal-сигнал готовности - это устаревший костыль, дающий ложную стабильность на одних страницах и вечное ожидание на других.
Вместо сетевой тишины ждут конкретное наблюдаемое событие. Нужный элемент интерфейса, изменившийся URL, конкретный сетевой ответ через waitForResponse, доменное состояние вроде появившегося сообщения об успехе. Это привязывает ожидание к тому, что реально означает готовность в терминах пользователя, а не к косвенному и капризному признаку активности сети, который к бизнес-состоянию отношения не имеет.
Типичные провалы ожиданий - главная причина флака в E2E. Фиксированная пауза waitForTimeout, которая на быстрой машине тратит время впустую, а на медленной не дожидается. Разовое чтение состояния сразу после действия, попадающее в момент до обновления. И networkidle как универсальный признак готовности на странице с постоянной сетевой активностью. Ждите условие - видимый UI, URL, ответ, бизнес-событие, - а не количество миллисекунд.
// Фиксированная пауза - угадывание времени
await page.waitForTimeout(2000)
expect(await status.textContent()).toBe('Готово')
// Наблюдаемое состояние - проверка повторяется до появления текста
await expect(status).toHaveText('Готово')