Утверждения (expect)

Урок: Утверждения (expect)

Введение

Представь, что ты проверяешь форму логина вручную. Ты вводишь данные, нажимаешь кнопку и… что дальше?

Ты смотришь:

  • появилась ли надпись «Успешно»;
  • открылся ли нужный экран;
  • исчезла ли ошибка.

То есть ты не просто выполняешь действия — ты проверяешь результат.

В автоматизации это ключевой момент. Если тест только кликает и вводит данные, но ничего не проверяет — он бесполезен.

Именно для проверок в Playwright используются утверждения — expect.

Они позволяют ответить на главный вопрос любого теста: «всё прошло правильно или нет?»

Официальный обзор: Assertions | Playwright. Про таймауты проверок и теста — Test timeouts. Список «обычных» матчеров (числа, объекты, строки) — Generic assertions; для DOM — Locator assertions и Page assertions.


Что такое expect

expect — это способ проверить, что состояние страницы (или произвольное значение) соответствует ожиданиям.

Два семейства:

  1. Общие матчерыexpect(значение).toEqual(...), toBeTruthy() и т.д. Они синхронны, автоматически не ждут изменений на странице.
  2. Веб-матчеры у локатора и страницыawait expect(locator).toHaveText(...), await expect(page).toHaveURL(...). Они асинхронны: Playwright многократно перечитывает DOM, пока условие не выполнится или не истечёт таймаут (по умолчанию для таких проверок обычно 5 секунд, см. use.expect.timeout в конфиге).

Пример с автоповтором:

await expect(page.getByText('Успешно')).toBeVisible();

Разберём:

  • expect(...) — «ожидаем, что…»;
  • toBeVisible() — элемент должен стать видимым;
  • await обязателен: внутри идёт ожидание и повторные попытки.

Если условие выполняется — тест проходит. Если нет — тест падает с логом последних попыток.


Почему expect важнее, чем кажется

Рассмотрим два теста.

Без expect:

await page.getByRole('button', { name: 'Отправить' }).click();

Этот тест ничего не проверяет. Даже если ничего не произошло — он «пройдёт».

С expect:

await page.getByRole('button', { name: 'Отправить' }).click();
await expect(page.getByText('Успешно')).toBeVisible();

Теперь тест:

  • выполняет действие;
  • проверяет результат.

И только теперь он имеет смысл.


Автоматические ожидания внутри expect

Одна из самых сильных сторон Playwright — это встроенные ожидания.

await expect(page.getByText('Успешно')).toBeVisible();

Что происходит:

  • Playwright не проверяет сразу;
  • он ждёт, пока текст появится;
  • проверяет условие;
  • если не появилось — падает с ошибкой.

Это избавляет от ручных ожиданий:

// так делать не нужно
await page.waitForTimeout(2000);

Утверждения над локатором (с автоповтором)

Ниже — основные асинхронные матчеры из таблицы auto-retrying assertions. Их вызывают как await expect(locator).….

Видимость и положение в DOM

await expect(page.getByRole('alert')).toBeVisible();
await expect(page.getByText('Черновик')).toBeHidden(); // не виден
await expect(page.getByTestId('modal')).toBeAttached(); // есть в DOM, даже если скрыт
await expect(page.getByRole('navigation')).toBeInViewport(); // пересекается с вьюпортом

Состояние контролов (форма, фокус, доступность клика)

await expect(page.getByRole('button', { name: 'Отправить' })).toBeEnabled();
await expect(page.getByRole('button', { name: 'Сохранить' })).toBeDisabled();
await expect(page.getByRole('textbox', { name: 'Комментарий' })).toBeEditable();

await expect(page.getByRole('checkbox', { name: 'Согласен' })).toBeChecked();
await expect(page.getByRole('checkbox', { name: 'Опция' })).not.toBeChecked();

await expect(page.getByLabel('Поиск')).toBeFocused();
await expect(page.getByRole('list')).toBeEmpty(); // контейнер без детей

Текст (точное совпадение и вхождение)

await expect(page.getByRole('heading')).toHaveText('Добро пожаловать');
await expect(page.getByRole('status')).toHaveText(/успешн/i);

