Создание реального теста: setup, codegen, debug, trace
В этом уроке, полный путь от чистой папки до работающего теста с использованием современного API Playwright. Реалистичный e-commerce-сценарий, codegen, инструменты дебага, trace viewer.
Шаг 1. Инициализация проекта
# Можно с нуля
npm init playwright@latest
# Или вручную в существующем
npm install -D @playwright/test
npx playwright install # скачать Chromium / Firefox / WebKit
npx playwright install --with-deps # на Linux: + системные зависимости
После init структура:
my-project/
├── playwright.config.ts
├── tests/
│ └── example.spec.ts
├── tests-examples/
│ └── demo-todo-app.spec.ts
└── package.json
package.json обычно дополняют скриптами:
{
"scripts": {
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:headed": "playwright test --headed",
"test:debug": "PWDEBUG=1 playwright test",
"report": "playwright show-report",
"codegen": "playwright codegen"
}
}
Шаг 2. Базовый playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['html', { open: 'never' }], ['list']],
use: {
baseURL: process.env.BASE_URL ?? 'https://demo.playwright.dev',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: devices['Desktop Chrome'] },
{ name: 'firefox', use: devices['Desktop Firefox'] },
{ name: 'webkit', use: devices['Desktop Safari'] },
],
});
Шаг 3. Codegen, записать первый тест кликами
npx playwright codegen https://demo.playwright.dev/todomvc
Откроется браузер + Playwright Inspector. Все ваши клики, фоллы, Enter записываются в код. Inspector показывает варианты selector'ов в порядке предпочтения (role > label > test-id > css).
Полученный код, копируете в tests/todo.spec.ts и доводите до ума:
import { test, expect } from '@playwright/test';
test('add and complete todo', async ({ page }) => {
await page.goto('/todomvc');
await page.getByPlaceholder('What needs to be done?').fill('Buy milk');
await page.getByPlaceholder('What needs to be done?').press('Enter');
await page.getByPlaceholder('What needs to be done?').fill('Walk dog');
await page.getByPlaceholder('What needs to be done?').press('Enter');
const items = page.getByTestId('todo-item');
await expect(items).toHaveCount(2);
await items.first().getByRole('checkbox').check();
await expect(items.first()).toHaveClass(/completed/);
});
Шаг 4. Реалистичный e-commerce тест
// tests/checkout.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Checkout flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('add product → cart → checkout → confirmation', async ({ page }) => {
// 1. Поиск товара
await page.getByRole('searchbox').fill('headphones');
await page.getByRole('searchbox').press('Enter');
await expect(page).toHaveURL(/\/search/);
// 2. Открыть карточку
const firstResult = page.getByRole('article').first();
await expect(firstResult).toBeVisible();
const productName = await firstResult.getByRole('heading').textContent();
await firstResult.getByRole('link', { name: /view/i }).click();
// 3. Добавить в корзину
await page.getByRole('button', { name: 'Add to cart' }).click();
const cartBadge = page.getByTestId('cart-count');
await expect(cartBadge).toHaveText('1');
// 4. Перейти в корзину
await page.getByRole('link', { name: /cart/i }).click();
await expect(page.getByRole('heading', { name: 'Your cart' })).toBeVisible();
await expect(page.getByText(productName!)).toBeVisible();
// 5. Чекаут
await page.getByRole('button', { name: 'Checkout' }).click();
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Address').fill('123 Test St');
await page.getByLabel('City').fill('Springfield');
await page.getByLabel('ZIP').fill('12345');
// 6. Оплата (мокаем backend)
await page.route('**/api/payment', route =>
route.fulfill({ status: 200, json: { orderId: 'TEST-42' } })
);
await page.getByRole('button', { name: 'Pay now' }).click();
// 7. Подтверждение
await expect(page.getByRole('heading')).toContainText('Order confirmed');
await expect(page.getByText(/TEST-42/)).toBeVisible();
});
test('cart persists after refresh', async ({ page }) => {
await page.getByRole('article').first().getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
await page.reload();
await expect(page.getByTestId('cart-count')).toHaveText('1');
});
test('empty cart shows message', async ({ page }) => {
await page.getByRole('link', { name: /cart/i }).click();
await expect(page.getByText('Your cart is empty')).toBeVisible();
});
});
Шаг 5. UI mode, главный инструмент разработки тестов
npx playwright test --ui
В UI mode:
- Watch mode: тесты ре-запускаются при изменении файлов.
- Time-travel debugger: для каждого шага видно DOM snapshot.
- Live picker locator'ов: наводите мышью на UI → копируется готовый locator.
- Network panel.
- Re-run одного теста, debug-step, скрин-tail.
UI mode сильно сокращает цикл «написал → запустил → понял где сломалось → правлю».
Шаг 6. Дебаг падающего теста
A. Через UI mode
Откройте, найдите красный, кликните последний шаг, увидите DOM-снимок ровно перед падением.
B. Через PWDEBUG
PWDEBUG=1 npx playwright test checkout.spec.ts -g "add product"
Запускает тест с открытым Playwright Inspector + браузер останавливается перед каждым action. Видно selector, который сейчас выполнится; можно поправить и продолжить.
C. Через --debug
npx playwright test --debug
D. page.pause() в коде
test('flaky test', async ({ page }) => {
await page.goto('/');
await page.click('text=Open');
await page.pause(); // ← тест остановится; откроется Inspector
await page.click('text=Save');
});
E. Trace viewer (для CI-падений)
# Скачали trace.zip из CI artifacts
npx playwright show-trace trace.zip
В trace viewer видно весь timeline: каждый step + DOM-snapshot до/после, network, console, source code теста с подсветкой текущей строки. Это самый эффективный способ дебага CI-only падений.
Шаг 7. Locator best practices в реальном коде
// ❌ Хрупко, ломается на любом CSS-рефакторинге
await page.locator('.MuiButton-root.MuiButton-containedPrimary').click();
// ❌ Хрупко, ломается, если поменяют структуру
await page.locator('div > section:nth-child(3) > button').click();
// ✅ Стабильно
await page.getByRole('button', { name: 'Submit' }).click();
// ✅ Если нет accessible name
// 1. Договориться с фронтом добавить `data-testid`
await page.getByTestId('submit-button').click();
// ✅ Уникальное сочетание role + текст рядом
await page.getByRole('listitem').filter({ hasText: 'iPhone 15' })
.getByRole('button', { name: 'Add' })
.click();
// ✅ Для формы
await page.getByLabel('Email').fill('a@b.com');
await page.getByPlaceholder('Search').press('Enter');
// ✅ Утверждения структуры через Aria snapshot (Playwright 1.50+)
await expect(page.getByRole('main')).toMatchAriaSnapshot(`
- heading "Cart" [level=1]
- list:
- listitem: "iPhone 15"
- listitem: "Headphones"
- button "Checkout"
`);
Шаг 8. Web-first assertions vs JavaScript-if
Антипаттерн:
// Хрупко: ассерт может сработать раньше, чем UI обновится
const text = await page.locator('.message').textContent();
expect(text).toBe('Saved');
Правильно:
await expect(page.locator('.message')).toHaveText('Saved');
// retry до 5s, пока текст не совпадёт
Помнить: expect(locator).toX(), web-first с retry. expect(value).toBe(...), обычный sync ассерт.
Шаг 9. Группировка через describe, beforeEach, afterEach
test.describe('Auth', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test.afterEach(async ({ page }, info) => {
if (info.status !== info.expectedStatus) {
await page.screenshot({ path: `failures/${info.title}.png` });
}
});
test('valid login', /*...*/);
test('invalid password', /*...*/);
});
test.describe.serial('Order pipeline', () => {
// тесты выполняются последовательно, делят state
});
Шаг 10. Snapshots / Visual regression
test('homepage looks correct', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('home.png', { maxDiffPixels: 100 });
});
Первый запуск создаст baseline в __screenshots__/, последующие сравнят. Для богатого workflow (preview, approve in PR, cross-browser), Argos CI, Lost Pixel, Percy, Chromatic.
Шаг 11. Fixtures для проекта
tests/fixtures.ts (расширение test):
import { test as base, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { CartPage } from './pages/CartPage';
type Fixtures = {
loginPage: LoginPage;
cartPage: CartPage;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => use(new LoginPage(page)),
cartPage: async ({ page }, use) => use(new CartPage(page)),
});
export { expect };
// tests/checkout.spec.ts
import { test, expect } from './fixtures';
test('checkout', async ({ loginPage, cartPage, page }) => {
await loginPage.loginAs('alice');
await cartPage.goto();
await cartPage.checkout();
await expect(page).toHaveURL(/order-confirmed/);
});
Шаг 12. Запуск
npx playwright test # все тесты, headless, всех браузеров
npx playwright test --headed --project=chromium
npx playwright test checkout.spec.ts # один файл
npx playwright test -g "empty cart" # фильтр по имени
npx playwright test --workers=1 # последовательно (для дебага)
npx playwright test --update-snapshots # обновить baseline для visual
npx playwright show-report # открыть HTML-отчёт
Главное
Цикл написания теста на Playwright в 2026:
- Codegen, наклацать каркас теста.
- Заменить хрупкие locator'ы на
getByRole/Label/TestId. - Доковать assertions через web-first
expect(...). - Закрыть бизнес-логику через POM, повторяющиеся setup'ы, через fixtures.
- UI mode для итераций.
- Trace viewer для дебага CI-падений.
В финальном уроке, production: CI/CD, отчёты, parallel/sharding, Docker, BrowserStack, мониторинг flaky-тестов.