К списку уроков
Блок 2 · Программирование для автоматизатора

Урок 4. Почему в каждом автотесте куча await?

Асинхронность JS/TS

Темы урока

Функции и стрелочные функции, Promise, async/await, забытый await, порядок шагов теста, makeUser и registerUser, правка чужого автотеста, browser.newContext, негативная проверка

Видео урока

Пройти тест по уроку

Конспект урока

Конспект — Урок 4: Почему в каждом автотесте куча await?

Главное за урок

Браузер — отдельный процесс, не мгновенная функция в вашем файле. page.click() не кликает «внутри TypeScript»: он отправляет команду и ждёт ответ. Пока ждём, программа может делать другое. Поэтому API Playwright построен на обещаниях (Promise), а await говорит: «подожди этот шаг, потом иди дальше».

Без await строки на экране идут сверху вниз, а в браузере — почти одновременно. Тест врёт о порядке действий: клика ещё не было, а проверка уже побежала.


Синхронный код vs асинхронный

Синхронный — следующая строка начинается, только когда предыдущая уже вернула результат. Сложение чисел, сбор объекта, JSON.parse — синхронны.

const a = 1 + 1;          // сразу 2
const user = { name: "Анна" }; // сразу объект

Асинхронный — результат будет позже: сеть, диск, браузер, таймер. Функция сразу возвращает не ответ, а обещание ответа.

const page = browser.newPage(); // это ещё не страница, а Promise

В автотесте почти всё общение со страницей асинхронное: открыть URL, заполнить поле, кликнуть, дождаться текста.


Promise: три состояния

Promise — объект-обещание: «результат придёт позже — успех или ошибка».

Состояние Смысл
pending Ещё ждём
fulfilled Сдержано: есть значение (resolve)
rejected Нарушено: есть ошибка (reject)

Пока обещание pending, притворяться, что результат уже есть, нельзя. goto, fill, click, newPage, expect(...).toBeVisible() — каждая такая строка возвращает Promise.

Старый способ дождаться — .then() / .catch(). В тестах так почти не пишут: вместо цепочек колбэков используют async / await.

// узнать можно, но в тестах так не пишем
page.goto("/login").then(() => page.getByLabel("Email").fill("a@test.com"));

async и await

Слово Что делает
async перед функцией Функция сама вернёт Promise; внутри разрешён await
await перед выражением Пауза, пока Promise не завершится; в переменную попадает уже результат, не обещание
async function openLogin(page: Page) {
  await page.goto("/login");
  await page.getByLabel("Email").fill("a@test.com");
}

Правила, без которых код не соберётся или будет врать:

  1. await можно писать только внутри async.
  2. async function всегда возвращает Promise, даже если вы написали return 5 — снаружи это Promise<number>.
  3. await достаёт значение из Promise: const page = await browser.newPage() — в page страница, не обещание.
  4. Если Promise rejected, await бросает ошибку — тест падает на этой строке. Это нормально: шаг не удался.

Стрелочная запись та же:

test("логин", async ({ page }) => {
  await page.goto("/login");
});

test(..., async ({ page }) => { ... }) — тело теста асинхронное, потому что внутри await.


Что кладётся в переменную: с await и без

Это главная ловушка новичка.

const page = await browser.newPage(); // страница
await page.goto("/login");            // дождались навигации

const notPage = browser.newPage();    // Promise, не страница
// notPage.goto("/login") — странная ошибка: у Promise нет goto
Код Что в переменной
await browser.newPage() Страница
browser.newPage() без await Обещание страницы
await page.goto("/login") Навигация закончилась
page.goto("/login") без await Обещание; следующая строка уже бежит

Забыли await перед click / fill / goto — следующая строка может начаться раньше, чем действие произошло. Симптомы: падение «не там», locator not found, или ложно-зелёный (проверка прошла по старой странице).

Правило: делаете что-то со страницей через Playwright — почти наверняка нужен await.


Порядок строк ≠ порядок в браузере

С await шаги идут строго один за другим:

await page.goto("/login");
await page.getByLabel("Email").fill(email);
await page.getByRole("button", { name: "Войти" }).click();
await expect(page).toHaveURL(/\/dashboard/);

Сначала открыли форму, потом заполнили, потом кликнули, потом проверили URL. Без await эти четыре обещания «выстреливают» почти сразу — проверка URL бежит, пока форма ещё грузится.

Порядок await — это и есть сценарий. В гонке двух пользователей за один ресурс он критичен: кто первый дождался «Подтвердить», тот занял слот.


Что ждать, а что нет: локатор vs действие

Не каждая строка Playwright асинхронная.

