Проверка expect() без await в Playwright всегда проходит, даже когда условие ложное: проверка запускается, но никогда не ждёт завершения. Это самый острый угол async в тестовом коде. Гайд охватывает как работают промисы, почему пропущенный await вызывает флакующие тесты вместо немедленных падений, Promise.all для параллельной настройки фикстур, Promise.allSettled для сбора частичных результатов и паттерн с гонкой состояний для корректной обработки навигации после клика по кнопке.
Что такое Promise
Promise служит обёрткой для значения которое ещё не готово. Заглушка для чего-то что придёт в будущем.
// Promise в трёх состояниях:
// 1. Pending — ждёт результата
// 2. Fulfilled — операция успешна, значение доступно
// 3. Rejected — операция провалилась, ошибка доступна
const promise = fetch('/api/users');
// В этот момент promise — PENDING
// Через некоторое время он становится либо:
// FULFILLED — ответ пришёл
// REJECTED — сетевая ошибка, ошибка сервера.then(): старый способ
До async/await промисы потреблялись через .then():
fetch('/api/users')
.then(response => response.json())
.then(users => {
console.log(users); // Используем данные
})
.catch(error => {
console.error('Failed:', error);
});Для простых случаев цепочки нормально читаются, но при сложной логике быстро превращаются в кашу. Async/await появился именно для этого.
async/await: современный способ
async function getUsers() {
try {
const response = await fetch('/api/users');
const users = await response.json();
return users;
} catch (error) {
console.error('Failed:', error);
throw error;
}
}await приостанавливает выполнение до разрешения промиса. Код читается как синхронный, но работает асинхронно.
Три правила: await можно использовать только внутри async-функций, async-функции всегда возвращают Promise, и если async-функция возвращает значение, оно оборачивается в resolved Promise.
Что происходит когда забываешь await
Самый частый async-баг в Playwright:
// БАГ: забыт await
test('тест логина', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@test.com');
page.click('[data-testid="submit"]'); // нет await!
// Эта проверка запускается ДО завершения клика
await expect(page).toHaveURL('/dashboard'); // Может упасть непостоянно
});Без await page.click() запускается, но не ждёт завершения. Следующая строка выполняется пока клик ещё в процессе. В Playwright это вызывает флакующие тесты: иногда клик успевает завершиться, иногда нет.
Исправление:
await page.click('[data-testid="submit"]');Последовательное vs параллельное выполнение
По умолчанию await выполняет операции последовательно:
// Последовательно — суммарное время: сумма всех ожиданий
const user = await getUser(1); // Ждём пользователя
const orders = await getOrders(1); // Потом ждём заказы
const profile = await getProfile(1); // Потом ждём профильЕсли операции независимы, запускай их параллельно:
// Параллельно — суммарное время: самое долгое одиночное ожидание
const [user, orders, profile] = await Promise.all([
getUser(1),
getOrders(1),
getProfile(1),
]);В фикстурах Playwright этот паттерн используется для одновременной настройки нескольких вещей:
// Настройка тест-данных параллельно
const [adminToken, testUser] = await Promise.all([
loginAsAdmin(request),
createTestUser(request),
]);Promise.all: запуск нескольких параллельно
// Все три запроса отправляются одновременно
const results = await Promise.all([
request.get('/api/users'),
request.get('/api/products'),
request.get('/api/orders'),
]);
// results — массив ответов
const [usersResp, productsResp, ordersResp] = results;Важно: если ЛЮБОЙ промис в Promise.all отклоняется, весь вызов отклоняется:
try {
const [a, b, c] = await Promise.all([
fetch('/api/users'),
fetch('/api/will-fail-with-404'), // Этот упадёт
fetch('/api/products'),
]);
// Сюда не дойдём если любой провалится
} catch (error) {
// Один из них провалился
console.error(error);
}Promise.allSettled: ждёт всех, включая упавших
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/might-fail'),
fetch('/api/products'),
]);
// У каждого результата есть status + value или reason
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failed:', result.reason);
}
});allSettled используй когда нужны все результаты независимо от отдельных сбоев.
Promise.race: побеждает первый
// Паттерн таймаута через Promise.race
const fetchWithTimeout = async (url, timeoutMs) => {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
);
return Promise.race([fetch(url), timeout]);
};
try {
const response = await fetchWithTimeout('/api/slow', 5000);
} catch (error) {
if (error.message === 'Timeout') {
console.log('Запрос занял слишком много времени');
}
}Обработка ошибок
try/catch с async/await
async function createUser(data: UserData) {
try {
const response = await request.post('/api/users', { data });
if (!response.ok()) {
const body = await response.json();
throw new Error(`API error: ${body.message}`);
}
return await response.json();
} catch (error) {
console.error('Failed to create user:', error);
throw error; // Перебрасываем чтобы вызывающий знал о провале
}
}В тестах Playwright
test('корректно обрабатывает ошибку API', async ({ page }) => {
// Мокаем API чтобы оно упало
await page.route('/api/users', route => route.fulfill({ status: 500 }));
await page.goto('/users');
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message')).toContainText('Something went wrong');
});Частые async-баги в тестах Playwright
1. Пропущенный await у проверок
// БАГ
expect(page.getByTestId('button')).toBeVisible(); // Без await — всегда проходит!
// Исправление
await expect(page.getByTestId('button')).toBeVisible();2. Неправильный await в циклах
// БАГ — все запускаются параллельно, ошибки могут проглатываться
const items = ['a', 'b', 'c'];
items.forEach(async (item) => {
await processItem(item); // Запускаются параллельно, не awaited
});
// Исправление: последовательно
for (const item of items) {
await processItem(item);
}
// Или параллельно с правильной обработкой
await Promise.all(items.map(item => processItem(item)));3. Гонка состояний
// БАГ: клик и навигация гонятся
await page.click('[data-testid="submit"]');
// Submit может перейти на другой URL, а может показать ошибку валидации
// Следующая строка может выполниться до того как станет ясно что произошло
// Исправление: явно ждём то что ожидаем
await Promise.all([
page.waitForURL('/dashboard'),
page.click('[data-testid="submit"]'),
]);
// Или: ждём ответа
const [response] = await Promise.all([
page.waitForResponse('/api/auth/login'),
page.click('[data-testid="submit"]'),
]);Async/Await в Page Objects
Методы page object должны быть async когда они что-то ожидают:
class LoginPage {
constructor(private page: Page) {}
// Async потому что выполняет навигацию
async navigate(): Promise<void> {
await this.page.goto('/login');
}
// Async потому что выполняет действия
async login(email: string, password: string): Promise<void> {
await this.page.fill('[data-testid="email"]', email);
await this.page.fill('[data-testid="password"]', password);
await this.page.click('[data-testid="submit"]');
}
// Async потому что читает со страницы
async getErrorMessage(): Promise<string | null> {
return this.page.getByTestId('error').textContent();
}
}
// Использование: всё awaited
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('user@test.com', 'ValidPass1');
const error = await loginPage.getErrorMessage();Краткая справка
| Концепция | Что делает |
|-----------|-----------|
| Promise | Заглушка для будущего значения |
| async | Помечает функцию как асинхронную |
| await | Приостанавливает до разрешения Promise |
| Promise.all() | Запускает несколько параллельно, ждёт всех |
| Promise.allSettled() | Как all(), но продолжает при сбоях |
| Promise.race() | Возвращает первый разрешённый/отклонённый |
| try/catch | Обработка async-ошибок |
Главное правило: всегда await для действий и проверок Playwright. Пропущенный await: причина большинства флакующих тестов. Тест падает непостоянно без очевидной причины, сначала ищи пропущенный await.