Durante años, las pruebas de componentes en React se fundamentaron en el uso combinado de Vitest/Jest y React Testing Library (RTL) ejecutándose dentro de entornos virtuales sintéticos (jsdom o happy-dom) en procesos Node.js.
Históricamente, este compromiso equilibraba la velocidad frente a la precisión. Los navegadores completos exigían recursos considerables, mientras que jsdom proporcionaba una emulación en memoria ligera del árbol DOM. No obstante, en las interfaces web modernas basadas en React 19, Tailwind CSS v4, animaciones avanzadas y CSS Grid, este compromiso ha quedado obsoleto debido a limitaciones técnicas insuperables de jsdom:
jsdom no realiza cálculos geométricos reales. Las llamadas a getBoundingClientRect(), offsetWidth y clientHeight devuelven ceros o aproximaciones simplistas. Resulta imposible determinar si un diálogo modal superpone a un botón, si un texto se trunca mediante text-overflow: ellipsis, o si un elemento se ancla adecuadamente con position: sticky.jsdom no analiza ni aplica estilos de Flexbox, CSS Grid, container queries (@container) o variables CSS nativas (var(--color-primary)).:hover, :active y :focus-visible carecen de soporte de dispositivos de puntero físicos en jsdom. No es posible simular trayectorias de ratón ni clics en coordenadas reales.jsdom no es un navegador. Las discrepancias de renderizado de WebKit (Safari) o particularidades de composición en Gecko (Firefox) son imposibles de reproducir.+-----------------------------------------------------------------------------+
| Limitaciones del entorno jsdom / happy-dom |
| |
| Node.js Memory -> Parser sintáctico HTML -> Emulación de nodos DOM |
| [Layout = 0px] [CSS Grid = Ignorado] [Tipografías / GPU = Inexistentes] |
+-----------------------------------------------------------------------------+
+-----------------------------------------------------------------------------+
| Playwright Component Testing (CT) en navegadores reales |
| |
| Test Runner (Node.js) |
| │ |
| ▼ (mount(<Button variant="primary" />)) |
| Servidor local Vite CT Bundler |
| │ (Compilación del componente aislado + Tailwind v4 CSS) |
| ▼ |
| Motor de navegador real (Chromium / Firefox / WebKit) |
| │ |
| ├──► 100% Real Layout Engine: CSS Grid, Flexbox, BoundingBox, z-index |
| ├──► Stack físico de entrada: mousemove, hover, drag-and-drop, focus |
| └──► Renderizado de fuentes por hardware y cálculo de CSS computado |
+-----------------------------------------------------------------------------+Playwright Component Testing (CT) erradica esta brecha técnica, uniendo la velocidad de las pruebas de componentes con la fidelidad absoluta de un entorno de navegador real.
En Playwright CT, el componente se ejecuta aislado del backend y del árbol de rutas de la aplicación, pero dentro de un proceso de navegador auténtico.
+-----------------------------------------------------------------------------+
| Proceso Test Runner de Node.js |
| test('renderiza interacción activa de tarjeta', async ({ mount }) => { ...|
+-----------------------------------------------------------------------------+
│
▼ (Comando de montaje)
+-----------------------------------------------------------------------------+
| Servidor Dev Vite CT |
| - Página anfitriona: playwright/index.html |
| - Inyección del sistema de diseño: playwright/index.tsx (Tailwind v4) |
| - Empaquetado dinámico del componente bajo demanda |
+-----------------------------------------------------------------------------+
│
▼ (WebSocket CDP / WebDriver BiDi)
+-----------------------------------------------------------------------------+
| Navegador real (Chromium / WebKit / Firefox) |
| |
| +---------------------------------------------------------------------+ |
| | HTML Shell (playwright/index.html) | |
| | <div id="root"> | |
| | <!-- Componente React 19 montado --> | |
| | <UserProfileCard user={mockUser} /> | |
| | </div> | |
| +---------------------------------------------------------------------+ |
| │ |
| ▼ |
| Cálculo geométrico (BoundingBox) -> Aserciones de CSS computado |
+-----------------------------------------------------------------------------+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: {
/* Puerto del servidor local de Vite para montaje */
ctPort: 3100,
/* Configuración del empaquetador 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#El archivo anfitrión configura el entorno de montaje, importa los estilos globales de Tailwind CSS v4 y expone los hooks de ciclo de vida:
import '@/app/globals.css';
import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
// Registrar hooks globales del ciclo de vida de componentes
beforeMount(async ({ App }) => {
// Por ejemplo, activar el tema oscuro en el elemento raíz para sistemas de diseño
document.documentElement.classList.add('dark');
return <App />;
});En jsdom es imposible verificar que la cabecera de una tabla permanezca anclada al hacer scroll. En Playwright CT podemos evaluar el comportamiento real de 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: `Elemento de catálogo #${i}`,
value: `${(i + 1) * 10} €`,
}));
test.describe('<DataTable /> Sticky Layout en navegador real', () => {
test('la cabecera de la tabla mantiene su posición en el viewport al hacer scroll', async ({
mount,
page,
}) => {
// Montamos el componente en un contenedor con overflow y altura fija
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: 'Nombre' });
// Obtenemos la posición geométrica inicial respecto al viewport
const initialBox = await header.boundingBox();
expect(initialBox).not.toBeNull();
const initialY = initialBox!.y;
// Desplazamos el contenedor 500 píxeles hacia abajo
await page.locator('#scroll-container').evaluate((el) => {
el.scrollTop = 500;
});
// Validamos que la cabecera conserva la misma coordenada Y gracias a position: sticky
const scrolledBox = await header.boundingBox();
expect(scrolledBox).not.toBeNull();
expect(scrolledBox!.y).toBeCloseTo(initialY, 1);
// Comprobamos el estilo CSS computado
await expect(header).toHaveCSS('position', 'sticky');
});
});:hover)#Comprobación de transiciones visuales bajo la interacción física del cursor:
import React from 'react';
import { test, expect } from '@playwright/experimental-ct-react';
import { ActionButton } from './ActionButton';
test.describe('<ActionButton /> Pseudoclases interactivas', () => {
test('el botón aplica los estilos calculados de Tailwind v4 al pasar el ratón', async ({ mount }) => {
let clickCount = 0;
const component = await mount(
<ActionButton
variant="accent"
onClick={() => {
clickCount += 1;
}}
>
Publicar lección
</ActionButton>
);
// 1. Validamos los estilos base iniciales
await expect(component).toHaveCSS('cursor', 'pointer');
// 2. Colocamos físicamente el cursor sobre el botón
await component.hover();
// 3. Confirmamos que el motor del navegador aplicó el estilo hover computado
await expect(component).toHaveCSS('transition-property', /background-color|all/);
// 4. Ejecutamos el clic físico
await component.click();
expect(clickCount).toBe(1);
});
});Verificación de la accesibilidad de un menú desplegable manejado íntegramente con el teclado:
import React from 'react';
import { test, expect } from '@playwright/experimental-ct-react';
import { DropdownMenu } from './DropdownMenu';
test('navegación por elementos del menú mediante teclas de dirección', async ({
mount,
page,
}) => {
const component = await mount(
<DropdownMenu
triggerLabel="Acciones"
items={[
{ id: 'edit', label: 'Editar registro' },
{ id: 'duplicate', label: 'Duplicar entrada' },
{ id: 'delete', label: 'Eliminar' },
]}
/>
);
const trigger = component.getByRole('button', { name: 'Acciones' });
// Foco en el botón mediante Tab
await page.keyboard.press('Tab');
await expect(trigger).toBeFocused();
// Abrir menú con Enter
await page.keyboard.press('Enter');
// El menú debe ser visible
const menuList = page.getByRole('menu');
await expect(menuList).toBeVisible();
// Desplazarse al primer elemento con la flecha abajo
await page.keyboard.press('ArrowDown');
const firstItem = page.getByRole('menuitem', { name: 'Editar registro' });
await expect(firstItem).toBeFocused();
// Desplazarse al segundo elemento
await page.keyboard.press('ArrowDown');
const secondItem = page.getByRole('menuitem', { name: 'Duplicar entrada' });
await expect(secondItem).toBeFocused();
});Los componentes en Playwright CT se empaquetan mediante Vite para ejecutarse en el navegador. Importar módulos de Node.js (fs, path, crypto, child_process) o Server Actions de Next.js sin polyfills provocará errores fatales de compilación en Vite.
// ❌ INCORRECTO: Intento de lectura de archivos en el bundle del navegador
import fs from 'fs';
export function BadComponent() {
const data = fs.readFileSync('config.json'); // Error de compilación en Vite
}
// ✅ CORRECTO: Pasar configuraciones a través de props serializables
export function GoodComponent({ config }: { config: AppConfig }) {
return <div>{config.title}</div>;
}Si no se importa el archivo de estilos raíz (globals.css) en playwright/index.tsx, los componentes se renderizarán en un documento HTML plano sin clases de Tailwind CSS. Las aserciones toHaveCSS, las comprobaciones de breakpoints responsivos y las pruebas visuales fallarán.
Inicializar el navegador conlleva una sobrecarga de arranque (20–50 ms por test). Ejecutar cientos de funciones puras, validadores de cadenas o algoritmos dentro de Playwright CT ralentizará innecesariamente los pipelines de CI. Solución: Utilizar Vitest en Node.js para funciones puras y lógica de negocio, reservando Playwright CT para la capa visual, maquetación compleja y validación de accesibilidad.
Limitar las pruebas a Chromium elimina la mayor ventaja de Playwright CT frente a jsdom: las garantías de compatibilidad entre navegadores. WebKit (motor de Safari) posee particularidades en Flexbox, suavizado de fuentes y line-clamp que deben validarse en las pruebas de componentes.
Instalar @playwright/experimental-ct-react y crear la plantilla anfitriona playwright/index.html.
Importar estilos globales de Tailwind v4 y configurar proveedores en playwright/index.tsx.
Importar test/expect de ct-react, montar el componente con mount() y validar el comportamiento.
Vite genera los bundles aislados y los transmite a las pestañas del navegador.
Ejecución paralela en motores Chromium, Firefox y WebKit con generación de trazas de error.