Історично автоматизація веб-браузерів будувалася навколо стандарту 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 просуває філософію тестування через призму сприйняття кінцевого користувача та специфікації 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: Переконуємося, що лоадер повністю зник з DOM
await expect(skeletonLoader).not.toBeAttached({ timeout: 10_000 });
// Крок 2: Веб-асерти expect(locator).to* мають вбудований механізм auto-waiting
// Вони періодично опитують стан до досягнення умови або таймауту
await expect(tableRows).toHaveCount(31); // 1 header + 30 data rows
// Крок 3: Перевіряємо конкретне значення в рядку
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: 'uk-UA',
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();
}
});Жорсткий сон (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 вимикає перевірку видимості, стабільності та хіт-тест перекриття. Клік надсилається безпосередньо за координатами центру елемента, навіть якщо поверх нього відкрито модальне вікно помилки або анімація ще не завершилася. Це маскує критичні дефекти UI.
// ❌ Антипатерн: кнопка фізично перекрита спінером, але тест примусово клікає крізь нього
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.
Playwright резолвить селектор у DOM. Якщо під умову потрапляє більше одного елемента, викидається Strict Mode Violation.
Переконується, що елемент підключений до активного документа (isAttached) і відображається (не має display: none, ненульовий розмір).
Виконується порівняння обмежувального прямокутника (BoundingBox) протягом щонайменше двох послідовних кадрів анімації.
Елемент автоматично прокручується в область видимості. Метод document.elementFromPoint перевіряє, що координати не перекриті іншими шарами.
Через протокол CDP або BiDi відправляються фізичні події миші: mousemove, mousedown, mouseup, click.