К списку уроков
Блок 4 · Playwright — AAA-структура теста

Урок 9. Assert — проверки в автотестах

expect и retrying assertions

Темы урока

Библиотека expect, retrying assertions, матчеры (toBeVisible, toHaveValue, toContainText, toHaveCount, toBeEnabled, toHaveURL), негатив через .not, soft assertions, что проверять после Act

Видео урока

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

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

Конспект — Урок 9: Тест зелёный — а в проде баг

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

Третья буква AAA — Assert. Это ожидаемый результат из тест-кейса: что пользователь должен увидеть после Act. Не «галочка в конце файла» и не «код не упал».

Урок 6 находил узел. Урок 7 готовил сцену. Урок 8 кликал и заполнял. Сегодня Playwright должен подтвердить, что продукт сделал то, что обещал.

Без Assert автотест проверяет только, что шаги выполнились. Клик прошёл, fill отработал — тест зелёный. В проде поле пустое.

Практика — живой profile-flow.spec.ts. Отдельного Assert Lab нет.


Где кончается Act и начинается Assert

Ручной кейс: предусловия / шаги / ожидаемый. Assert = ожидаемый.

Часть теста Что это Пример
Arrange Сцена до действия makeUser + регистрация + goto профиля
Act То, что проверяем fill + click «Сохранить» / «Добавить»
Assert Что должно быть на экране поле содержит новое имя; чип виден; пустой ввод — чипа нет

click и fill не гарантируют успех: сервер мог не сохранить, UI мог не обновиться. Один дымовой expect с Урока 8 — только «что-то изменилось». Сегодня проверка привязана к смыслу кейса.

Один expect — один смысл. Двенадцать матчеров на один клик — простыня: чинишь по одному, теряешь время.


await expect и забытый await

expect возвращает промис. Без await тест не ждёт проверку: идёт дальше и часто остаётся зелёным, хотя assert не выполнился.

// плохо — промис «ушёл», проверка могла не случиться
const x = expect(page.getByLabel("Имя")).toHaveValue("Анна");

// правильно
await expect(page.getByLabel("Имя")).toHaveValue("Анна");

Правило то же, что с click на Уроке 4 и 8: await перед каждым expect.


Retrying assertions — почему не sleep

Локаторный expect не сравнивает DOM один раз. Он переспрашивает, пока условие не станет true или не истечёт timeout.

UI после клика догоняет — expect ждёт. waitForTimeout(3000) лечит симптом и делает флаки: завтра страница чуть медленнее — снова красный.

Таймаут задаётся в конфиге (expect.timeout) или точечно:

await expect(page.getByRole("button", { name: "Сохранить" })).toBeEnabled({
  timeout: 10_000,
});

Не всё в Playwright — retrying. Обычный expect(2 + 2).toBe(4) сравнивает сразу. Retry — у проверок локатора и страницы (toBeVisible, toHaveURL и т.д.).


Какой матчер для чего

Смотри на экран, не в кишки React. Хорошо: текст, value, видимость, disabled. Плохо: внутренний state, точный CSS-класс, порядок вызовов API.

Видимость и DOM

Матчер Когда использовать Пример
toBeVisible() элемент на странице и виден пользователю кнопка «Сохранить», чип навыка, тост
toBeHidden() скрыт: display: none, нулевой размер или нет в DOM модалка закрылась
not.toBeVisible() того, чего быть не должно, нет на экране чип не появился, ошибка не показалась
toBeAttached() / toBeDetached() узел в DOM / уже убрали — даже если невидим спиннер ушёл из дерева
toBeInViewport() элемент в видимой области, без скролла к нему sticky-хедер, активный таб

toBeVisible ≠ «нашёлся в DOM». Элемент может быть attached и при этом скрыт оверлеем или display: none. Для «пользователь это видит» — toBeVisible. Для «узла больше нет» — toBeDetached или toHaveCount(0).

toBeHidden и not.toBeVisible близки, но не близнецы. toBeHidden проходит и для hidden-в-DOM, и для отсутствующего. Если смысл кейса «этого текста нет» — чаще not.toContainText / not.toBeVisible.

Текст и значения полей

Матчер Когда использовать Пример
toHaveText('Профиль') точный текст целиком заголовок, лейбл, одно слово в чипе
toContainText('Playwright') содержит подстроку или regex чип, алерт, карточка с лишними пробелами
toHaveValue('Анна') value у input / textarea / native select имя, Telegram, «О себе» после save
toHaveValues(['a', 'b']) несколько выбранных option в multi-select фильтры, теги в <select multiple>
toBeEmpty() нет текста и нет детей пустой список, пустой контейнер

Не путать: toHaveValue — то, что лежит в поле. toContainText — то, что написано на экране (чип, заголовок, ошибка). Для <input> после fill почти всегда toHaveValue.

await expect(page.getByLabel("Имя")).toHaveValue("Анна Тест");
await expect(page.getByTestId("can-help-skills")).toContainText("Playwright");

Количество

Матчер Когда использовать Пример
toHaveCount(1) ровно N узлов один чип после добавления
toHaveCount(0) список пуст навыков нет, слотов нет

toHaveCount(0) — позитивная формулировка «ноль штук». not.toBeVisible() — «этого элемента нет на экране». Для списка чаще count; для одного узла — not.toBeVisible.

Состояние элемента

Матчер Когда использовать Пример
toBeEnabled() можно кликнуть / отправить кнопка после валидной формы
toBeDisabled() нельзя действовать «Продолжить» без чекбокса оферты
toBeEditable() в поле можно печатать, не readonly имя в профиле
toBeChecked() checkbox / radio / switch включён «Принимаю условия»
toBeFocused() фокус после Tab или автофокуса первое поле формы

