Пропущенный await в ассёрте Playwright это тихий баг: expect(page.getByText('Error')).toBeVisible() без await всегда проходит потому что вычисляет объект Promise, а не реальное состояние страницы. По умолчанию JavaScript не ждёт завершения браузерных операций, поэтому каждый метод Playwright обращающийся к браузеру возвращает Promise который нужно дождаться до использования результата. Здесь разобрана механика, примеры что ломается когда забываешь await, и паттерны для async-функций, сохранённых значений и параллельных операций.
Проблема которую решает async/await
JavaScript неблокирующий по умолчанию. Когда даёшь браузеру задание (перейти по URL, кликнуть кнопку, загрузить страницу), JavaScript не ждёт завершения и сразу переходит к следующей строке.
Для тестов это проблема. Нужно перейти на страницу до клика по кнопке. Нужно кликнуть до проверки результата.
До появления async/await решением были колбэки и Promises. Они работают, но порождают запутанный код который сложно читать и отлаживать. async/await это современное решение: делает асинхронные операции похожими на последовательные.
Простейшее объяснение
await означает: «жди здесь пока операция завершится, затем продолжай».
// Без await: не ждёт, падает или ведёт себя непредсказуемо
test('broken example', async ({ page }) => {
page.goto('https://lab.becomeqa.com'); // начинает навигацию, сразу продолжает
page.getByRole('button', { name: 'Login' }).click(); // пытается кликнуть до загрузки страницы
});
// С await: ждёт завершения каждого шага
test('correct example', async ({ page }) => {
await page.goto('https://lab.becomeqa.com'); // ждёт завершения навигации
await page.getByRole('button', { name: 'Login' }).click(); // затем кликает
});Второй тест работает потому что каждый await приостанавливает выполнение до завершения операции.
Что означает async
async перед функцией означает что эта функция может использовать await внутри и вернёт Promise:
// Эта функция async: она использует await
test('login test', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
// ...
});В тестах Playwright все тестовые функции автоматически async потому что все включают браузерные операции. Playwright test runner обрабатывает это сам. Нужно лишь помнить ставить await перед операциями.
Правило: когда использовать await в Playwright
Используй await перед любым методом Playwright который взаимодействует с браузером (навигация, клики, заполнение, выборки), возвращает информацию со страницы (текстовое содержимое, значения атрибутов, счётчики) или делает ассёрты (expect(...).toBeVisible()).
test('demonstrates await usage', async ({ page }) => {
// Навигация: await
await page.goto('https://lab.becomeqa.com');
// Действия: await
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
// Ассёрты: await
await expect(page.getByRole('heading', { name: 'My Travel Items' })).toBeVisible();
// Получение значений со страницы: await
const heading = await page.getByRole('heading').textContent();
console.log(heading); // 'My Travel Items'
// Проверка видимости: await
const isVisible = await page.getByRole('button', { name: 'Add Item' }).isVisible();
expect(isVisible).toBe(true);
});Что происходит если забыть await
Последствия зависят от того на чём забыл.
Забыл await на действии
// Заполняет поле и сразу продолжает, иногда случайно работает
// но упадёт при любой нагрузке или медленной сети
page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByRole('button', { name: 'Submit' }).click(); // может кликнуть до завершения fillЗабыл await на ассёрте
// Очень распространённая ошибка: ассёрт вычисляется в объект Promise
// который всегда истинный, поэтому тест всегда проходит даже когда должен падать
expect(page.getByText('Error')).toBeVisible(); // НЕПРАВИЛЬНО: без await, тест всегда проходит
await expect(page.getByText('Error')).toBeVisible(); // ПРАВИЛЬНОПоследнее особенно опасно: тесты которые всегда проходят хуже чем тесты которые всегда падают. TypeScript помогает поймать это. В строгом режиме TypeScript предупредит когда ассёрт содержит неожиданный Promise.
Сохранение значений с await
Когда нужно захватить то что возвращает Playwright:
test('checking values', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
// Сохраняем текстовое содержимое
const title = await page.getByRole('heading').textContent();
// title теперь строка: 'My Travel Items'
// Сохраняем счётчик
const rowCount = await page.getByRole('row').count();
// rowCount теперь число: 5
// Сохраняем статус видимости
const isButtonVisible = await page.getByRole('button', { name: 'Add Item' }).isVisible();
// isButtonVisible это булево: true или false
// Теперь используем
expect(title).toBe('My Travel Items');
expect(rowCount).toBeGreaterThan(0);
expect(isButtonVisible).toBe(true);
});Паттерн всегда такой: const result = await page.someMethod().
Async-функции за пределами тестов
Когда пишешь хелперы или методы Page Object использующие Playwright, они тоже должны быть async:
// Метод Page Object: должен быть async
class LoginPage {
constructor(private page: Page) {}
async login(email: string, password: string) {
await this.page.getByRole('button', { name: 'Login' }).click();
await this.page.getByLabel('Username').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Submit' }).click();
}
}
// При вызове async-метода нужен await
test('login test', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.login('admin@becomeqa.com', 'testpass123'); // await для async-метода
});Правило: если функция использует await внутри, объявляй её async. Если вызываешь async-функцию, ставь await перед вызовом.
Параллельные операции с Promise.all
Иногда нужно чтобы два действия происходили одновременно. Promise.all запускает несколько async-операций параллельно и ждёт завершения всех:
// Ждём навигации И появления конкретного элемента одновременно
// Это быстрее чем последовательное ожидание
await Promise.all([
page.waitForURL('/dashboard'),
expect(page.getByRole('heading', { name: 'My Travel Items' })).toBeVisible(),
]);В Playwright чаще всего Promise.all используется для одновременного ожидания навигации и ассёрта после клика. Playwright обрабатывает большинство таких случаев автоматически через auto-waiting, но Promise.all встречается в старом коде и специфических сценариях с таймингом.
Что такое Promises (кратко)
Когда видишь что-то вроде:
const textContent = page.getByRole('heading').textContent();
// textContent это Promise<string>, а не stringPromise это заглушка для значения которого ещё нет, потому что операция всё ещё выполняется. await разворачивает Promise, приостанавливая выполнение пока значение не будет готово:
const textContent = await page.getByRole('heading').textContent();
// Теперь textContent это stringГлубокого понимания Promises не нужно для написания Playwright-тестов. Ментальная модель «await заставляет ждать» достаточна для 95% тестового кода. Просто помни что каждый Playwright-метод возвращает Promise, поэтому нужен await.
TypeScript помогает не забывать
TypeScript знает какие методы возвращают Promises. Если забыл await, TypeScript предупредит:
Type 'Promise<string>' is not assignable to type 'string'Это одна из причин использовать TypeScript для Playwright-тестов: компилятор ловит пропущенные await до запуска тестов.
Краткая справка
| Что делаешь | Паттерн |
|-------------|---------|
| Навигация | await page.goto(url) |
| Клик | await element.click() |
| Заполнение поля | await element.fill('text') |
| Проверка видимости | await expect(element).toBeVisible() |
| Получить текст | const text = await element.textContent() |
| Получить счётчик | const n = await elements.count() |
| Вызов async-функции | await myAsyncFunction() |
| Параллельный запуск | await Promise.all([op1, op2]) |
FAQ
Нужно ли понимать как работает event loop JavaScript
Нет, не для написания Playwright-тестов. «await заставляет ждать результат» достаточно. Event loop это механизм под капотом, но понимать механизм для использования инструмента не нужно.
Почему тест проходит когда я забыл await на expect
Потому что expect(element).toBeVisible() без await возвращает объект Promise (который истинный), а test runner не вычисляет его как ассёрт. Тест не видит падений. Это тихий баг. Строгий режим TypeScript его помечает.
Можно ли использовать .then() вместо await
Да, .then() это более старый синтаксис Promises. Работает, но порождает код сложнее для чтения:
// Старый синтаксис Promise
page.goto('https://lab.becomeqa.com').then(() => {
return page.getByRole('button', { name: 'Login' }).click();
}).then(() => {
// ...
});
// То же самое с async/await, намного понятнее
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();Используй async/await. Избегай цепочек .then() в новом тестовом коде.
Вижу Promise в TypeScript. Что это значит
void означает что функция не возвращает значимого результата. Она просто что-то делает (например, навигация или клик). await всё равно нужен, но результат не сохраняют в переменную.
→ See also: TypeScript для QA: почему статическая типизация улучшает тесты | Начало работы с Playwright: первые тесты за 30 минут