El paquete @playwright/experimental-ct-react monta componentes React individuales en un navegador real usando un fixture mount(): puedes testear el comportamiento de la tecla Escape en un dropdown o el estado de error de un formulario sin levantar toda la aplicación. Usa los mismos locators y aserciones que tus tests de Playwright normales, configurados mediante un playwright-ct.config.ts separado. Esta guía cubre la configuración, el testeo de props y event handlers, la re-renderización con props actualizadas en medio de un test, la regresión visual a nivel de componente, y cuándo los tests de componentes reemplazan a los E2E y cuándo no.

Cuándo tiene sentido el testing de componentes

Usa tests de componentes para

  • Componentes de UI con lógica de interacción compleja (dropdowns, modals, date pickers)
  • Componentes difíciles de alcanzar vía E2E (estados límite, estados de error)
  • Componentes de design system que necesitan verificación de consistencia visual
  • Testear event handling y props sin la aplicación completa

No reemplaces los tests E2E con tests de componentes

Los tests de componentes no cubren routing, autenticación, integración con API, ni la interacción entre componentes. Necesitas ambos.

Configuración

npm init playwright@latest -- --ct
# O agregar a un proyecto existente:
npm install --save-dev @playwright/experimental-ct-react

Esto crea un playwright-ct.config.ts separado de tu configuración de Playwright normal:

// playwright-ct.config.ts
import { defineConfig, devices } from '@playwright/experimental-ct-react';

export default defineConfig({
  testDir: './src',
  testMatch: '**/*.ct.spec.{ts,tsx}',
  
  use: {
    ctPort: 3100,  // Puerto para el servidor de tests de componentes
    viewport: { width: 1280, height: 720 },
  },
  
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
  ],
});

Tu primer test de componente

// components/Button.tsx
interface ButtonProps {
  label: string;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  onClick?: () => void;
}

export function Button({ label, variant = 'primary', disabled, onClick }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      disabled={disabled}
      onClick={onClick}
      data-testid="button"
    >
      {label}
    </button>
  );
}

// components/Button.ct.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';

test('renderiza con el label correcto', async ({ mount }) => {
  const component = await mount(<Button label="Hacer clic" />);
  
  await expect(component).toContainText('Hacer clic');
});

test('aplica la clase de variante', async ({ mount }) => {
  const component = await mount(<Button label="Peligro" variant="danger" />);
  
  await expect(component).toHaveClass(/btn-danger/);
});

test('el botón deshabilitado no puede clickearse', async ({ mount }) => {
  const component = await mount(<Button label="Deshabilitado" disabled />);
  
  await expect(component.getByTestId('button')).toBeDisabled();
});

test('llama a onClick cuando se hace clic', async ({ mount }) => {
  let clicked = false;
  
  const component = await mount(
    <Button 
      label="Hacer clic" 
      onClick={() => { clicked = true; }} 
    />
  );
  
  await component.click();
  
  expect(clicked).toBe(true);
});

Ejecuta los tests de componentes:

npx playwright test --config playwright-ct.config.ts

Testear un componente de formulario

// components/LoginForm.tsx
interface LoginFormProps {
  onSubmit: (email: string, password: string) => void;
  error?: string;
}

export function LoginForm({ onSubmit, error }: LoginFormProps) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      onSubmit(email, password);
    }}>
      <input
        data-testid="email"
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        data-testid="password"
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
        placeholder="Contraseña"
      />
      {error && <p data-testid="error">{error}</p>}
      <button type="submit" data-testid="submit">Iniciar sesión</button>
    </form>
  );
}

// components/LoginForm.ct.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { LoginForm } from './LoginForm';

test('envía con credenciales correctas', async ({ mount }) => {
  let emailEnviado = '';
  let passwordEnviado = '';
  
  const component = await mount(
    <LoginForm
      onSubmit={(email, password) => {
        emailEnviado = email;
        passwordEnviado = password;
      }}
    />
  );
  
  await component.getByTestId('email').fill('usuario@test.com');
  await component.getByTestId('password').fill('ClaveValida1');
  await component.getByTestId('submit').click();
  
  expect(emailEnviado).toBe('usuario@test.com');
  expect(passwordEnviado).toBe('ClaveValida1');
});

test('muestra el mensaje de error cuando se proporciona', async ({ mount }) => {
  const component = await mount(
    <LoginForm
      onSubmit={() => {}}
      error="Credenciales inválidas"
    />
  );
  
  await expect(component.getByTestId('error')).toHaveText('Credenciales inválidas');
});

test('no muestra error cuando error es undefined', async ({ mount }) => {
  const component = await mount(
    <LoginForm onSubmit={() => {}} />
  );
  
  await expect(component.getByTestId('error')).not.toBeVisible();
});

Testear un componente dropdown

Los componentes complejos con estado son donde los tests de componentes brillan:

// components/Dropdown.ct.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Dropdown } from './Dropdown';

const opciones = [
  { value: 'chrome', label: 'Chrome' },
  { value: 'firefox', label: 'Firefox' },
  { value: 'safari', label: 'Safari' },
];