toBeEnabled / toBeDisabled — про actionability с Урока 8, только уже как проверка, не как ожидание перед кликом.

await expect(page.getByRole("button", { name: "Продолжить" })).toBeDisabled();
await page.getByLabel("Принимаю").check();
await expect(page.getByRole("button", { name: "Продолжить" })).toBeEnabled();

Атрибуты, роли, страница

Матчер Когда использовать Пример
toHaveAttribute('href', '/pomidorqa/profile') ссылка, aria-*, data-* пункт меню ведёт туда
toHaveId('email') стабильный id якорь формы
toHaveRole('alert') роль из accessibility tree сообщение об ошибке
toHaveAccessibleName('Сохранить') имя для скринридера кнопка без видимого текста
toHaveClass(/active/) / toContainClass('active') CSS-класс активный таб. Легко стать implementation detail
toHaveCSS('color', 'rgb(225, 29, 72)') computed style редко нужно в UI-тесте
toHaveURL(/\/pomidorqa\/?$/) навигация после submit регистрация, логин
toHaveTitle('PomidorQA') <title> вкладки редирект на другую страницу
await expect(page).toHaveURL(/\/pomidorqa\/?$/);
await expect(page.getByRole("link", { name: "Профиль" })).toHaveAttribute(
  "href",
  "/pomidorqa/profile",
);

toHaveClass и toHaveCSS падают от рефакторинга без бага. Если пользователь видит тот же экран — не проверяй класс .skill-chip.

Ответ API и «голые» значения

Матчер Когда использовать Пример
toBeOK() HTTP-ответ успешный expect(response).toBeOK()
toEqual / toBe / toContain данные, не UI объект из фабрики, длина массива

Обычный expect(user.email).toContain('@') не retrying: сравнил один раз и всё. Для экрана — локаторный expect.

Скриншот

toHaveScreenshot() — пиксельное сравнение. В этом курсе не нужно: ломается от шрифта, ретины и «сдвинулось на 2px». База — состояние UI, не картинка.


Негативные проверки: .not

Негатив — такой же assert: ожидаем, что не случилось. В Playwright это .not перед матчером.

await expect(page.getByText("Навык обязателен")).not.toBeVisible();
await expect(page.getByLabel("Имя")).not.toHaveValue("");
await expect(page.getByTestId("can-help-skills")).not.toContainText("Playwright");
await expect(page.getByRole("button", { name: "Добавить" })).not.toBeEnabled();

Типичные кейсы:

Сценарий Матчер
Элемента нет / скрыт not.toBeVisible()
Текст не появился not.toContainText('...')
Старое значение ушло not.toHaveValue('старое')
Кнопка так и не включилась not.toBeEnabled()
Список пуст toHaveCount(0) или not.toBeVisible() на элемент списка

Пустой ввод: required в HTML блокирует submit — чип не появляется. Тест должен это утвердить, а не «кликнул и пошёл дальше».

.not обязателен в ДЗ этого урока: хотя бы один тест с expect(...).not.*.


expect.soft — собрать ошибки разом

Обычный expect: упал — тест остановился. Остальные проверки даже не бегут.

expect.soft: упал — пошёл дальше, в конце красный со списком. Удобно на форме: Имя, Telegram, «О себе» — чинишь все поля за один прогон.

await expect.soft(page.getByLabel("Имя")).toHaveValue("Анна");
await expect.soft(page.getByLabel("Telegram")).toHaveValue("@anna");
await expect.soft(page.getByLabel("О себе")).toHaveValue("Пишу тесты");

Не замена жёсткому assert. Если страница не та — дальше кликать бессмысленно, там нужен обычный expect. Soft — бонус, не обязательная часть ДЗ.


Антипаттерны

Антипаттерн Почему плохо
Act без Assert зелёный тест при сломанном продукте
Забытый await проверка не выполнилась
waitForTimeout перед expect флаки, лечит симптом
Двенадцать expect на один клик чинишь по одному
Assert в Arrange / в хелпере хук «проверил» то, что должен проверить тест
Implementation details CSS-класс, внутренний state — красный без бага
Только позитив баг «пустое прошло» не ловится

expect живёт в тесте, не в beforeEach и не в функции «кликни Добавить». Границы с Page Object — Урок 10.


Как подойти к домашке

  1. git checkout maingit pull origin main, ветка hw9-<github-username>.
  2. Дописать необходимые автотесты на профиль. Минимум 3 теста.
  3. Один из них — обязательно негативный через expect(...).not.*.
  4. В каждом тесте после Act есть осмысленный expect (value, текст, видимость) — не «кликнул и всё».
  5. arrange-practice и booking-flow не трогать. Без хардкода email. Календарь и бронь не трогаем.
  6. Все тесты зелёные по одному и пачкой → PR в main.

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

  • Assert — ожидаемый результат после Act, не «код не упал».
  • await перед каждым expect; без await проверка может не выполниться.
  • Локаторный expect переспрашивает UI — sleep перед проверкой не нужен.
  • toHaveValue — поле; toContainText / toHaveText — текст на экране.
  • toBeVisible — виден пользователю; toBeAttached — просто есть в DOM.
  • Негатив пишется через .not (not.toBeVisible, not.toContainText, not.toHaveValue).
  • toHaveCount(0) — список пуст; toBeEnabled / toBeDisabled — можно ли действовать.
  • toHaveURL — навигация после submit.
  • expect.soft собирает несколько ошибок; не заменяет жёсткий expect на развилке.
  • Не проверяй CSS-класс и внутренний state — смотри на экран.

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

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

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

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