В течение многих лет тестирование компонентов интерфейса в 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 возвращают нули или захардкоженные эвристики. Невозможно проверить, перекрывает ли модальное окно кнопку, ушел ли текст в 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: CSS Grid, Flexbox, BoundingBox, z-index |
| ├──► Физический стек ввода: 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/index.html.
Импорт глобального файла Tailwind v4 и конфигурация темы в файле playwright/index.tsx.
Импорт test/expect из ct-react, монтирование компонента через mount() и проверка состояний.
Vite компилирует изолированные чанки компонентов и передает их в открытые браузерные вкладки.
Параллельный запуск спецификаций в процессах Chromium, Firefox и WebKit с сохранением отчетов.
Почему тестирование адаптивной верстки (CSS Grid, Flexbox, Tailwind) и состояния hover невозможно полноценно выполнить в jsdom/happy-dom?
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: 'Наименование' });
// Проверяем начальное положение заголовка относительно вьюпорта
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
// Значение rgb/hex вычисляется настоящим движком рендеринга
await expect(component).toHaveCSS('transition-property', /background-color|all/);
// 4. Физический клик
await component.click();
expect(clickCount).toBe(1);
});
});Проверка доступности выпадающего меню при управлении только с клавиатуры:
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();
});Почему импорт модулей Node.js (fs, path, crypto) или Server Actions внутри компонентов, тестируемых через Playwright CT, приводит к ошибке сборки?
Компоненты в 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'); // Ошибка сборки в браузере
}
// ✅ Правильное решение: передавать данные через сериализуемые пропсы компонента
export function GoodComponent({ config }: { config: AppConfig }) {
return <div>{config.title}</div>;
}Если в playwright/index.tsx не импортирован корневой файл стилей (globals.css), компоненты будут смонтированы в чистый HTML-шелл без стилей Tailwind CSS. Все проверки toHaveCSS(), проверки видимости скрытых по брейкпоинтам элементов и визуальные диффы упадут.
Запуск браузера требует инициализации процесса (20–50 мс на тест). Запуск 500 тестов математических функций, парсеров строк или утилит валидации форм в Playwright CT замедлит CI до нескольких минут. Решение: использовать быстрый Vitest в Node.js для чистой бизнес-логики, а Playwright CT — исключительно для проверки визуального слоя, сложных UI-компонентов и контрактов доступности в браузерах.
Запуск CT только в Chrome лишает команду главного преимущества Playwright CT перед jsdom — кроссбраузерной гарантии. WebKit (движок Safari) имеет специфические особенности обработки Flexbox, субпиксельного сглаживания и line-clamp, которые необходимо отлавливать на этапе компонентного тестирования.