Playwright Basics
Playwright — фреймворк для E2E-тестирования от Microsoft. Поддерживает Chromium, Firefox, WebKit в одном API. Быстрый, надёжный, с автогенерацией тестов.
Зачем нужно
Playwright решает проблемы Cypress: кросс-браузерность (не только Chrome), работа с несколькими вкладками, iframe, загрузка файлов, мобильные устройства. Автоматическое ожидание, параллельный запуск, trace viewer для отладки.
Где используется
E2E и integration тесты веб-приложений, визуальные сравнения, тестирование на мобильных viewport, автоматизация браузера, scraping.
Предпосылки
Виды тестов, Основы тестирования, async/await
Установка
# Создание нового проекта
npm init playwright@latest
# Или добавление в существующий
npm install -D @playwright/test
npx playwright install # Скачивает браузеры
playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30000,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [['html'], ['list']],
use: {
baseURL: 'http://localhost:3000',
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 14'] } },
],
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: !process.env.CI,
},
});
Скрипты
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed",
"test:e2e:debug": "playwright test --debug",
"test:e2e:codegen": "playwright codegen http://localhost:3000"
}
}
Основные команды
Навигация
import { test, expect } from '@playwright/test';
test('навигация', async ({ page }) => {
await page.goto('/');
await page.goto('/login');
await page.goBack;
await page.goForward;
await page.reload;
});
Locators — поиск элементов
test('locators', async ({ page }) => {
// По роли (рекомендуется! Доступность!)
page.getByRole('button', { name: 'Войти' });
page.getByRole('heading', { name: 'Главная' });
page.getByRole('link', { name: 'О нас' });
page.getByRole('textbox', { name: 'Email' });
// По тексту
page.getByText('Добро пожаловать');
page.getByText(/пожаловать/i); // RegExp
// По label (для форм)
page.getByLabel('Email');
page.getByLabel('Пароль');
// По placeholder
page.getByPlaceholder('Введите email');
// По test-id
page.getByTestId('login-button');
// CSS-селектор (последний вариант)
page.locator('.btn-primary');
page.locator('#submit');
page.locator('[data-testid="card"]');
// Цепочки
page.getByRole('list').getByRole('listitem');
page.locator('.card').filter({ hasText: 'Алиса' });
});
Действия
test('действия', async ({ page }) => {
// Клик
await page.getByRole('button', { name: 'Войти' }).click();
await page.getByRole('link', { name: 'Ссылка' }).click();
// Ввод текста
await page.getByLabel('Email').fill('alice@test.com');
await page.getByLabel('Email').clear();
// Клавиатура
await page.getByLabel('Поиск').press('Enter');
await page.keyboard.press('Escape');
// Чекбокс
await page.getByLabel('Запомнить').check;
await page.getByLabel('Запомнить').uncheck;
// Select
await page.getByLabel('Страна').selectOption('RU');
// Hover
await page.getByRole('button').hover;
// Загрузка файла
await page.getByLabel('Файл').setInputFiles('test.pdf');
});
Assertions
test('assertions', async ({ page }) => {
// Видимость
await expect(page.getByText('Привет')).toBeVisible;
await expect(page.getByTestId('modal')).toBeHidden;
// Текст
await expect(page.getByRole('heading')).toHaveText('Заголовок');
await expect(page.getByRole('heading')).toContainText('Заголов');
// URL
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveURL(/dashboard/);
// Title
await expect(page).toHaveTitle('Мой сайт');
// Значение input
await expect(page.getByLabel('Email')).toHaveValue('alice@test.com');
// CSS-класс
await expect(page.locator('.btn')).toHaveClass(/active/);
// Количество элементов
await expect(page.getByRole('listitem')).toHaveCount(5);
// Атрибут
await expect(page.getByRole('link')).toHaveAttribute('href', '/about');
// Доступен/недоступен
await expect(page.getByRole('button')).toBeEnabled;
await expect(page.getByRole('button')).toBeDisabled;
});
Полный пример: тест авторизации
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Авторизация', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('успешный логин', async ({ page }) => {
await page.getByLabel('Email').fill('alice@test.com');
await page.getByLabel('Пароль').fill('secret123');
await page.getByRole('button', { name: 'Войти' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading')).toContainText('Алиса');
});
test('ошибка при неверном пароле', async ({ page }) => {
await page.getByLabel('Email').fill('alice@test.com');
await page.getByLabel('Пароль').fill('wrong');
await page.getByRole('button', { name: 'Войти' }).click();
await expect(page.getByTestId('error')).toBeVisible;
await expect(page.getByTestId('error')).toHaveText('Неверный пароль');
await expect(page).toHaveURL('/login');
});
});
Codegen — автогенерация тестов
npx playwright codegen http://localhost:3000
Открывается браузер. Кликаете, вводите — Playwright записывает код теста. Идеально для быстрого старта.
Visual Comparisons — визуальные сравнения
test('страница выглядит правильно', async ({ page }) => {
await page.goto('/');
// Скриншот всей страницы
await expect(page).toHaveScreenshot('homepage.png');
// Скриншот элемента
await expect(page.getByTestId('header')).toHaveScreenshot('header.png');
// С допуском
await expect(page).toHaveScreenshot('page.png', {
maxDiffPixels: 100,
});
});
# Первый запуск: создаёт эталонные снапшоты
npx playwright test --update-snapshots
API-запросы в тестах
// Перехват запросов
test('мок API', async ({ page }) => {
await page.route('/api/users', async (route) => {
await route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'Алиса' }]),
});
});
await page.goto('/users');
await expect(page.getByTestId('user-card')).toHaveCount(1);
});
// Прямые API-запросы (без браузера)
test('API тест', async ({ request }) => {
const response = await request.post('/api/login', {
data: { email: 'alice@test.com', password: 'secret123' },
});
expect(response.ok).toBeTruthy();
const body = await response.json();
expect(body.token).toBeDefined();
});
Trace Viewer — отладка
# Включить tracing
npx playwright test --trace on
# Посмотреть trace
npx playwright show-trace trace.zip
Trace Viewer показывает: каждый шаг теста, скриншоты, DOM-снапшоты, network requests, console logs.
Частые ошибки
1. Не используют auto-waiting
// ПЛОХО: ручные ожидания
await page.waitForTimeout(3000);
// ХОРОШО: Playwright ждёт автоматически
await expect(page.getByText('Загружено')).toBeVisible;
2. Хрупкие селекторы
// ПЛОХО:
page.locator('div > div:nth-child(3) > button');
// ХОРОШО:
page.getByRole('button', { name: 'Отправить' });
3. Не используют page fixtures
// ПЛОХО: повторяющийся логин
test('тест 1', async ({ page }) => {
await page.goto('/login');
await page.fill('#email', 'a@t.com');
// ...
});
// ХОРОШО: fixture для аутентификации
// auth.setup.ts
test('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('alice@test.com');
await page.getByLabel('Пароль').fill('secret123');
await page.getByRole('button', { name: 'Войти' }).click();
await page.context.storageState({ path: '.auth/user.json()' });
});
// Другие тесты используют сохранённое состояние
test.use({ storageState: '.auth/user.json()' });
Практика
- Установи Playwright и сгенерируй первый тест через codegen
- Напиши тест формы поиска: ввод запроса, нажатие Enter, проверка результатов
- Перехвати API-запрос через
page.routeи верни мок-данные - Сделай визуальное сравнение (screenshot) для компонента
- Запусти тесты в трёх браузерах одновременно