Исторически автоматизация веб-браузеров строилась вокруг стандарта W3C WebDriver (Selenium). Эта архитектура требовала отдельного HTTP-сервера драйвера (ChromeDriver, geckodriver) для каждого браузера. Каждый клик, ввод текста или чтение DOM транслировались в цепочку синхронных HTTP-запросов через JSON Wire Protocol. Подобный подход создавал три фундаментальные проблемы:
StaleElementReferenceException.+-----------------------------------------------------------------------+
| Selenium WebDriver Архитектура |
| |
| +--------------+ HTTP / REST +--------------+ Custom Protocol |
| | Node.js Test | ---------------> | ChromeDriver | ----------------> | Browser |
| +--------------+ (JSON Wire) +--------------+ | |
+-----------------------------------------------------------------------+
+-----------------------------------------------------------------------+
| Playwright 2026 Архитектура |
| |
| +--------------+ Двунаправленный WebSocket |
| | Node.js / TS | <=================================================> | Browser |
| | Test Runner | (CDP / WebDriver BiDi / LibWebSockets) | Process |
| +--------------+ +---------+
+-----------------------------------------------------------------------+Playwright устраняет эти ограничения за счет прямого мультиплексированного подключения к движкам браузеров (Chromium, Firefox, WebKit) через постоянный двунаправленный WebSocket-канал. В случае Chromium используется протокол Chrome DevTools Protocol (CDP) и развивающийся кроссбраузерный стандарт WebDriver BiDi.
Такая связка обеспечивает:
networkidle), создание контекстов исполнения JS;Архитектура Playwright строится на строгой иерархии четырех сущностей: Browser, BrowserContext, Page и Locator.
+-----------------------------------------------------------------------------+
| Browser Instance |
| (Единый процесс ОС: chromium.exe / firefox.exe / webkit.exe) |
| |
| +-----------------------------------+ +---------------------------------+ |
| | BrowserContext 1 | | BrowserContext 2 | |
| | - Изолированные Cookies | | - Независимый localStorage | |
| | - Изолированный IndexedDB | | - Независимый Cache Storage | |
| | - Свои права (Permissions, Geo) | | - Собственный профиль сети | |
| | | | | |
| | +-----------+ +-----------+ | | +-----------+ | |
| | | Page 1 | | Page 2 | | | | Page 3 | | |
| | | (Tab / | | (Popup) | | | | (Tab) | | |
| | | Frame) | | | | | | | | |
| | +-----------+ +-----------+ | | +-----------+ | |
| +-----------------------------------+ +---------------------------------+ |
+-----------------------------------------------------------------------------+Главная причина нестабильности (flakiness) тестов в веб-приложениях — попытка взаимодействия с элементами, находящимися в промежуточных состояниях (CSS-транзишены, ожидание ответа API, блокировка оверлеем).
В Playwright каждое действие над элементом (например, locator.click() или locator.fill()) запускает конвейер Actionability Checks:
+-------------------------------------------+
| Вызов locator.click() |
+-------------------------------------------+
│
▼
+-------------------------------------------+
| 1. Attached? (Узел присутствует в DOM) |
+-------------------------------------------+
│ Да
▼
+-------------------------------------------+
| 2. Visible? (Не hidden, размер > 0x0) |
+-------------------------------------------+
│ Да
▼
+-------------------------------------------+
| 3. Stable? (CSS-анимации завершены, |
| координаты не меняются два кадра) |
+-------------------------------------------+
│ Да
▼
+-------------------------------------------+
| 4. Enabled? (Нет атрибута disabled) |
+-------------------------------------------+
│ Да
▼
+-------------------------------------------+
| 5. Editable? (Только для fill / clear) |
+-------------------------------------------+
│ Да
▼
+-------------------------------------------+
| 6. Receives Events? (Хит-тест: целевая |
| точка не перекрыта модальным оверлеем)|
+-------------------------------------------+
│ Да
▼
+-------------------------------------------+
| Физическая отправка события клика (CDP) |
+-------------------------------------------+Если хотя бы одна из проверок не проходит, Playwright не выбрасывает ошибку мгновенно, а активирует цикл Auto-waiting: проверка повторяется непрерывно до истечения заданного таймаута (по умолчанию 30 секунд).
Playwright резолвит селектор в DOM. Если под условие попадает более одного элемента, выбрасывается Strict Mode Violation.
Убеждается, что элемент подключен к активному документу (isAttached) и отображается (не имеет display: none, visibility: hidden, нулевых размеров).
Выполняется сравнение ограничивающего прямоугольника (BoundingBox) в течение минимум двух последовательных кадров анимации.
Элемент автоматически прокручивается в область видимости. Метод document.elementFromPoint проверяет, что координаты не перекрыты другими слоями.
Через протокол CDP или BiDi отправляются физические события мыши: mousemove, mousedown, mouseup, click.
Почему в архитектуре Playwright изоляция тестов на уровне BrowserContext предпочтительнее создания нового экземпляра Browser для каждого теста?
Playwright продвигает философию тестирования через призму восприятия конечного пользователя и спецификации W3C Accessible Rich Internet Applications (ARIA).
import { test, expect } from '@playwright/test';
test('демонстрация семантических локаторов высокого приоритета', async ({ page }) => {
await page.goto('/checkout');
// 1. Поиск по доступной роли (W3C ARIA role) и имени
const submitButton = page.getByRole('button', { name: /оформить заказ/i });
// 2. Поиск поля формы по связанному элементу <label>
const emailInput = page.getByLabel('Корпоративный E-mail');
// 3. Поиск по видимому тексту
const banner = page.getByText('Скидка 20% применена');
// 4. Поиск по атрибуту доступности placeholder
const promoInput = page.getByPlaceholder('Введите промокод');
// 5. Запасной вариант для динамических несемантических узлов (data-testid)
const orderSummary = page.getByTestId('order-summary-container');
// Цепочки и фильтрация
const premiumCard = page.getByRole('listitem').filter({
has: page.getByRole('heading', { name: 'Тариф Enterprise' }),
});
await emailInput.fill('[email protected]');
await submitButton.click();
});Все методы действий принимают конфигурационный объект для тонкой настройки поведения Auto-waiting:
interface ActionOptions {
/** Максимальное время ожидания прохождения проверок actionability в миллисекундах */
timeout?: number;
/** Принудительное выполнение действия в обход actionability checks (не рекомендуется) */
force?: boolean;
/** Пропуск ожидания навигации, инициированной данным действием */
noWaitAfter?: boolean;
/** Режим "холостого хода": выполнить все проверки actionability, но не нажимать кнопку */
trial?: boolean;
/** Точные координаты клика относительно левого верхнего угла bounding-бокса элемента */
position?: { x: number; y: number };
}Trace Viewer — это полноценный инструмент расследования сбоев, фиксирующий DOM-снапшоты на каждый шаг, сетевые запросы, консольные ошибки и кадры рендеринга. Запуск трейсинга на каждый успешный тест замедляет пайплайн, поэтому в продакшн-конфигурациях применяется политика 'on-first-retry'.
import { defineConfig, devices } from '@playwright/test';
import path from 'path';
export default defineConfig({
testDir: './e2e',
/* Запуск тестов в параллельных файлах */
fullyParallel: true,
/* Запрет test.only в CI */
forbidOnly: !!process.env.CI,
/* Количество повторов упавших тестов */
retries: process.env.CI ? 2 : 0,
/* Количество параллельных воркеров */
workers: process.env.CI ? '100%' : undefined,
/* Репортеры: консольный + HTML-отчет с артефактами */
reporter: [
['list'],
['html', { outputFolder: 'playwright-report', open: 'never' }],
],
use: {
/* Базовый URL для сокращения page.goto('/') */
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
/* Запись трейса только при повторе упавшего теста */
trace: 'on-first-retry',
/* Скриншот только при ошибке */
screenshot: 'only-on-failure',
/* Запись видео только при первой попытке восстановления */
video: 'retain-on-failure',
/* Дефолтный таймаут для каждого действия (click, fill) */
actionTimeout: 10_000,
/* Дефолтный таймаут для навигаций */
navigationTimeout: 15_000,
},
/* Настройка пула поддерживаемых браузерных движков */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chromium',
use: { ...devices['Pixel 7'] },
},
],
});sleep()#Никогда не используйте фиксированные задержки page.waitForTimeout(3000). Они либо замедляют выполнение пайплайна (если тест мог пройти быстрее), либо приводят к случайным сбоям (если CI-агент был перегружен).
import { test, expect } from '@playwright/test';
test('корректная обработка асинхронного обновления таблицы через Auto-waiting', async ({ page }) => {
await page.goto('/analytics/reports');
const filterDropdown = page.getByRole('combobox', { name: 'Период' });
const tableRows = page.getByRole('table').getByRole('row');
const skeletonLoader = page.getByTestId('table-skeleton-loader');
// Выбираем фильтр
await filterDropdown.selectOption('last-30-days');
// Шаг 1: Дожидаемся появления лоадера (если он быстрый, assertion может пройти мгновенно)
// Шаг 2: Убеждаемся, что индикатор загрузки полностью исчез из DOM
await expect(skeletonLoader).not.toBeAttached({ timeout: 10_000 });
// Шаг 3: Веб-ассерты expect(locator).to* имеют встроенный механизм auto-waiting
// Они периодически опрашивают состояние до достижения условия или таймаута
await expect(tableRows).toHaveCount(31); // 1 header + 30 data rows
// Шаг 4: Проверяем конкретное значение в строке
const firstDataRow = tableRows.nth(1);
await expect(firstDataRow.getByRole('cell').first()).toHaveText(/2026-08/);
});В одном E2E сценарии требуется проверить взаимодействие двух независимых клиентов: оператора поддержки и клиента в чате WebSockets. Создавать второй экземпляр Browser не требуется — достаточно развернуть два изолированных BrowserContext.
import { test, expect } from '@playwright/test';
test('синхронизация сообщений между двумя изолированными пользователями в реальном времени', async ({ browser }) => {
// Контекст первого пользователя (Клиент)
const clientContext = await browser.newContext({
locale: 'ru-RU',
permissions: ['notifications'],
});
const clientPage = await clientContext.newPage();
// Контекст второго пользователя (Оператор)
const agentContext = await browser.newContext({
locale: 'en-US',
});
const agentPage = await agentContext.newPage();
try {
// Клиент заходит на страницу поддержки и создает тикет
await clientPage.goto('/support/chat');
await clientPage.getByRole('textbox', { name: 'Ваше сообщение' }).fill('Не проходит платеж по карте');
await clientPage.getByRole('button', { name: 'Отправить' }).click();
// Оператор открывает панель управления входящими тикетами
await agentPage.goto('/admin/tickets');
// Auto-waiting: сообщение клиента должно появиться в списке оператора по WebSocket
const ticketItem = agentPage.getByRole('listitem').filter({
hasText: 'Не проходит платеж по карте',
});
await expect(ticketItem).toBeVisible({ timeout: 8_000 });
// Оператор берет тикет в работу и отвечает
await ticketItem.getByRole('button', { name: 'Ответить' }).click();
await agentPage.getByRole('textbox', { name: 'Ответ клиенту' }).fill('Здравствуйте! Проверяем шлюз.');
await agentPage.getByRole('button', { name: 'Отправить ответ' }).click();
// Клиент получает ответ оператора в окне своего контекста
const incomingMessage = clientPage.getByRole('article').filter({
hasText: 'Здравствуйте! Проверяем шлюз.',
});
await expect(incomingMessage).toBeVisible();
} finally {
// Гарантированная очистка ресурсов обоих контекстов
await clientContext.close();
await agentContext.close();
}
});Почему использование жестких пауз page.waitForTimeout() и принудительного клика { force: true } считается антипаттерном в Playwright?
Жесткий сон (sleep) блокирует выполнение теста на фиксированное количество миллисекунд. На быстрых машинах это расходует ресурсы впустую, а на загруженных CI-серверах задержки может не хватить, что вызывает ложные падения тестов.
// ❌ Антипаттерн: жесткая пауза в надежде, что таблица отрисуется
await page.getByRole('button', { name: 'Загрузить' }).click();
await page.waitForTimeout(5000);
expect(await page.locator('.row').count()).toBe(10);
// ✅ Правильное решение: асинхронный ассерт с автоматическим ожиданием
await page.getByRole('button', { name: 'Загрузить' }).click();
await expect(page.locator('.row')).toHaveCount(10);Флаг force: true отключает проверку видимости, стабильности и хит-тест перекрытия. Клик отправляется непосредственно по координатам центра элемента, даже если поверх него открыто модальное окно ошибки или анимация еще не завершилась. Это маскирует критические дефекты пользовательского интерфейса.
// ❌ Антипаттерн: кнопка физически перекрыта спиннером, но тест принудительно кликает сквозь него
await page.getByRole('button', { name: 'Подтвердить' }).click({ force: true });
// ✅ Правильное решение: убедиться, что блокирующий оверлей исчез естественным путем
await expect(page.getByTestId('global-spinner')).toBeHidden();
await page.getByRole('button', { name: 'Подтвердить' }).click();Локаторы вида div > div.flex > button.btn-primary:nth-child(2) ломаются при малейшем рефакторинге верстки (например, замене div на section или оборачивании элементов во flex-контейнер), даже если поведение для пользователя не изменилось.
// ❌ Антипаттерн: локация по структуре верстки и CSS-классам
await page.locator('div.header-container > ul > li:nth-child(3) > a').click();
// ✅ Правильное решение: локация по семантической роли и имени
await page.getByRole('navigation').getByRole('link', { name: 'Документация' }).click();Создание нового инстанса Browser (chromium.launch()) на каждый тест в хуках beforeEach увеличивает время прогона набора тестов в 10–20 раз из-за инициализации процессов операционной системы.
Решение: использовать стандартную модель фикстур Playwright, где процесс Browser создается один раз на worker thread, а каждый тест получает чистый легковесный BrowserContext.