Протягом багатьох років тестування інтерфейсних компонентів у React спиралося на зв'язку Vitest/Jest та бібліотеку React Testing Library (RTL), що працює всередині емульованого віртуального DOM-середовища (jsdom або happy-dom) у процесі Node.js.
Історично цей компроміс балансував швидкість виконання та точність перевірок. Повноцінні браузери вимагали значних ресурсів процесора й пам'яті, тоді як jsdom надавав легковиправну імітацію дерева DOM. Проте в сучасних вебінтерфейсах на базі React 19, Tailwind CSS v4, складних анімацій та CSS Grid цей компроміс вичерпав себе через фундаментальні технічні обмеження jsdom:
jsdom не виконує геометричних розрахунків. Виклики getBoundingClientRect(), offsetWidth та clientHeight повертають нулі або жорстко зашиті евристики. Неможливо перевірити, чи модальне вікно не перекриває кнопку, чи обрізається текст за правилом text-overflow: ellipsis, або чи коректно зафіксовано елемент за допомогою position: sticky.jsdom не парсить і не застосовує стилі Flexbox, сітки CSS Grid, контейнерні запити (@container) та змінні CSS (var(--color-primary)).:hover, :active, :focus-visible у jsdom позбавлені підтримки фізичного координатного вводу. Неможливо емулювати траєкторію миші або клік за реальними координатами.jsdom не є браузером. Особливості рендерингу WebKit (Safari) або специфіка композитингу шарів у Gecko (Firefox) у ньому принципово не відтворюються.+-----------------------------------------------------------------------------+
| Обмеження середовища jsdom / happy-dom |
| |
| Node.js Memory -> Синтаксичний парсер HTML -> Емуляція дерева вузлів |
| [Layout = 0px] [CSS Grid = Ігнорується] [Шрифти / GPU = Відсутні] |
+-----------------------------------------------------------------------------+
+-----------------------------------------------------------------------------+
| Playwright Component Testing (CT) у реальних браузерах |
| |
| Test Runner (Node.js) |
| │ |
| ▼ (mount(<Button variant="primary" />)) |
| Локальний Vite CT Bundler |
| │ (Компіляція ізольованого компонента + Tailwind v4 CSS) |
| ▼ |
| Реальний браузерний рушій (Chromium / Firefox / WebKit) |
| │ |
| ├──► 100% реальний Layout Engine: CSS Grid, Flexbox, BoundingBox |
| ├──► Фізичний стек вводу: mousemove, hover, drag-and-drop, tab-фокус |
| └──► Апаратний рендеринг шрифтів та обчислення CSS-властивостей |
+-----------------------------------------------------------------------------+Playwright Component Testing (CT) усуває цю проблему, поєднуючи швидкість компонентних тестів із бездоганною точністю справжнього браузерного оточення.
У Playwright CT компонент тестується ізольовано від бекенду та маршрутизації всього застосунку, проте всередині справжнього процесу браузера.
+-----------------------------------------------------------------------------+
| Node.js Test Runner Process |
| test('відображає активний стан картки', async ({ mount }) => { ... |
+-----------------------------------------------------------------------------+
│
▼ (Команда монтування)
+-----------------------------------------------------------------------------+
| Vite CT Dev-сервер |
| - Хостова сторінка: playwright/index.html |
| - Підключення дизайн-системи: playwright/index.tsx (Tailwind v4) |
| - Динамічний бандлінг монтованого компонента |
+-----------------------------------------------------------------------------+
│
▼ (WebSocket CDP / WebDriver BiDi)
+-----------------------------------------------------------------------------+
| Реальний браузер (Chromium / WebKit / Firefox) |
| |
| +---------------------------------------------------------------------+ |
| | HTML Shell (playwright/index.html) | |
| | <div id="root"> | |
| | <!-- Змонтований компонент React 19 --> | |
| | <UserProfileCard user={mockUser} /> | |
| | </div> | |
| +---------------------------------------------------------------------+ |
| │ |
| ▼ |
| Обчислення точних координат (BoundingBox) -> Перевірка стилів getComputed |
+-----------------------------------------------------------------------------+playwright-ct.config.ts#import { defineConfig, devices } from '@playwright/experimental-ct-react';
import path from 'path';
export default defineConfig({
testDir: './src',
testMatch: /.*\.ct\.(ts|tsx)$/,
timeout: 15_000,
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
use: {
/* Порт для локального Vite-сервера монтування */
ctPort: 3100,
/* Конфігурація бандлінгу Vite */
ctViteConfig: {
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
},
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});playwright/index.tsx#Вхідна точка хоста налаштовує середовище, імпортує глобальні стилі Tailwind CSS v4 та підключає хуки життєвого циклу:
import '@/app/globals.css';
import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
// Реєстрація глобальних хуків життєвого циклу компонентів
beforeMount(async ({ App }) => {
// Наприклад, примусове встановлення темної теми для дизайн-системи
document.documentElement.classList.add('dark');
return <App />;
});У jsdom неможливо перевірити, що заголовок таблиці зберігає фіксовану позицію під час прокручування. У Playwright CT можна детерміновано протестувати справжню поведінку position: sticky:
import React from 'react';
import { test, expect } from '@playwright/experimental-ct-react';
import { DataTable } from './DataTable';
const MOCK_ROWS = Array.from({ length: 40 }, (_, i) => ({
id: `row-${i}`,
name: `Елемент каталогу #${i}`,
value: `${(i + 1) * 100} ₴`,
}));
test.describe('<DataTable /> Sticky Layout in Real Browser', () => {
test('заголовок таблиці зберігає фіксовану координату під час вертикального скролу', async ({
mount,
page,
}) => {
// Монтуємо компонент у контейнері з фіксованою висотою та скролом
const component = await mount(
<div className="h-[300px] overflow-y-auto border border-border" id="scroll-container">
<DataTable data={MOCK_ROWS} />
</div>
);
const header = component.getByRole('rowheader', { name: 'Найменування' });
// Отримуємо початковий bounding box відносно в'юпорту
const initialBox = await header.boundingBox();
expect(initialBox).not.toBeNull();
const initialY = initialBox!.y;
// Скролимо контейнер униз на 500 пікселів
await page.locator('#scroll-container').evaluate((el) => {
el.scrollTop = 500;
});
// Переконуємось, що заголовок залишився на тій самій Y-координаті (position: sticky)
const scrolledBox = await header.boundingBox();
expect(scrolledBox).not.toBeNull();
expect(scrolledBox!.y).toBeCloseTo(initialY, 1);
// Перевіряємо обчислене CSS-правило
await expect(header).toHaveCSS('position', 'sticky');
});
});:hover)#Перевірка плавних переходів стилів під час наведення фізичного курсора миші:
import React from 'react';
import { test, expect } from '@playwright/experimental-ct-react';
import { ActionButton } from './ActionButton';
test.describe('<ActionButton /> Interactive Pseudo-classes', () => {
test('кнопка застосовує розраховані стилі Tailwind v4 при наведенні курсора', async ({ mount }) => {
let clickCount = 0;
const component = await mount(
<ActionButton
variant="accent"
onClick={() => {
clickCount += 1;
}}
>
Опублікувати
</ActionButton>
);
// 1. Перевіряємо базові стилі курсора
await expect(component).toHaveCSS('cursor', 'pointer');
// 2. Фізично наводимо курсор на кнопку
await component.hover();
// 3. Перевіряємо, що реальний браузер застосував стиль hover
await expect(component).toHaveCSS('transition-property', /background-color|all/);
// 4. Фізичний клік
await component.click();
expect(clickCount).toBe(1);
});
});Перевірка відповідності випадаючого списку вимогам A11y виключно за допомогою клавіатури:
import React from 'react';
import { test, expect } from '@playwright/experimental-ct-react';
import { DropdownMenu } from './DropdownMenu';
test('навігація по пунктах меню за допомогою стрілок клавіатури', async ({
mount,
page,
}) => {
const component = await mount(
<DropdownMenu
triggerLabel="Дії"
items={[
{ id: 'edit', label: 'Редагувати запис' },
{ id: 'duplicate', label: 'Створити копію' },
{ id: 'delete', label: 'Видалити' },
]}
/>
);
const trigger = component.getByRole('button', { name: 'Дії' });
// Переходимо до кнопки за допомогою клавіші Tab
await page.keyboard.press('Tab');
await expect(trigger).toBeFocused();
// Відкриваємо меню клавішею Enter
await page.keyboard.press('Enter');
// Меню має стати видимим
const menuList = page.getByRole('menu');
await expect(menuList).toBeVisible();
// Переходимо до першого пункту стрілкою вниз
await page.keyboard.press('ArrowDown');
const firstItem = page.getByRole('menuitem', { name: 'Редагувати запис' });
await expect(firstItem).toBeFocused();
// Переходимо до другого пункту
await page.keyboard.press('ArrowDown');
const secondItem = page.getByRole('menuitem', { name: 'Створити копію' });
await expect(secondItem).toBeFocused();
});Компоненти у Playwright CT компілюються Vite для виконання безпосередньо в браузері. Спроба імпортувати пакети Node.js (fs, path, crypto, child_process) чи Server Actions Next.js без моків викличе фатальну помилку бандлінгу Vite.
// ❌ АНТИПАТЕРН: спроба читання файлу з диска всередині браузерного бандла
import fs from 'fs';
export function BadComponent() {
const data = fs.readFileSync('config.json'); // Помилка компіляції Vite
}
// ✅ ПРАВИЛЬНО: передавати конфігурацію через серіалізовані пропси
export function GoodComponent({ config }: { config: AppConfig }) {
return <div>{config.title}</div>;
}Якщо у файлі playwright/index.tsx пропущено імпорт кореневого файлу стилів (globals.css), компоненти монтуватимуться у порожній HTML-документ без класів Tailwind CSS. Усі перевірки toHaveCSS, перевірки адаптивних брейкпоінтів та візуальні знімки зазнають невдачі.
Запуск браузерного контексту потребує часу (20–50 мс на тест). Виконання сотень тестів чистих функцій, валідаторів форм чи алгоритмів через Playwright CT значно збільшить тривалість прогону в CI. Рішення: використовувати надшвидкий Vitest у Node.js для чистих функцій та бізнес-логіки; залишати Playwright CT для візуального шару, розрахунків геометрії та перевірок доступності.
Запуск CT лише в Chromium нівелює головну перевагу Playwright CT над jsdom — кросбраузерну надійність. Рушій WebKit (Safari) має специфічні особливості субпіксельного рендерингу, Flexbox та line-clamp, які необхідно виявляти на етапі тестування компонентів.
Встановлення залежності та генерація шаблону хостової сторінки playwright/index.html.
Імпорт глобального файлу стилів Tailwind v4 та провайдерів у playwright/index.tsx.
Імпорт test/expect із ct-react, монтування компонента через mount() та асерти поведінки.
Vite компілює ізольовані чанки та передає їх у відкриті вкладки браузерів.
Паралельне виконання специфікацій у рушіях Chromium, Firefox та WebKit із генерацією артефактів.