Вызов await? Почему
page.getByRole("button", { name: "Войти" }) Нет Локатор — описание «где искать». Поиск произойдёт в момент действия
locator.click() / .fill() / .check() Да Команда браузеру
page.goto() / page.reload() Да Навигация
browser.newContext() / context.newPage() Да Создаём сессию и вкладку
expect(locator).toBeVisible() / .toHaveText() Да Проверка сама ждёт и возвращает Promise
expect(2 + 2).toBe(4) Нет Обычный синхронный expect, без страницы
const button = page.getByRole("button", { name: "Войти" }); // без await
await button.click();                                      // с await
await expect(button).toBeVisible();                        // с await

Auto-waiting: зачем не нужен sleep(3000)

Playwright сам ждёт, пока элемент станет готовым к действию (виден, стабилен, не перекрыт, enabled). Это auto-waiting. Таймаут по умолчанию — несколько секунд, потом падение.

Поэтому перед кликом не ставят waitForTimeout(3000) «на всякий случай». Сон делает тест медленным и хрупким: то мало, то много.

await и auto-waiting — разные вещи:

  • auto-waitingкак долго Playwright готов ждать элемент внутри одного действия;
  • awaitкогда ваш код перейдёт к следующей строке.

Ни auto-waiting, ни Promise.all не спасают от забытого await.


Promise.all — когда шаги независимы

Promise.all запускает несколько обещаний параллельно и ждёт все сразу.

await Promise.all([
  hostContext.close(),
  guestContext.close(),
]);

Имеет смысл, если шаги не зависят друг от друга. Заполнить поле и сразу кликнуть «Войти» параллельно нельзя: клик должен быть после fill. В пользовательском сценарии почти всегда идём цепочкой await.


Синхронная фабрика и асинхронный шаг

Две почти одинаковые регистрации — сигнал вынести функцию. Данные и браузер — разные функции.

function makeUser(role: string, runId: number) {
  return {
    name: `${role}-${runId}`,
    email: `${role}-${runId}@example.com`,
    password: "password1234",
  };
}

async function registerUser(page: Page, user: { name: string; email: string; password: string }) {
  await page.goto("/register");
  await page.getByLabel("Имя").fill(user.name);
  await page.getByLabel("Email").fill(user.email);
  await page.getByLabel("Пароль").fill(user.password);
  await page.getByRole("button", { name: "Зарегистрироваться" }).click();
}

makeUser не ходит в UI — обычный return, без async. registerUser кликает по форме — async и await на каждом действии.


Два пользователя — два контекста

Один page делит cookies. Второй «пользователь» в той же вкладке — это всё ещё первый человек.

const hostContext = await browser.newContext();
const guestContext = await browser.newContext();
const hostPage = await hostContext.newPage();
const guestPage = await guestContext.newPage();

newContext() — как отдельное окно инкогнито: своя сессия. Два аккаунта = два контекста и две страницы. Вторая вкладка page.context().newPage() остаётся в той же сессии.

В конце контексты закрывают: await hostContext.close().


Негатив и гонка: порядок await — часть сценария

Ожидаемая ошибка в UI — валидный результат теста. Assert ждёт текст ошибки или недоступность кнопки, а не «всё зелёное».

Если два пользователя бьются за один ресурс, второму часто нужно открыть тот же экран до подтверждения первым. Иначе ресурс уже занят, и второй не увидит ту же форму или ту же ошибку. Это не синтаксис — это порядок await в сценарии.


Как читать асинхронный тест сверху вниз

test("логин", async ({ browser }) => {
  const context = await browser.newContext();
  const page = await context.newPage();
  const user = makeUser("guest", Date.now()); // синхронно

  await registerUser(page, user);
  await expect(page).toHaveURL(/\/dashboard/);
  await context.close();
});
  1. Тело testasync, иначе await нельзя.
  2. Создание контекста и страницы — с await.
  3. Данные — без await.
  4. Шаги в браузере и expect по странице — с await.
  5. Следующая строка = следующий шаг в сценарии.

Ключевые тезисы для теста

  • Браузер отвечает не мгновенно: действие Playwright возвращает Promise.
  • await достаёт результат из Promise и фиксирует порядок шагов.
  • await только внутри async; забытый await кладёт в переменную обещание, не страницу.
  • Локатор (getByRole) без await; click / fill / goto / expect(locator) — с await.
  • Auto-waiting ждёт готовности элемента; sleep вместо него не нужен и не заменяет await.
  • Фабрика данных синхронная; шаг в UI — async.
  • Два пользователя — два browser.newContext(), не вторая вкладка той же сессии.
  • Ожидаемая ошибка — нормальный assert; порядок await может быть частью гонки.

Домашнее задание

Индивидуальная проверка ДЗ — на Boosty

Конспект, видео и тест открыты всем. Текст задания и проверка работы — по подписке.

Индивидуальная проверка ДЗ на Boosty
База для QA — вопросы на собеседование, тренажёры и материалы