🎭 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».