Flaky‑тесты — бич E2E‑автоматизации. Команда перезапускает пайплайн, пока не позеленеет. Доверие к тестам падает. В итоге CI‑статус игнорируется, и баг всё равно попадает в прод.
Playwright — фреймворк от Microsoft для E2E‑тестирования — был построен с нуля, чтобы решить именно эту проблемную. В нем есть автоматические ожидания, изоляция через Browser Contexts и встроенный тест‑раннер. Разберем, чем он отличается от Selenium и Cypress, и как писать тесты, которые не падают от ветра.
Почему тесты флакуют: корень проблемы
90% flaky‑тестов — это гонки между тестом и браузером. Тест говорит «кликни кнопку», а кнопка ещё не появилась в DOM. Или появилась, но ещё не стала кликабельной (перекрыта оверлеем). Или кликабельна, но обработчик click ещё не привязан — React не завершил гидрацию.
Тот же Selenium решает это через явные ожидания (WebDriverWait, Expected Conditions). Разработчик вручную пишет: «подожди, пока элемент станет видимым, максимум 10 секунд». Забыл написать — тест флакует. Написал слишком маленький таймаут — флакует на медленном CI. Написал слишком большой — тесты могут работать вечность.
Playwright подходит иначе: все действия автоматически ждут, пока элемент будет готов. page.click('#submit') внутри себя делает:
-
подождать появления элемента в DOM,
-
подождать видимости, подождать стабильности (элемент перестал двигаться),
-
подождать кликабельности (не перекрыт другим элементом),
-
подождать привязки обработчика, кликнуть.
Весь этот цикл — автоматический, без единой строки ожидания в тестовом коде.
Это фундаментальное архитектурное решение, которое устраняет главную причину нестабильности E2E‑тестов.
Установка и первый тест
npm init playwright@latest
Команда создаёт проект с конфигурацией, примером теста и настроенным playwright.config.ts. Браузеры (Chromium, Firefox, WebKit) скачиваются автоматически — не нужно ставить отдельно Chrome или geckodriver.
Первый тест (tests/login.spec.js):
const { test, expect } = require('@playwright/test');
test('пользователь может войти в систему', async ({ page }) => {
await page.goto('https://app.example.com/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'secret123');
await page.click('button[type="submit"]');
// Проверяем, что попали на дашборд
await expect(page).toHaveURL(/dashboard/);
await expect(page.locator('h1')).toHaveText('Добро пожаловать');
});
page.fill('#email', ...) — автоматически ждёт появления инпута, очищает его и вводит текст. page.click('button...') — ждёт кликабельности. expect(page).toHaveURL(...) — ждёт, пока URL изменится. Ни одного sleep, waitFor или setTimeout.
Запуск:
npx playwright test # Все тесты
npx playwright test login.spec # Конкретный файл
npx playwright test --headed # С видимым браузером (для отладки)
npx playwright test --ui # Интерактивный UI-режим
Локаторы: как находить элементы надёжно
Главная причина хрупких тестов после гонок — хрупкие селекторы. div.container > div:nth‑child(3) > button.primary сломается при любом изменении вёрстки. Playwright предлагает user‑facing локаторы — поиск элементов так, как их находит пользователь:
// По тексту кнопки (пользователь видит текст, а не CSS-класс)
page.getByRole('button', { name: 'Отправить' });
// По label инпута
page.getByLabel('Email');
// По placeholder
page.getByPlaceholder('Введите имя');
// По тексту ссылки
page.getByRole('link', { name: 'Личный кабинет' });
// По test-id (для элементов без видимого текста)
page.getByTestId('cart-counter');
getByRole — самый надёжный. Он ищет по ARIA‑роли и accessible name, то есть по тому, как элемент воспринимается пользователем (и скринридером). Вёрстка может меняться, CSS‑классы переименовываться, а getByRole('button', { name: 'Отправить' }) продолжит работать, пока кнопка называется «Отправить».
getByTestId — для кейсов, где текстовый локатор невозможен (иконки, динамический контент). Атрибут data-testid добавляется специально для тестов и не меняется при рефакторинге.
test('добавление товара в корзину', async ({ page }) => {
await page.goto('/catalog');
// Находим карточку товара по названию
const productCard = page.locator('.product-card')
.filter({ hasText: 'Ноутбук Pro 16' });
// Кликаем кнопку внутри карточки
await productCard.getByRole('button', { name: 'В корзину' }).click();
// Проверяем счётчик
await expect(page.getByTestId('cart-counter')).toHaveText('1');
});
locator.filter({ hasText: ... }) — фильтрация по содержимому. Позволяет найти конкретную карточку среди десятков одинаковых по структуре.
Автоматические ожидания: как это работает
Каждое действие Playwright проходит серию проверок перед выполнением. Для click:
-
Элемент присутствует в DOM (attached).
-
Элемент видим (visible) — не
display: none, неvisibility: hidden, не нулевой размер. -
Элемент стабилен — не анимируется, не двигается (два последовательных кадра в одной позиции).
-
Элемент принимает события — не перекрыт другим элементом (overlay, modal, tooltip).
-
Элемент не disabled.
Если любое условие не выполнено, Playwright ждёт. По умолчанию до 30 секунд (настраивается). Если за это время условие не выполнилось — тест падает с понятным сообщением: «Element is not visible» или «Element is covered by another element».
Для expect тоже работают автоматические ожидания:
// Ждёт, пока текст элемента станет 'Готово' (а не проверяет мгновенно)
await expect(page.locator('#status')).toHaveText('Готово');
// Ждёт, пока элемент исчезнет
await expect(page.locator('.spinner')).not.toBeVisible();
// Ждёт, пока количество элементов станет 3
await expect(page.locator('.item')).toHaveCount(3);
Каждый expect с await — это polling assertion. Playwright проверяет условие, если не выполнено — ждёт, проверяет снова. До таймаута. Это заменяет паттерн waitUntil + assert, который в Selenium пишется вручную.
Browser Contexts: изоляция без overhead
Каждый тест в Playwright запускается в своём Browser Context. Это как отдельный профиль браузера: свои cookies, свой localStorage, свои сессии. Но без запуска нового процесса браузера — контексты легковесные, создаются за миллисекунды.
test('первый пользователь видит свои данные', async ({ page }) => {
// page уже в свежем контексте — нет cookies от других тестов
await page.goto('/profile');
});
test('второй пользователь видит свои данные', async ({ page }) => {
// другой контекст — полностью изолирован от первого теста
await page.goto('/profile');
});
В Selenium изоляция — больная тема. Общий профиль браузера, cookies протекают между тестами, localStorage не чистится. Отсюда классические flaky: тест проходит поодиночке, но падает в пачке, потому что предыдущий тест оставил авторизационную cookie.
В Playwright это невозможно по конструкции. Каждый тест — чистый лист.
Параллелизм из коробки
npx playwright test --workers=4
Playwright запускает тесты параллельно. Каждый worker — отдельный процесс. Тесты изолированы через Browser Contexts, поэтому параллелизм безопасен без дополнительных усилий. На CI с 4 ядрами — ускорение в ~3.5 раза.
Для тестов, которые не должны параллелиться (общая база данных, общий стейт), есть режим serial:
test.describe.serial('checkout flow', () => {
test('добавить товар', async ({ page }) => { /* ... */ });
test('оформить заказ', async ({ page }) => { /* ... */ });
test('проверить подтверждение', async ({ page }) => { /* ... */ });
});
Network interception: моки и перехват
Playwright может перехватывать и подменять сетевые запросы. Это устраняет зависимость тестов от внешних сервисов:
test('показывает ошибку при недоступном API', async ({ page }) => {
// Перехватываем запрос к API и возвращаем ошибку
await page.route('**/api/orders', route =>
route.fulfill({ status: 500, body: 'Internal Server Error' })
);
await page.goto('/orders');
await expect(page.locator('.error-message'))
.toHaveText('Не удалось загрузить заказы');
});
test('показывает список заказов', async ({ page }) => {
// Подменяем данные
await page.route('**/api/orders', route =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, title: 'Заказ #1', amount: 15000 },
{ id: 2, title: 'Заказ #2', amount: 23000 }
])
})
);
await page.goto('/orders');
await expect(page.locator('.order-card')).toHaveCount(2);
});
Тесты детерминированы: не зависят от состояния бэкенда, не флакуют из‑за медленного API, не требуют seed‑данных в базе.
Trace и отладка: когда тест всё‑таки упал
// playwright.config.js
module.exports = {
use: {
trace: 'on-first-retry', // Записывать trace при повторе упавшего теста
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
retries: 1, // Один повтор для упавших тестов
};
Trace — запись всех действий теста: скриншоты, DOM‑снапшоты, сетевые запросы, логи консоли, таймлайн. Открывается в Trace Viewer:
npx playwright show-trace trace.zip
Trace Viewer показывает пошаговый реплей теста: что было на экране на каждом шаге, какие запросы отправлялись, какой DOM был в момент клика.
screenshot: 'only-on-failure' — скриншот при падении теста. Прикладывается к отчёту CI. video: 'retain-on-failure' — видеозапись всего теста при падении.
Codegen: генерация тестов записью
npx playwright codegen https://app.example.com
Открывается браузер и инспектор. Вы кликаете, заполняете формы, навигируете. Playwright записывает действия и генерирует тестовый код. Результат не идеальный, но рабочий скелет теста, который останется отредактировать: заменить CSS‑селекторы на getByRole, добавить assertions, убрать лишние шаги.
Page Object Model: организация кода
Для проекта с 50+ тестами — Page Object обязателен:
// pages/login-page.js
class LoginPage {
constructor(page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Пароль');
this.submitButton = page.getByRole('button', { name: 'Войти' });
this.errorMessage = page.locator('.login-error');
}
async goto() {
await this.page.goto('/login');
}
async login(email, password) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
module.exports = { LoginPage };
// tests/login.spec.js
const { test, expect } = require('@playwright/test');
const { LoginPage } = require('../pages/login-page');
test('успешный логин', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'secret123');
await expect(page).toHaveURL(/dashboard/);
});
test('ошибка при неверном пароле', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'wrong');
await expect(loginPage.errorMessage).toBeVisible();
await expect(loginPage.errorMessage).toHaveText('Неверный пароль');
});
Локаторы описаны в одном месте. Изменилась вёрстка страницы логина — меняете один файл, а не двадцать тестов.
CI/CD: GitHub Actions за 5 минут
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
playwright install --with-deps — ставит браузеры и системные зависимости (шрифты, медиа‑кодеки). Артефакт playwright-report — HTML‑отчёт со скриншотами и trace‑файлами. Всё, что нужно для разбора падений, доступно прямо в CI.
Playwright не исключает flaky‑тесты полностью. Если тест зависит от внешнего API без мока, от стейта базы данных или от системного времени — он будет нестабильным в любом фреймворке. Но Playwright убирает целый класс проблем, которые в Selenium и Cypress создают ту самую базовую нестабильность: гонки с DOM, протечки между тестами, ненадёжные ожидания. И это делает E2E‑тесты тем, чем они должны были быть всегда — защитной сетью, а не источником ложных срабатываний.

Поделиться практическими подходами и показать, как уже сегодня применять искусственный интеллект в тестировании, мы планируем на открытом уроке «Искусственный интеллект для тестировщика: инструменты, которые уже меняют профессию», который пройдёт 16 апреля в 20:00 в рамках курса «Автоматизатор тестирования на JavaScript». Мы разберём реальные сценарии использования и посмотрим, как это влияет на повседневную работу тестировщика. Регистрируйтесь на странице курса.
Сейчас действует скидка 15% за прохождение тестирования — это хороший момент, чтобы оценить свой уровень в E2E‑тестировании на JavaScript и работе с Playwright и заодно получить более выгодные условия. ☛
[Пройти тест]
Автор: badcasedaily1


