page.waitForTimeout(2000) перед ассертом почти всегда неверное решение: ассерты Playwright уже поллят до прохождения, поэтому sleep избыточен при быстрых прогонах и всё равно слишком короток при медленных. Правильный паттерн: дать ассерту делать ожидание, или использовать waitForResponse внутри Promise.all чтобы поймать конкретный API-вызов перед проверкой его результата. Эта статья разбирает условия auto-waiting в Playwright, когда их недостаточно, и пять явных API ожидания: какие использовать, а какие считать признаком проблемы в коде.
Как работает auto-waiting в Playwright
Когда пишешь await page.getByRole('button', { name: 'Submit' }).click(), Playwright не кликает немедленно. Он ждёт пока кнопка будет:
- Присоединена к DOM
- Видима (не скрыта, не
display: none) - Стабильна (не анимируется)
- Доступна (не отключена)
- Принимает события указателя (не перекрыта другим элементом)
Всё это происходит автоматически, до истечения actionTimeout (по умолчанию 30 секунд). Никакого кода ожидания писать не нужно.
Именно поэтому Playwright быстрее в написании чем Selenium. В Selenium написал бы WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ROLE, 'button'))). В Playwright: просто клик.
Когда auto-waiting недостаточно
Auto-waiting работает для взаимодействий с элементами. Не всё он охватывает:
Навигация по страницам. После отправки формы или клика по ссылке URL меняется. Auto-waiting не будет ждать пока новая страница полностью загрузится перед следующим действием. Загрузка данных. Элемент существует и виден, но пока данные загружаются показывает спиннер. Playwright может кликнуть по нему до прихода данных. Несколько сетевых запросов. Загрузка страницы запускает три API-вызова. Playwright видит что DOM готов, но третий вызов ещё не завершён. Завершение анимации. Элемент технически виден, но в середине анимации. Auto-waiting справляется с простыми CSS-переходами, но не со всеми состояниями анимации.expect как инструмент ожидания
Самый недооценённый паттерн ожидания в Playwright: ассерты ждут.
// Ждёт до таймаута пока URL не совпадёт
await expect(page).toHaveURL('/dashboard');
// Ждёт пока текст не появится
await expect(page.getByRole('heading')).toHaveText('Order confirmed');
// Ждёт пока количество элементов не достигнет 5
await expect(page.getByRole('listitem')).toHaveCount(5);Каждый expect-ассерт в Playwright поллит до прохождения или таймаута. Это делает ассерты самым чистым способом ждать состояния приложения.
Антипаттерн
await page.waitForTimeout(2000); // никогда так не делай
await expect(page.getByRole('heading')).toHaveText('Order confirmed');Правильно
await expect(page.getByRole('heading')).toHaveText('Order confirmed');expect делает ожидание. waitForTimeout: признак того что непонятно чего именно ждать.
Явные инструменты ожидания
Когда явное ожидание всё же нужно, Playwright предоставляет точечные варианты:
waitForURL
await page.getByRole('button', { name: 'Log in' }).click();
await page.waitForURL('/dashboard');
// Теперь можно безопасно проверять содержимое дашбордаwaitForResponse: ожидание конкретного API-вызова
const [response] = await Promise.all([
page.waitForResponse(resp => resp.url().includes('/api/orders') && resp.status() === 200),
page.getByRole('button', { name: 'Place order' }).click(),
]);
const orderData = await response.json();
expect(orderData.status).toBe('created');Начинай ожидание до действия которое запускает запрос. Promise.all гарантирует что быстрый ответ не будет пропущен.
waitForRequest: проверка что запрос был отправлен
const [request] = await Promise.all([
page.waitForRequest(req => req.url().includes('/api/track') && req.method() === 'POST'),
page.getByRole('button', { name: 'Purchase' }).click(),
]);
// Проверяем что аналитическое событие сработало
expect(request.postDataJSON()).toMatchObject({ event: 'purchase' });waitForSelector: ожидание состояния элемента
// Ждём пока спиннер загрузки не исчезнет
await page.waitForSelector('.spinner', { state: 'detached' });
// Ждём пока элемент не станет видимым
await page.waitForSelector('[data-testid="results-table"]', { state: 'visible' });Опции state: 'attached', 'detached', 'visible', 'hidden'.
Предпочитай expect(locator).toBeVisible() вместо waitForSelector: ассертный подход читабельнее.
waitForLoadState
await page.goto('/heavy-page');
await page.waitForLoadState('networkidle'); // Ждём пока 500мс не будет сетевой активностиОпции loadState: 'load' (событие window.load сработало, по умолчанию для goto), 'domcontentloaded' (DOM разобран, до загрузки изображений и скриптов) и 'networkidle' (нет сетевых запросов 500мс).
'networkidle' медленный и ненадёжный. Избегай его если страница не сигнализирует о готовности другим способом. Лучше ждать конкретный элемент.
Настройка таймаутов
Таймауты настраиваются на трёх уровнях:
// playwright.config.ts — применяется ко всем тестам
export default defineConfig({
timeout: 30000, // Таймаут теста (весь тест)
expect: {
timeout: 5000, // Таймаут ассерта
},
use: {
actionTimeout: 15000, // Таймаут отдельного действия (клик, fill и т.д.)
navigationTimeout: 30000,
},
});Переопределение для конкретного теста:
test('slow data load', async ({ page }) => {
test.setTimeout(60000); // Этот тест получает 60 секунд
// ...
});Переопределение для конкретного ассерта:
await expect(page.getByText('Report generated')).toBeVisible({ timeout: 30000 });Правило waitForTimeout
page.waitForTimeout(ms) это sleep. Иногда он нужен как последнее средство (например, ожидание стороннего скрипта который нельзя наблюдать). Но каждое его появление стоит считать TODO: чего именно здесь нужно ждать?
Если await page.waitForTimeout(1000) встречается больше чем в одном-двух тестах, у твоего сьюта структурная проблема с ожиданиями. Стоит разобраться.