test('el dropdown muestra opciones al hacer clic', async ({ mount }) => {
  const component = await mount(
    <Dropdown options={opciones} placeholder="Seleccionar navegador" />
  );
  
  // Inicialmente cerrado
  await expect(component.getByTestId('dropdown-list')).not.toBeVisible();
  
  // Clic para abrir
  await component.getByTestId('dropdown-trigger').click();
  
  // Ahora visible con todas las opciones
  await expect(component.getByTestId('dropdown-list')).toBeVisible();
  
  const items = component.getByTestId('dropdown-option');
  await expect(items).toHaveCount(3);
  await expect(items.nth(0)).toHaveText('Chrome');
});

test('se cierra después de seleccionar una opción', async ({ mount }) => {
  let seleccionado = '';
  
  const component = await mount(
    <Dropdown
      options={opciones}
      placeholder="Seleccionar navegador"
      onSelect={(value) => { seleccionado = value; }}
    />
  );
  
  await component.getByTestId('dropdown-trigger').click();
  await component.getByText('Firefox').click();
  
  expect(seleccionado).toBe('firefox');
  await expect(component.getByTestId('dropdown-list')).not.toBeVisible();
  await expect(component.getByTestId('dropdown-trigger')).toHaveText('Firefox');
});

test('se cierra con la tecla Escape', async ({ mount }) => {
  const component = await mount(
    <Dropdown options={opciones} placeholder="Seleccionar" />
  );
  
  await component.getByTestId('dropdown-trigger').click();
  await expect(component.getByTestId('dropdown-list')).toBeVisible();
  
  await component.press('Escape');
  await expect(component.getByTestId('dropdown-list')).not.toBeVisible();
});

Actualizar componentes en medio de un test

Puedes re-renderizar un componente con diferentes props:

test('el mensaje de error se actualiza cuando cambian las props', async ({ mount }) => {
  const component = await mount(
    <LoginForm onSubmit={() => {}} />
  );
  
  // Sin error al inicio
  await expect(component.getByTestId('error')).not.toBeVisible();
  
  // Actualizar props para agregar error
  await component.update(
    <LoginForm onSubmit={() => {}} error="Sesión expirada" />
  );
  
  await expect(component.getByTestId('error')).toHaveText('Sesión expirada');
  
  // Limpiar el error
  await component.update(
    <LoginForm onSubmit={() => {}} />
  );
  
  await expect(component.getByTestId('error')).not.toBeVisible();
});

Hooks en tests de componentes

Los tests de componentes de Playwright soportan los mismos hooks que los tests normales:

import { test, expect, beforeEach } from '@playwright/experimental-ct-react';

let contadorClicks = 0;

test.beforeEach(() => {
  contadorClicks = 0;
});

test('cuenta los clics correctamente', async ({ mount }) => {
  const component = await mount(
    <Button label="Clic" onClick={() => { contadorClicks++; }} />
  );
  
  await component.click();
  await component.click();
  await component.click();
  
  expect(contadorClicks).toBe(3);
});

Testing visual con componentes

Los tests de componentes funcionan bien con regresión visual:

test('las variantes del botón se ven correctas', async ({ mount }) => {
  for (const variant of ['primary', 'secondary', 'danger'] as const) {
    const component = await mount(<Button label={variant} variant={variant} />);
    await expect(component).toHaveScreenshot(`button-${variant}.png`);
  }
});

Esto es más confiable que los screenshots de página completa: hay menos piezas en movimiento y es más fácil aislar qué cambió.

Vue y Svelte

Los mismos conceptos aplican a Vue y Svelte:

# Vue
npm install --save-dev @playwright/experimental-ct-vue

# Svelte
npm install --save-dev @playwright/experimental-ct-svelte

<!-- Test de Vue -->
<script setup>
import { test, expect } from '@playwright/experimental-ct-vue'
import MiComponente from './MiComponente.vue'
</script>

Componente vs E2E: cuándo usar cada uno

| Escenario | Test de componente | Test E2E |

|-----------|-------------------|----------|

| Handler de clic en botón | ✅ | ❌ |

| Dropdown se abre/cierra | ✅ | ❌ |

| Mensajes de validación de formulario | ✅ | ❌ |

| Flujo de login de punta a punta | ❌ | ✅ |

| Navegación entre páginas | ❌ | ✅ |

| Integración con API | ❌ | ✅ |

| Regresión visual de componente | ✅ | A veces |

Resumen de comandos

# Configuración
npm install --save-dev @playwright/experimental-ct-react

# Config: playwright-ct.config.ts
# Tests: NombreComponente.ct.spec.tsx

# Ejecutar
npx playwright test --config playwright-ct.config.ts

Operaciones clave:

  • mount(): renderiza un componente en un navegador real
  • component.getByTestId(...): locators estándar de Playwright
  • component.click(): acciones estándar de Playwright
  • component.update(): re-renderiza con nuevas props
  • expect(component).toHaveScreenshot(): regresión visual
→ See also: La Pirámide de Tests Explicada para Ingenieros QA | Assertions en Playwright: La Guía Completa | Pruebas de Regresión Visual con Playwright: toHaveScreenshot sin Applitools