page.getByText('Success').isVisible() возвращает булево значение проверяя DOM ровно один раз: если элемент ещё не отрендерился, получаешь false даже если он появится через 200 мс. expect(page.getByText('Success')).toBeVisible() повторяет попытки до 5 секунд перед тем как упасть. Это поведение с повторными попытками и есть основа дизайна ассёртов в Playwright, а большинство ошибок у начинающих происходит из незнания у каких методов оно есть. Гайд охватывает все типы ассёртов, отрицание через not, мягкие ассёрты для сбора всех сбоев, и паттерны которые молча отключают авто-повтор.
Как работают ассёрты в Playwright
Ассёрты Playwright используют функцию expect() из @playwright/test. Это не то же самое что expect в Jest. Версия Playwright асинхронная и имеет встроенную логику повторных попыток.
import { test, expect } from '@playwright/test';Ключевое отличие от большинства тест-фреймворков: ассёрты Playwright автоматически повторяют проверку. Когда пишешь:
await expect(page.getByText('Welcome')).toBeVisible();Playwright не проверяет один раз. Он проверяет многократно до 5 секунд (дефолтный expect timeout), ожидая пока условие не станет истинным. Это устраняет необходимость ручных вызовов waitFor в 90% случаев.
Если условие не становится истинным в рамках таймаута, тест падает с понятным сообщением: что ожидалось и что реально существовало.
Ассёрты элементов (на основе локаторов)
Проверяют свойства конкретного элемента на странице.
toBeVisible / toBeHidden
// Элемент отрендерен и виден пользователю
await expect(page.getByText('Dashboard')).toBeVisible();
// Элемент отсутствует или присутствует но скрыт (display:none, visibility:hidden, opacity:0)
await expect(page.getByRole('dialog')).toBeHidden();toBeHidden() истинен если элемент не существует ИЛИ существует но невидим. Используй not.toBeAttached() если нужно конкретно подтвердить что элемента нет в DOM вообще.
toHaveText / toContainText
// Точное совпадение текста (пробелы обрезаются автоматически)
await expect(page.getByRole('heading', { level: 1 })).toHaveText('My Travel Items');
// Частичное совпадение
await expect(page.getByRole('heading')).toContainText('Travel');
// Массив: проверяем текст нескольких элементов
await expect(page.getByRole('listitem')).toHaveText(['Tokyo', 'Paris', 'London']);
// Regex: совпадение по паттерну
await expect(page.getByTestId('price')).toHaveText(/\$\d+\.\d{2}/);toHaveText с массивом проверяет полный текст каждого элемента по порядку. Очень полезно для проверки строк таблиц или отсортированных списков.
toHaveValue
Для элементов , , :
// Текстовый инпут
await expect(page.getByLabel('Email')).toHaveValue('admin@becomeqa.com');
// Пустой инпут
await expect(page.getByLabel('Search')).toHaveValue('');
// Выпадающий список
await expect(page.getByLabel('Status')).toHaveValue('completed');toBeChecked / not.toBeChecked
Для чекбоксов и радиокнопок:
await expect(page.getByLabel('Remember me')).toBeChecked();
await expect(page.getByLabel('Subscribe to newsletter')).not.toBeChecked();toBeEnabled / toBeDisabled
// Кнопка доступна для нажатия
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
// Кнопка задизейблена или имеет атрибут disabled
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled();Используй для проверки что кнопка submit формы становится активной только после заполнения обязательных полей.
toBeEditable / toBeReadOnly
await expect(page.getByLabel('Username')).toBeEditable();
await expect(page.getByLabel('Created At')).toBeReadOnly();toHaveAttribute
Проверяет любой HTML-атрибут:
// href у ссылки
await expect(page.getByRole('link', { name: 'Home' })).toHaveAttribute('href', '/');
// data-testid
await expect(page.getByTestId('status-badge')).toHaveAttribute('data-status', 'active');
// aria-label
await expect(page.getByRole('button', { name: 'Close' })).toHaveAttribute('aria-label', 'Close dialog');
// Совпадение значения по regex
await expect(page.getByRole('img')).toHaveAttribute('src', /\/images\/.+\.svg/);toHaveClass
// Элемент имеет этот CSS-класс
await expect(page.getByTestId('alert')).toHaveClass(/error/);
// Точный список классов
await expect(page.getByTestId('button')).toHaveClass('btn btn-primary active');toHaveClass проверяет что класс присутствует, не что он единственный. Используй regex для частичных совпадений.
toHaveCount
Для коллекций элементов:
// Таблица: заголовок + 5 строк данных
await expect(page.getByRole('row')).toHaveCount(6);
// Выпадающий список: 4 варианта
await expect(page.getByRole('option')).toHaveCount(4);
// Нет сообщений об ошибках
await expect(page.getByRole('alert')).toHaveCount(0);toHaveCSS
Проверяет вычисленные CSS-свойства:
await expect(page.getByTestId('error-message')).toHaveCSS('color', 'rgb(220, 38, 38)');
await expect(page.getByRole('dialog')).toHaveCSS('display', 'flex');Используй вычисленные значения (rgb(...)) а не CSS-переменные или сокращённые свойства.
Ассёрты страницы
Проверяют свойства всей страницы, а не конкретного элемента.
toHaveURL
// Точный URL
await expect(page).toHaveURL('https://lab.becomeqa.com/dashboard');
// Частичное совпадение через regex
await expect(page).toHaveURL(/\/dashboard/);
// С baseURL настроенным в playwright.config.ts
await expect(page).toHaveURL('/dashboard');Используй после навигации чтобы подтвердить что попал куда нужно.
toHaveTitle
await expect(page).toHaveTitle('My Travel Items | BecomeQA Lab');
await expect(page).toHaveTitle(/BecomeQA/);toHaveScreenshot (визуальная регрессия)
// Первый запуск создаёт референсный скриншот
// Последующие сравнивают с ним
await expect(page).toHaveScreenshot('dashboard.png');
// С опциями
await expect(page).toHaveScreenshot('dashboard.png', {
maxDiffPixels: 100, // допустимые незначительные пиксельные отличия
threshold: 0.2, // шкала 0–1 допустимого различия цветов
});Визуальное регрессионное тестирование: первый запуск сохраняет референсное изображение. Каждый последующий сравнивает текущее состояние с этим референсом. Падает если различие превышает порог.
Ассёрты API-ответов
При использовании фикстуры request для API-тестирования:
test('GET /api/items returns valid data', async ({ request }) => {
const response = await request.get('/api/items');
// Код статуса
expect(response.status()).toBe(200);
expect(response.ok()).toBeTruthy(); // true для 200–299
// Тело ответа
const items = await response.json();
expect(items).toHaveLength(5);
expect(items[0]).toHaveProperty('id');
expect(items[0]).toHaveProperty('title');
expect(items[0].status).toBe('planned');
// Заголовки
expect(response.headers()['content-type']).toContain('application/json');
});Встроенный метод response.ok() (не ассёрт) возвращает true для 2xx статус-кодов.
Ассёрты обычных значений
Для не-элементных значений: переменных, API-ответов, вычисленных значений:
// Равенство
expect(items.length).toBe(5);
expect(user.role).toBe('admin');
// Неравенство
expect(errorCode).not.toBe(0);
// Truthy/falsy
expect(isVisible).toBeTruthy();
expect(errorMessage).toBeFalsy();
// Null/undefined
expect(result).toBeNull();
expect(result).not.toBeNull();
expect(result).toBeDefined();
expect(result).toBeUndefined();
// Числа
expect(count).toBeGreaterThan(0);
expect(price).toBeGreaterThanOrEqual(9.99);
expect(discount).toBeLessThan(100);
// Массивы
expect(statuses).toContain('completed');
expect(items).toHaveLength(3);
expect(tags).toEqual(['qa', 'automation', 'playwright']); // точное совпадение массива
// Объекты
expect(user).toMatchObject({ email: 'admin@becomeqa.com', role: 'admin' }); // частичное совпадение
expect(user).toEqual({ id: 1, email: 'admin@becomeqa.com', role: 'admin' }); // точное совпадение
// Строки
expect(message).toContain('success');
expect(slug).toMatch(/^[a-z0-9-]+$/);Отрицание: not
Любой ассёрт можно отрицать через .not:
await expect(page.getByRole('dialog')).not.toBeVisible();
await expect(page.getByText('Error')).not.toBeAttached();
expect(response.status()).not.toBe(404);Мягкие ассёрты: сбор всех сбоев
По умолчанию первый упавший ассёрт останавливает тест. Мягкие ассёрты продолжают выполнение даже после сбоя и сообщают обо всех в конце:
test('dashboard data is correct', async ({ page }) => {
await page.goto('/dashboard');
// Не останавливаются на первом сбое
await expect.soft(page.getByRole('heading')).toHaveText('My Travel Items');
await expect.soft(page.getByRole('row')).toHaveCount(6);
await expect.soft(page).toHaveURL('/dashboard');
// Выбрасывает исключение если любой мягкий ассёрт выше упал
expect(test.info().errors).toHaveLength(0);
});Используй мягкие ассёрты когда нужно увидеть всё что сломано за один прогон, а не только первый сбой.
Кастомные сообщения ассёртов
Когда ассёрт падает, Playwright показывает что ожидалось и что было найдено. Можно добавить кастомное сообщение чтобы сбои легче читались:
await expect(page.getByRole('heading'), 'Страница должна показывать дашборд после входа')
.toHaveText('My Travel Items');
expect(items.length, `Ожидалось 5 элементов, получено ${items.length}`)
.toBe(5);Настройка таймаутов
Дефолтный таймаут ассёрта: 5 секунд. Можно переопределить для конкретного ассёрта или глобально:
// Для конкретного ассёрта (10 секунд для медленной операции)
await expect(page.getByText('Report ready')).toBeVisible({ timeout: 10000 });
// Глобально в playwright.config.ts
export default defineConfig({
expect: {
timeout: 10000, // все ассёрты ждут до 10 секунд
},
});Не увеличивай таймауты чтобы исправить нестабильные тесты. Нестабильный тест с большим таймаутом остаётся нестабильным. Просто падает медленнее. Исправляй первопричину.
Типичные ошибки
Использование page.locator().isVisible() вместо expect().toBeVisible()
// Неправильно: проверяет один раз, нет повторных попыток, возвращает булево
const visible = await page.getByText('Success').isVisible();
expect(visible).toBe(true);
// Правильно: повторяет попытки до появления или таймаута
await expect(page.getByText('Success')).toBeVisible();Первый вариант может нестабильно падать потому что проверяет ровно один раз. Второй повторяет попытки.
Ассёрты устаревших локаторов
// Не сохраняй локаторы до навигации и не ассёрти их после
const heading = page.getByRole('heading');
await page.goto('/new-page');
await expect(heading).toHaveText('New Page'); // может быть устаревшим
// Лучше: создавай локатор рядом с ассёртом
await page.goto('/new-page');
await expect(page.getByRole('heading')).toHaveText('New Page');Использование expect(await locator.textContent()).toBe(...) вместо toHaveText
// Неправильно: вычисляется один раз, нет повторных попыток
expect(await page.getByRole('heading').textContent()).toBe('Dashboard');
// Правильно: повторяет попытки с авто-ожиданием
await expect(page.getByRole('heading')).toHaveText('Dashboard');Проверка количества на динамических списках
Если список загружается асинхронно, ассёрти количество после того как список стал видимым:
await expect(page.getByRole('list')).toBeVisible(); // ждём появления списка
await expect(page.getByRole('listitem')).toHaveCount(5); // затем считаем элементыПолный тест с несколькими типами ассёртов
import { test, expect } from '@playwright/test';
test('user can add and view a travel item', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
// Ассёрт страницы: проверяем навигацию
await expect(page).toHaveURL('/dashboard');
// Ассёрт видимости
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
// Ассёрт текста
await expect(page.getByRole('heading', { level: 1 })).toHaveText('My Travel Items');
// Добавляем новый элемент
await page.getByRole('button', { name: 'Add Item' }).click();
await page.getByLabel('Destination').fill('Tokyo');
await page.getByRole('button', { name: 'Save' }).click();
// Ассёрт количества: в таблице должно стать на одну строку больше
const rows = page.getByRole('row');
await expect(rows).toHaveCount(7); // заголовок + 6 элементов
// Ассёрт содержимого новой строки
await expect(page.getByRole('cell', { name: 'Tokyo' })).toBeVisible();
// Ассёрт статуса нового элемента
const tokyoRow = page.getByRole('row').filter({ hasText: 'Tokyo' });
await expect(tokyoRow.getByRole('cell').last()).toHaveText('Planned');
});FAQ
Почему ассёрт проходит локально но падает в CI?
Таймингии. Машины CI медленнее. Элемент существует но дольше появляется. Увеличь таймаут ассёрта в playwright.config.ts или разберись почему элемент медленно загружается в CI.
В чём разница между toEqual и toBe?
toBe проверяет равенство по ссылке (один и тот же объект в памяти, или идентичные примитивы). toEqual проверяет глубокое равенство (одинаковая структура и значения, работает для объектов и массивов). Для сравнения объектов и массивов используй toEqual. Для строк, чисел и булевых: toBe.
Когда использовать toMatchObject вместо toEqual?
toMatchObject: частичное совпадение. Реальный объект может иметь больше свойств чем указано. toEqual требует точного совпадения. Для API-ответов где нужно проверить ключевые поля без перечисления всех используй toMatchObject.
toHaveText падает из-за лишних пробелов в реальном тексте. Как исправить?
toHaveText автоматически обрезает ведущие и завершающие пробелы. Для внутренних пробелов (множественные пробелы, переносы строк) используй regex: toHaveText(/destination:\s+Tokyo/i).
→ See also: Кастомные фикстуры в Playwright: паттерн, который делает тесты читаемыми | Отладка нестабильных тестов: практическое руководство