Построение промышленного конвейера сквозного тестирования (End-to-End) на базе Playwright: архитектура Page Object Model (POM), однократная авторизация с сохранением контекста в Storage State, кастомные фикстуры и параллельный шардинг в GitHub Actions.
Неорганизованные E2E-тесты быстро становятся узким горлышком разработки:
sleep(3000) вместо умного механизма проверок готовности элементов (Actionability) приводит к случайным падениям тестов при сетевых колебаниях.Связка Playwright + POM + Storage State + CI Sharding обеспечивает надежный, самовосстанавливающийся тестовый конвейер: авто-ожидание элементов (Auto-waiting), мгновенный запуск тестов под уже авторизованным пользователем и параллельный запуск тестов на матрице виртуальных машин за считанные минуты.
Архитектура промышленного E2E-набора разделяет запуск тестов на этап глобальной аутентификации и параллельные рабочие потоки (Workers):
+-------------------------------------------------------------------------------+
| PLAYWRIGHT CI/CD EXECUTION PIPELINE |
| |
| 1. Project 'setup': Global Auth (Выполняется ОДНОКРАТНО перед тестами) |
| - Логин через форму -> Сохранение cookies/JWT в auth/user-session.json |
+---------------------------------------+---------------------------------------+
| (auth/user-session.json ready)
+---------------------------------------v---------------------------------------+
| 2. Тестовые проекты (Desktop Chrome, Mobile Safari, Firefox) |
| Каждый тест стартует с: storageState: 'auth/user-session.json' |
| |
| +-----------------------------------+-----------------------------------+ |
| | Worker #1 (Shard 1/3) | Worker #2 (Shard 2/3) | |
| | CheckoutPage (POM) | UserProfilePage (POM) | |
| | - Mute-free auto-waiting | - Auto-retry network requests | |
| | - Network Route Mocking | - Accessibility audits (axe) | |
| +-----------------------------------+-----------------------------------+ |
| | |
| v |
| 3. Playwright Trace Viewer & Unified HTML Report |
| Запись скриншотов, сетевых дампов и DOM-снапшотов только при падении (trace)|
+-------------------------------------------------------------------------------+Конфигурация проекта 'setup' для однократного логина и привязка тестовых проектов через dependencies: ['setup'].
Создание классов LoginPage, DashboardPage с инкапсуляцией локаторов page.getByRole() и бизнес-методов.
Внедрение зависимостей страниц в тесты через фикстуры с автоматической инициализацией и очисткой.
Изоляция E2E-тестов от нестабильных сторонних платежных систем и внешних SMS-шлюзов.
Параллельное распределение тестов по виртуальным машинам через --shard=${{ matrix.shard }}/4 со сборкой единого отчета.
Почему в Playwright строго запрещено использовать задержки page.waitForTimeout(3000) перед нажатием на кнопку?
Ниже приведена эталонная архитектура enterprise-проекта на Playwright: проект глобальной авторизации, реализация Page Object Model и сквозной тест заказа.
import { defineConfig, devices } from '@playwright/test';
import path from 'path';
export const STORAGE_STATE_PATH = path.join(__dirname, 'playwright/.auth/user.json');
export default defineConfig({
testDir: './e2e',
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 || 'http://localhost:3000',
trace: 'on-first-retry', // Запись трейса только при падении для экономии диска
screenshot: 'only-on-failure'
},
projects: [
// Фаза 1: Однократная аутентификация
{
name: 'setup',
testMatch: /.*\.setup\.ts/
},
// Фаза 2: Основные браузерные прогоны с пред-загруженной сессией
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: STORAGE_STATE_PATH
},
dependencies: ['setup'] // Ждем успешного завершения логина!
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
storageState: STORAGE_STATE_PATH
},
dependencies: ['setup']
}
]
});import { test as setup, expect } from '@playwright/test';
import { STORAGE_STATE_PATH } from '../playwright.config';
setup('аутентификация системного тестового пользователя', async ({ page }) => {
await page.goto('/login');
// Заполняем форму входа
await page.getByRole('textbox', { name: /электронная почта/i }).fill('[email protected]');
await page.getByRole('textbox', { name: /пароль/i }).fill('SecurePass2026!');
await page.getByRole('button', { name: /войти в систему/i }).click();
// Ожидаем успешного перехода в дашборд (подтверждение валидности куки)
await expect(page.getByRole('heading', { name: /панель управления/i })).toBeVisible();
// Сохраняем сессию (cookies + localStorage) в файл
await page.context().storageState({ path: STORAGE_STATE_PATH });
console.log('[PLAYWRIGHT AUTH] Сессия успешно сохранена в', STORAGE_STATE_PATH);
});import { type Page, type Locator, expect } from '@playwright/test';
export class CheckoutPage {
readonly page: Page;
readonly heading: Locator;
readonly addressInput: Locator;
readonly confirmCheckbox: Locator;
readonly submitButton: Locator;
readonly successBanner: Locator;
constructor(page: Page) {
this.page = page;
// Локаторы инкапсулируются строго по доступным ролям WAI-ARIA
this.heading = page.getByRole('heading', { name: /оформление заказа/i });
this.addressInput = page.getByRole('textbox', { name: /адрес доставки/i });
this.confirmCheckbox = page.getByRole('checkbox', { name: /согласен с условиями/i });
this.submitButton = page.getByRole('button', { name: /оплатить заказ/i });
this.successBanner = page.getByRole('alert');
}
async goto() {
await this.page.goto('/checkout');
await expect(this.heading).toBeVisible();
}
async completeOrder(address: string) {
await this.addressInput.fill(address);
await this.confirmCheckbox.check();
await this.submitButton.click();
}
async expectOrderSuccessful() {
await expect(this.successBanner).toContainText(/заказ успешно создан/i);
}
}import { test, expect } from '@playwright/test';
import { CheckoutPage } from './pages/CheckoutPage';
test.describe('Сквозной сценарий оформления заказа', () => {
test('должен успешно оформлять заказ под авторизованным пользователем', async ({ page }) => {
// 1. Симулируем ответ внешнего платежного шлюза для надежности
await page.route('**/api/v1/payments/process', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ transactionId: 'txn-mock-2026', status: 'SUCCESS' })
});
});
const checkoutPage = new CheckoutPage(page);
// 2. Пользователь уже авторизован благодаря storageState!
await checkoutPage.goto();
// 3. Выполнение бизнес-действия
await checkoutPage.completeOrder('ул. Инженерная, д. 42, оф. 101');
// 4. Проверка результата
await checkoutPage.expectOrderSuccessful();
});
});Локаторы вида page.locator('.btn-primary_xyz-123') или page.locator('div > div:nth-child(3)') ломаются при первой же перекомпиляции Tailwind CSS или смене разметки. Используйте page.getByRole(), page.getByLabel() и page.getByText(), ориентированные на доступность для человека.
Запуск 500 E2E-тестов на одной виртуальной машине GitHub Actions приводит к таймаутам сборок и очередям. Используйте встроенный параметр --shard=1/4, запускающий 4 параллельные джобы, и объединяйте сгенерированные blob-отчеты с помощью утилиты npx playwright merge-reports.
Если один тест меняет пароль пользователя или удаляет сущность, все последующие тесты упадут из-за поврежденного состояния. Генерируйте уникальные имена сущностей для каждого запуска (например, с суффиксом Date.now()) или изолируйте тенанты.
Что позволяет делать Playwright Trace Viewer при анализе упавшего в CI/CD автотеста?