// локатор сразу на несколько узлов — массив ожидаемых текстов по порядку
await expect(page.getByRole('listitem')).toHaveText(['Товар A', 'Товар B', 'Товар C']);

await expect(page.getByRole('paragraph')).toContainText('часть фразы');

Опции вроде { ignoreCase: true } и таймаут см. в toHaveText.


Значения полей и <select>

await expect(page.getByLabel('Email')).toHaveValue('test@mail.com');

// несколько выбранных option (multiple)
await expect(page.getByLabel('Теги')).toHaveValues([/bug/i, 'docs']);

Роли и доступное имя (a11y)

await expect(page.getByTestId('widget')).toHaveRole('dialog');
await expect(page.getByRole('button')).toHaveAccessibleName('Закрыть');
await expect(page.getByRole('textbox')).toHaveAccessibleDescription(/минимум 8 символов/);
await expect(page.getByRole('navigation')).toMatchAriaSnapshot(`
  - navigation:
    - link "Главная"
`);

toMatchAriaSnapshot удобен для стабильной сверки дерева доступности; формат — в документации.


Атрибуты, классы, стили, id

await expect(page.getByRole('link', { name: 'Документация' })).toHaveAttribute('href', /docs\//);
await expect(page.locator('.pill')).toHaveClass(/active/);
await expect(page.locator('.badge')).toContainClass('badge--new');
await expect(page.getByRole('textbox')).toHaveId('email-field');

await expect(page.getByTestId('banner')).toHaveCSS('background-color', 'rgb(0, 128, 0)');
await expect(page.locator('canvas')).toHaveJSProperty('width', 800);

Количество совпадений локатора

await expect(page.getByRole('listitem')).toHaveCount(3);

Полезно для списков, строк таблицы, карточек.


Скриншот элемента

await expect(page.getByTestId('chart')).toHaveScreenshot('chart.png');

Первый прогон может записать эталон; дальше сравнивается пиксельно (настройки — в доке по toHaveScreenshot).


Утверждения над страницей (page)

await expect(page).toHaveURL(/\/dashboard/);
await expect(page).toHaveTitle('Кабинет — Мой сервис');

await expect(page).toHaveScreenshot({ fullPage: true });

Ответ HTTP (APIResponse)

После page.goto или request.get можно проверить статус:

const response = await page.goto('https://example.com');
await expect(response).toBeOK();

См. APIResponseAssertions.


Отрицание (not)

await expect(page.getByText('Ошибка')).not.toBeVisible();
await expect(page.getByRole('textbox', { name: 'Логин' })).not.toHaveValue('');

Таймаут и своё сообщение об ошибке

Увеличить ожидание для одной проверки:

await expect(page.getByText('Готово')).toBeVisible({ timeout: 15_000 });

Второй аргумент у expect — текст для отчёта (видно и при успехе, и при падении):

await expect(page.getByRole('banner'), 'пользователь должен быть залогинен').toContainText('Анна');

Глобально таймаут проверок задаётся в playwright.configuse.expect.timeout (см. Test timeouts).


Мягкие проверки (expect.soft)

По умолчанию при падении expect тест сразу останавливается. expect.soft помечает ошибку, но даёт дойти до следующих строк — удобно собрать несколько независимых проверок за один прогон:

await expect.soft(page.getByTestId('status')).toHaveText('Успех');
await expect.soft(page.getByTestId('eta')).toHaveText('1 день');
await page.getByRole('link', { name: 'Дальше' }).click();

Работает только в раннере @playwright/test. После блока soft-проверок можно явно проверить, не было ли ошибок: expect(test.info().errors).toHaveLength(0) — см. Soft assertions.


Свой expect: expect.configure

Общий таймаут или режим soft для группы проверок:

const slowExpect = expect.configure({ timeout: 10_000 });
await slowExpect(page.getByText('Отправлено')).toBeVisible();

const softExpect = expect.configure({ soft: true });
await softExpect(page.getByRole('status')).toHaveText('OK');

Опрос до условия: expect.poll

Когда нужно ждать не DOM, а произвольное условие (например, API ответил 200):

await expect
  .poll(
    async () => {
      const res = await page.request.get('https://api.example.com/health');
      return res.status();
    },
    { message: 'API должен ожить', timeout: 10_000 },
  )
  .toBe(200);

Интервалы между опросами настраиваются опцией intervals. Подробнее — expect.poll.


Повтор блока кода: expect.toPass

Несколько обычных expect внутри функции выполняются снова и снова, пока все не пройдут:

await expect(async () => {
  const res = await page.request.get('https://api.example.com/health');
  expect(res.status()).toBe(200);
}).toPass({ timeout: 60_000, intervals: [1_000, 2_000, 5_000] });

По умолчанию у toPass своё поведение по таймауту — см. expect.toPass.


«Обычные» матчеры без автоповтора

Для чисел, объектов, массивов, функций используй синхронные матчеры из Generic assertions: toEqual, toStrictEqual, toContain, toMatch, toHaveProperty, toBeGreaterThan, toThrow и др. Они один раз сравнивают значение — для UI это часто хуже, чем await expect(locator)…, потому что страница может обновиться чуть позже.

Асимметричные матчеры (expect.objectContaining, expect.stringMatching, …) — в доке.


Свои матчеры: expect.extend

Можно добавить, например, toHaveAmount через expect.extend и экспортировать общий expect из fixtures.ts — шаблон в документации. Несколько модулей с матчерами объединяют через mergeExpects.

Важно: не путай expect из @playwright/test с библиотекой Jest expect — для тестов Playwright нужен встроенный expect, иначе потеряются автоповтор и интеграция с отчётами.


Пример полного сценария

await page.goto('https://example.com/login');

await page.getByLabel('Email').fill('test@mail.com');
await page.getByLabel('Пароль').fill('secret');

await page.getByRole('button', { name: 'Войти' }).click();

await expect(page.getByRole('heading'), 'после входа виден кабинет').toHaveText(
  /добро пожаловать/i,
);

Здесь:

  1. выполняются действия пользователя;
  2. затем проверяется результат.

Это правильная структура теста.


Разница между expect и ручной проверкой

Плохо:

const text = await page.textContent('.message');

if (text !== 'Успешно') {
  throw new Error('Ошибка');
}

Проблемы:

  • нет ожидания;
  • код громоздкий;
  • ошибки менее информативные.

Лучше:

await expect(page.getByText('Успешно')).toBeVisible();

Playwright:

  • сам ждёт;
  • даёт понятную ошибку;
  • делает код чище.

Частые ошибки

Ошибка 1 — отсутствие проверок:

await page.getByRole('button', { name: 'OK' }).click();
// дальше ни одного expect — тест «зелёный», но ничего не гарантирует

Ошибка 2 — разовое чтение DOM вместо автоповторя у expect(locator):

const text = await page.locator('.message').textContent();
expect(text).toBe('Готово'); // гонка: сообщение могло ещё не обновиться

Ошибка 3 — waitForTimeout вместо ожидания условия:

await page.waitForTimeout(2000);

Ошибка 4 — ждать UI через «голый» expect без локатора там, где нужен DOM:

expect(await page.locator('.x').isVisible()).toBe(true); // хрупко; лучше await expect(locator).toBeVisible()

Где это используется на практике

Утверждения используются в каждом тесте:

  • проверка логина;
  • проверка отображения данных;
  • проверка ошибок;
  • проверка переходов между страницами.

Без expect невозможно понять, работает ли приложение правильно.


Итоговое понимание

expect превращает сценарий в проверяемый тест.

Для интерфейса опирайся на асинхронные матчеры локатора и страницы из Assertions: видимость, текст, значения, роли, атрибуты, количество, URL, заголовок, скриншоты, toMatchAriaSnapshot. Для произвольной логики — expect.poll и expect.toPass. Для нескольких независимых проверок подряд — expect.soft и кастомное сообщение у expect.

Хороший e2e-тест явно отвечает на вопрос «стало ли состояние таким, как нужно» — в первую очередь через await expect(…) к DOM и странице, а не через случайные таймауты.


Веб-страница из видео

Демо для отработки действий с элементами: Assertions — демо-страница.