Віртуальні DOM-середовища (jsdom та happy-dom) чудово підходять для швидкої перевірки бізнес-логіки та деревоподібних структур компонентів. Проте вони принципово не здатні перевірити візуальний шар інтерфейсу, оскільки в них повністю відсутній справжній графічний рушій рендерингу:
getBoundingClientRect() повертають нулі або примітивні евристики замість фізичних пікселів екрана.@container та медіа-запити колірних схем.:hover, :active, :focus-visible та апаратні CSS-транзишени.Паттерн Playwright Component Testing (CT) усуває цей розрив: компонент монтується безпосередньо в реальний браузер, отримуючи доступ до повноцінного графічного стека операційної системи та відеокарти (GPU).
Це дозволяє автоматизувати попіксельне скриншотне тестування (Visual Regression Testing) та гарантувати бездоганний UI на всіх пристроях.
На відміну від класичного E2E-тестування, де браузер завантажує повну кодову базу застосунку за мережевою адресою (page.goto('/dashboard')), у Playwright CT локальний легковаговий Vite-сервер компілює та монтує конкретний ізольований React-компонент у порожній тестовий HTML-хост за 20–50 мс:
[Тестовий раннер Playwright Node.js]
│
▼ (Команда mount(<Button variant="primary" />))
[Локальний Vite CT Dev-сервер]
│ ──► Збирає мінімальний HTML-хост (playwright/index.html)
│ ──► Підключає Tailwind CSS v4 та глобальні токени теми
▼
[Реальний процес браузера: Chromium / Firefox / WebKit]
│
├──► Розрахунок точних координат (BoundingBox, z-index)
├──► Рендеринг апаратними засобами GPU
├──► Перевірка hover-ефектів та CSS-транзишенів
│
▼ (Порівняння буфера кадрів)
[Visual Regression Engine: toHaveScreenshot()]
│
└──► Попіксельний дифінг із золотим еталоном (Golden Master)Встановіть пакет раннера та завантажте браузерні бінарники за допомогою pnpm exec playwright install.
Сконфігуруйте playwright/index.html та playwright/index.ts, підключіть стилі Tailwind CSS v4 і теми.
Імпортуйте test та expect, змонтуйте компонент через mount() і опишіть дії та скриншоти.
Виконайте pnpm exec playwright test -u для генерації знімків-еталонів у директорії __snapshots__.
Запустіть тести в офіційному Docker-образі Playwright для усунення шрифтових розбіжностей між OS.
Чому візуальне тестування верстки (CSS Grid, правильність z-index та переповнення Viewport) неможливо здійснити за допомогою Vitest у поєднанні з jsdom?
position: sticky та складних SVG у Safari (WebKit) та Chrome (Blink).focus-visible: ring-2 ring-accent-cyan) під час навігації клавішею Tab.Транспіляція JSX, підключення CSS-токенів Tailwind v4 та ін'єкція в чистий HTML-хост.
Chromium/WebKit запускається з фіксацією щільності пікселів (devicePixelRatio) та шрифтів.
Наведення курсору миші (.hover()), введення тексту або фокусування з клавіатури.
Захоплення скриншота елемента та попіксельне порівняння з маскуванням динамічних зон.
Конфігураційний файл playwright-ct.config.ts визначає браузерні рушії, директорію скриншотів та параметри сервера Vite:
import { defineConfig, devices } from '@playwright/experimental-ct-react';
import path from 'path';
export default defineConfig({
testDir: './src',
testMatch: /.*\.ct\.(ts|tsx)$/,
snapshotDir: './__snapshots__',
timeout: 15 * 1000,
fullyParallel: true,
use: {
trace: 'on-first-retry',
ctPort: 3100,
ctViteConfig: {
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
},
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
],
});// Підключення глобальних стилів дизайн-системи Tailwind CSS v4
import '@/app/globals.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
// Хук перед монтуванням кожного компонента (фіксація темної теми)
beforeMount(async ({ App }) => {
document.documentElement.classList.add('dark');
return <App />;
});Метод toHaveScreenshot() захоплює буфер кадру компонента та здійснює дифінг з еталоном. Для захисту від хибних спрацьовувань через субпіксельне згладжування шрифтів налаштовуються параметри допуску:
await expect(component).toHaveScreenshot('button-primary-hover.png', {
maxDiffPixelRatio: 0.01, // Допустимо не більше 1% змінених пікселів (згладжування шрифтів)
threshold: 0.2, // Поріг колірної відмінності окремого пікселя
animations: 'disabled', // Зупинка CSS-анімацій для стабілізації кадру
});Тест toHaveScreenshot() локально на macOS розробника проходить успішно, проте в GitHub Actions на Linux-агенті постійно падає з 0.8% неспівпадінням пікселів навколо літер. У чому полягає причина і як правильно вирішити проблему?
Нижче наведено компонентний тест для складної інтерактивної картки профілю (UserProfileCard.ct.tsx), що перевіряє стилі Tailwind v4, адаптивність та скриншотні регресії:
import React from 'react';
import { test, expect } from '@playwright/experimental-ct-react';
import { UserProfileCard, type UserProfileData } from '../UserProfileCard';
const MOCK_USER: UserProfileData = {
id: 'usr-9812',
name: 'Костянтин Архітекторов',
role: 'Principal Staff Engineer',
avatarUrl: '/avatars/sample.webp',
reputation: 9840,
joinedDate: '2026-03-15T12:00:00Z',
isVerified: true,
};
test.describe('<UserProfileCard /> Visual & Interaction CT', () => {
test('відповідає візуальному еталону в десктопному представленні', async ({ mount }) => {
const component = await mount(
<div className="p-8 bg-[#0B0B11]">
<UserProfileCard user={MOCK_USER} />
</div>
);
// Маскуємо аватар, щоб уникнути флаків через затримку підвантаження картинки
await expect(component).toHaveScreenshot('user-card-desktop-idle.png', {
mask: [component.locator('[data-testid="user-avatar"]')],
maxDiffPixelRatio: 0.01,
animations: 'disabled',
});
});
test('коректно відображає Tailwind hover-ефекти та активні стани', async ({ mount }) => {
const component = await mount(
<div className="p-8 bg-[#0B0B11]">
<UserProfileCard user={MOCK_USER} />
</div>
);
const followButton = component.getByRole('button', { name: /підписатися/i });
// Емулюємо фізичне наведення курсору миші
await followButton.hover();
// Перевіряємо обчислений курсор та CSS-стиль
await expect(followButton).toHaveCSS('cursor', 'pointer');
// Фіксуємо скриншот кнопки під ховером
await expect(followButton).toHaveScreenshot('user-card-follow-button-hover.png');
// Клікаємо по кнопці
await followButton.click();
await expect(component.getByRole('button', { name: /ви підписані/i })).toBeVisible();
});
test('адаптується під мобільний Viewport 375x667 без горизонтального скролу', async ({
mount,
page,
}) => {
// Встановлюємо мобільний розмір екрана
await page.setViewportSize({ width: 375, height: 667 });
const component = await mount(
<div className="p-4 bg-[#0B0B11]">
<UserProfileCard user={MOCK_USER} />
</div>
);
// Перевіряємо, що картка не виходить за межі екрана
const boundingBox = await component.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
expect(boundingBox.width).toBeLessThanOrEqual(375);
}
// Скриншот мобільної компоновки
await expect(component).toHaveScreenshot('user-card-mobile-layout.png', {
mask: [component.locator('[data-testid="user-avatar"]')],
maxDiffPixelRatio: 0.02,
});
});
});Один і той самий браузер Chromium по-різному згладжує шрифти (Font Subpixel Antialiasing) на macOS, Windows та Linux у GitHub Actions. Запуск toHaveScreenshot() з нульовим порогом неминуче ламає CI.
// ❌ Антипатерн: 0% терпимості до пікселів (тест впаде на Linux у CI)
await expect(component).toHaveScreenshot('modal.png');
// ✅ Правильне рішення: налаштувати допустимий коефіцієнт розбіжності
await expect(component).toHaveScreenshot('modal.png', {
maxDiffPixelRatio: 0.02, // 2% відхилення пікселів на межах шрифтів
threshold: 0.2,
});Якщо компонент містить нескінченний пульсуючий індикатор (animate-pulse) або обертовий спінер (animate-spin), скриншот фіксується у випадковій фазі кадру, викликаючи постійні хибні падіння.
// ❌ Антипатерн: скриншот лоадера в довільний момент фази
await expect(component).toHaveScreenshot();
// ✅ Правильне рішення: зупинка анімацій через опцію animations: 'disabled'
// або маскування анімованого вузла через mask
await expect(component).toHaveScreenshot({
animations: 'disabled',
mask: [component.locator('.animate-pulse')],
});Запуск реального браузера вимагає часу на ініціалізацію процесу (20–60 мс на тест). Спроба переписати всі 1 000 модульних тестів логіки на Playwright CT призведе до того, що прогін CI триватиме пів години замість 10 секунд. Рішення: Використовувати Vitest для 80% перевірок бізнес-логіки та хуків, а Playwright CT — суворо для перевірки візуального шару, складних CSS Grid сіток та ключових компонентів дизайн-системи.