🎭 Playwright
Полный справочник: установка, тесты и раннер, локаторы, автоожидания и expect, фикстуры, сеть и моки, API-тесты, авторизация, трассировка, visual и a11y, параллельность, CI и отладка.
Шпаргалки · Тестирование · #testing #playwright #e2e #ui #automation
Что это
Playwright (Microsoft) автоматизирует Chromium, Firefox и WebKit одним API. Языки: TypeScript / JavaScript, Python, Java, .NET. Сильные стороны: автоожидания, изолированные контексты, перехват сети, трассировка, параллельный запуск, генератор кода, тестирование веб и мобильной эмуляции.
Установка и запуск
npm init playwright@latest # проект, конфигурация, примеры, браузеры
npx playwright install [chromium] [--with-deps] # браузеры (и системные зависимости для Linux/CI)
npx playwright test npx playwright test login.spec.ts npx playwright test -g "вход"
npx playwright test --project=chromium npx playwright test --headed npx playwright test --ui
npx playwright test --debug npx playwright test --workers=4 npx playwright test --repeat-each=5
npx playwright test --last-failed npx playwright test --shard=1/3
npx playwright show-report npx playwright show-trace trace.zip
npx playwright codegen https://example.com # запись действий в код
Python: pip install pytest-playwright && playwright install, запуск pytest --headed --browser firefox.
Первый тест (TypeScript)
import { test, expect } from '@playwright/test';
test('вход в профиль', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('anna@x.ru');
await page.getByLabel('Пароль').fill('Qwerty123!');
await page.getByRole('button', { name: 'Войти' }).click();
await expect(page).toHaveURL(/profile/);
await expect(page.getByRole('heading', { name: 'Анна' })).toBeVisible();
});
Структура: test.describe('группа', () => {}), test.beforeEach, test.afterEach, test.beforeAll, test.skip, test.fixme, test.only, test.step('шаг', async () => {}), теги test('...', { tag: '@smoke' }, ...), test.slow().
Локаторы (по приоритету)
| Локатор | Пример |
|---|---|
| Роль (лучший) | page.getByRole('button', { name: 'Оплатить' }), getByRole('link', { name: /заказ/i }), getByRole('checkbox', { checked: true }) |
| Подпись поля | page.getByLabel('Email') |
| Placeholder | page.getByPlaceholder('Поиск') |
| Текст | page.getByText('Корзина', { exact: true }) |
| Alt, Title | page.getByAltText('Логотип'), getByTitle('Закрыть') |
| Тестовый id | page.getByTestId('submit') (атрибут data-testid, настраивается testIdAttribute) |
| CSS / XPath | page.locator('.card >> text=Купить'), locator('css=ul > li'), locator('xpath=//h1') |
const row = page.getByRole('row').filter({ hasText: 'Аня' }); // фильтр
await row.getByRole('button', { name: 'Удалить' }).click();
page.locator('li').nth(2); page.locator('li').first(); page.locator('li').last();
page.locator('article').filter({ has: page.getByRole('heading', { name: 'Новости' }) });
page.locator('div').and(page.locator('.active')); page.locator('a').or(page.locator('button'));
await page.locator('li').count(); await page.locator('li').allTextContents();
page.frameLocator('#payment').getByLabel('Номер карты'); // iframe
Локаторы ленивые и строгие: ищутся в момент действия; если найдено больше одного элемента, будет ошибка (strict mode). Shadow DOM проходится автоматически.
Действия
await page.goto('/path'); await page.goBack(); await page.reload();
await el.click(); await el.dblclick(); await el.click({ button: 'right', modifiers: ['Shift'] }); await el.hover();
await el.fill('текст'); await el.pressSequentially('медленный ввод', { delay: 50 }); await el.press('Enter');
await el.clear(); await el.check(); await el.uncheck(); await el.selectOption('msk'); await el.setInputFiles('photo.jpg');
await el.focus(); await el.blur(); await el.scrollIntoViewIfNeeded(); await el.dragTo(target);
await page.keyboard.press('Control+A'); await page.mouse.click(100, 200);
await el.screenshot({ path: 'el.png' }); await page.screenshot({ path: 'full.png', fullPage: true });
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.pdf({ path: 'a.pdf' }); // только Chromium
Перед каждым действием Playwright ждёт: элемент прикреплён, виден, стабилен (не анимируется), доступен и не перекрыт. Отдельные sleep не нужны.
Проверки expect (с авто-повтором)
await expect(el).toBeVisible(); await expect(el).toBeHidden(); await expect(el).toBeEnabled(); await expect(el).toBeDisabled();
await expect(el).toHaveText('Привет'); await expect(el).toContainText('при'); await expect(el).toHaveValue('a@x.ru');
await expect(el).toHaveAttribute('href', /example/); await expect(el).toHaveClass(/active/); await expect(el).toHaveCSS('color', 'rgb(0, 0, 0)');
await expect(el).toBeChecked(); await expect(el).toBeFocused(); await expect(list).toHaveCount(3); await expect(list).toHaveText(['a', 'b']);
await expect(page).toHaveURL(/dashboard/); await expect(page).toHaveTitle('Главная');
await expect(el).not.toBeVisible();
await expect.poll(async () => (await api.get('/job')).json()).toMatchObject({ status: 'done' }); // опрос произвольного значения
await expect(async () => { /* блок до успеха */ }).toPass({ timeout: 10_000 });
expect.soft(el).toHaveText('x'); // мягкая проверка: тест продолжится
await expect(page).toHaveScreenshot('home.png', { maxDiffPixelRatio: 0.01 }); // визуальное сравнение
Обычные expect(value).toBe(...) синхронны и не ждут.
Сеть: перехват и моки
await page.route('**/api/users', (route) => route.fulfill({ json: [{ id: 1, name: 'Тест' }] })); // подмена ответа
await page.route('**/*.{png,jpg}', (r) => r.abort()); // блокировка картинок
await page.route('**/api/order', async (route) => {
const res = await route.fetch(); // дернуть реальный
const json = await res.json(); json.total = 0;
await route.fulfill({ response: res, json });
});
const [resp] = await Promise.all([
page.waitForResponse((r) => r.url().includes('/api/order') && r.status() === 201),
page.getByRole('button', { name: 'Заказать' }).click(),
]);
page.on('request', (r) => console.log(r.method(), r.url())); page.on('console', (m) => console.log(m.text())); page.on('pageerror', (e) => console.error(e));
await page.routeFromHAR('mock.har', { update: false }); // записанные ответы
API-тесты
test('создание пользователя', async ({ request }) => {
const res = await request.post('/api/users', { data: { email: 'a@x.ru' }, headers: { Authorization: `Bearer ${token}` } });
expect(res.status()).toBe(201);
const body = await res.json();
expect(body).toMatchObject({ email: 'a@x.ru' });
});
Данные для UI-тестов создавайте через API: быстрее и стабильнее, чем через интерфейс.
Авторизация один раз
// auth.setup.ts
setup('вход', async ({ page }) => {
await page.goto('/login'); await page.getByLabel('Email').fill(process.env.USER!); await page.getByLabel('Пароль').fill(process.env.PASS!);
await page.getByRole('button', { name: 'Войти' }).click(); await page.waitForURL('/profile');
await page.context().storageState({ path: '.auth/user.json' });
});
// playwright.config.ts: projects: [{ name: 'setup', testMatch: /auth\.setup\.ts/ }, { name: 'chromium', use: { storageState: '.auth/user.json' }, dependencies: ['setup'] }]
Файл состояния (.auth/) добавьте в .gitignore.
Фикстуры и Page Object
import { test as base } from '@playwright/test';
class CartPage { constructor(private page: Page) {} async add(name: string) { await this.page.getByRole('button', { name: `В корзину ${name}` }).click(); } }
export const test = base.extend<{ cart: CartPage }>({
cart: async ({ page }, use) => { await page.goto('/shop'); await use(new CartPage(page)); },
});
test('добавление', async ({ cart, page }) => { await cart.add('Чай'); await expect(page.getByTestId('count')).toHaveText('1'); });
Встроенные фикстуры: page, context, browser, browserName, request, baseURL. Свои фикстуры гарантируют настройку и очистку и делятся между тестами.
Конфигурация (playwright.config.ts)
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000,
expect: { timeout: 5_000 },
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['html'], ['list'], ['junit', { outputFile: 'results.xml' }]],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
locale: 'ru-RU', timezoneId: 'Europe/Moscow',
actionTimeout: 10_000,
},
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', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI },
});
Контексты и страницы
const context = await browser.newContext({ viewport: { width: 1280, height: 720 }, geolocation: { latitude: 55.75, longitude: 37.62 }, permissions: ['geolocation'], colorScheme: 'dark' });
const page = await context.newPage();
const [popup] = await Promise.all([context.waitForEvent('page'), page.getByText('Открыть').click()]); // новая вкладка
page.on('dialog', (d) => d.accept()); // alert, confirm
const [download] = await Promise.all([page.waitForEvent('download'), page.getByText('Скачать').click()]); await download.saveAs('f.pdf');
Каждый тест получает чистый контекст (как новый профиль), поэтому тесты изолированы без очистки cookies вручную.
Отладка и трассировка
npx playwright test --debugилиawait page.pause()открывает Inspector (шаги, локаторы).- Trace Viewer (
trace: 'on'): временная шкала действий, DOM-снимки до и после, сеть, консоль, исходный код. Для упавших CI-тестов откройтеtrace.zipвnpx playwright show-traceили на trace.playwright.dev. - VS Code: расширение Playwright (запуск и отладка по клику, выбор локатора).
- Переменная
DEBUG=pw:apiпечатает вызовы.
Мобильная эмуляция, доступность, визуальные тесты
test.use({ ...devices['Pixel 7'], locale: 'ru-RU' });
import AxeBuilder from '@axe-core/playwright';
const results = await new AxeBuilder({ page }).analyze(); expect(results.violations).toEqual([]);
await expect(page).toHaveScreenshot(); // эталоны хранятся в *-snapshots; обновление: npx playwright test --update-snapshots
Скриншоты зависят от ОС и шрифтов: запускайте в одном окружении (Docker-образ mcr.microsoft.com/playwright).
Python-вариант
import re
from playwright.sync_api import Page, expect
def test_login(page: Page):
page.goto("https://example.com/login")
page.get_by_label("Email").fill("anna@x.ru")
page.get_by_role("button", name="Войти").click()
expect(page).to_have_url(re.compile(r"profile"))
pytest --browser chromium --headed --slowmo 300 --tracing on. Асинхронный API: playwright.async_api.
CI (GitHub Actions)
- 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: ${{ !cancelled() }}
with: { name: playwright-report, path: playwright-report/ }
Шардирование: --shard=${{ matrix.shard }}/4 и слияние отчётов merge-reports. Docker: образ mcr.microsoft.com/playwright:v1.xx-jammy.
Практика и частые проблемы
- Предпочитайте локаторы по роли и тексту, как видит пользователь, вместо CSS-цепочек.
- Не используйте
waitForTimeout: ждите конкретное условие (expect,waitForResponse,waitForURL). - Тест не зависит от порядка и данных других тестов; данные создаёт и удаляет сам.
- Нестабильность:
trace: 'on-first-retry'покажет причину; проверьте гонки с анимацией и сетью. - Strict mode violation: уточните локатор (
getByRoleсname,filter,first()как крайнее средство). - Работа с
iframe:frameLocator. Шрифты и локаль: задавайтеlocaleиtimezoneId. - Для тяжёлых сквозных сценариев: небольшое число критичных путей, остальное на API и компонентные тесты (Playwright Component Testing).
Playwright или Cypress или Selenium
Playwright: мультибраузерность (включая WebKit), параллельность, несколько вкладок и доменов, быстрый и стабильный. Cypress: приятный отладчик, но один браузерный контекст. Selenium: максимальная совместимость с корпоративной инфраструктурой. См. шпаргалку «Selenium».