Проверка 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.

→ See also: Async/Await простым языком (для тестировщиков, которых сбивают с толку промисы) | Нестабильные тесты: почему они возникают и как их устранить | Обработка ошибок JavaScript с try/catch для QA-инженеров