O pacote @playwright/experimental-ct-react monta componentes React individuais em um navegador real usando uma fixture mount(). Com isso, você consegue testar o comportamento da tecla Escape num dropdown ou o estado de erro de um formulário sem precisar rodar toda a stack do aplicativo. Usa os mesmos locators e assertions dos seus testes Playwright regulares, configurado por um playwright-ct.config.ts separado.

Quando testes de componente fazem sentido

Use testes de componente para:
  • Componentes de UI com lógica de interação complexa (dropdowns, modais, date pickers)
  • Componentes difíceis de alcançar via E2E (estados de edge case, estados de erro)
  • Componentes de design system que precisam de verificação de consistência visual
  • Testar event handling e props sem o app completo
Não substitua E2E por testes de componente:

Testes de componente não cobrem roteamento, autenticação, integração com API ou interação entre componentes. Você precisa dos dois.

Setup

npm init playwright@latest -- --ct
# Ou adicionar a projeto existente:
npm install --save-dev @playwright/experimental-ct-react

Isso cria um playwright-ct.config.ts separado do seu config regular:

// 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,  // Porta para o servidor de testes de componente
    viewport: { width: 1280, height: 720 },
  },
  
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
  ],
});

Seu primeiro teste 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 com o label correto', async ({ mount }) => {
  const component = await mount(<Button label="Clique aqui" />);
  
  await expect(component).toContainText('Clique aqui');
});

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

test('botão desabilitado não pode ser clicado', async ({ mount }) => {
  const component = await mount(<Button label="Desabilitado" disabled />);
  
  await expect(component.getByTestId('button')).toBeDisabled();
});

test('chama onClick quando clicado', async ({ mount }) => {
  let clicked = false;
  
  const component = await mount(
    <Button 
      label="Clique aqui" 
      onClick={() => { clicked = true; }} 
    />
  );
  
  await component.click();
  
  expect(clicked).toBe(true);
});

Execute os testes de componente:

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

Testando um componente de formulário

// 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="Senha"
      />
      {error && <p data-testid="error">{error}</p>}
      <button type="submit" data-testid="submit">Entrar</button>
    </form>
  );
}

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

test('envia com credenciais corretas', async ({ mount }) => {
  let submittedEmail = '';
  let submittedPassword = '';
  
  const component = await mount(
    <LoginForm
      onSubmit={(email, password) => {
        submittedEmail = email;
        submittedPassword = password;
      }}
    />
  );
  
  await component.getByTestId('email').fill('user@test.com');
  await component.getByTestId('password').fill('SenhaValida1');
  await component.getByTestId('submit').click();
  
  expect(submittedEmail).toBe('user@test.com');
  expect(submittedPassword).toBe('SenhaValida1');
});

test('exibe mensagem de erro quando fornecida', async ({ mount }) => {
  const component = await mount(
    <LoginForm
      onSubmit={() => {}}
      error="Credenciais inválidas"
    />
  );
  
  await expect(component.getByTestId('error')).toHaveText('Credenciais inválidas');
});

test('não exibe erro quando error é undefined', async ({ mount }) => {
  const component = await mount(
    <LoginForm onSubmit={() => {}} />
  );
  
  await expect(component.getByTestId('error')).not.toBeVisible();
});

Testando um componente dropdown

Componentes complexos com estado são onde os testes de componente brilham:

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

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

test('dropdown exibe opções ao clicar', async ({ mount }) => {
  const component = await mount(
    <Dropdown options={options} placeholder="Selecione o navegador" />
  );
  
  // Inicialmente fechado
  await expect(component.getByTestId('dropdown-list')).not.toBeVisible();
  
  // Clique para abrir
  await component.getByTestId('dropdown-trigger').click();
  
  // Agora visível com todas as opções
  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('fecha após selecionar uma opção', async ({ mount }) => {
  let selected = '';
  
  const component = await mount(
    <Dropdown
      options={options}
      placeholder="Selecione o navegador"
      onSelect={(value) => { selected = value; }}
    />
  );
  
  await component.getByTestId('dropdown-trigger').click();
  await component.getByText('Firefox').click();
  
  expect(selected).toBe('firefox');
  await expect(component.getByTestId('dropdown-list')).not.toBeVisible();
  await expect(component.getByTestId('dropdown-trigger')).toHaveText('Firefox');
});

test('fecha com a tecla Escape', async ({ mount }) => {
  const component = await mount(
    <Dropdown options={options} placeholder="Selecione" />
  );
  
  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();
});

Atualizando componentes no meio do teste

Você pode re-renderizar um componente com props diferentes:

test('mensagem de erro atualiza quando props mudam', async ({ mount }) => {
  const component = await mount(
    <LoginForm onSubmit={() => {}} />
  );
  
  // Inicialmente sem erro
  await expect(component.getByTestId('error')).not.toBeVisible();
  
  // Atualizar props para adicionar erro
  await component.update(
    <LoginForm onSubmit={() => {}} error="Sessão expirada" />
  );
  
  await expect(component.getByTestId('error')).toHaveText('Sessão expirada');
  
  // Limpar erro
  await component.update(
    <LoginForm onSubmit={() => {}} />
  );
  
  await expect(component.getByTestId('error')).not.toBeVisible();
});

Hooks em testes de componente

Os testes de componente do Playwright suportam os mesmos hooks dos testes regulares:

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

let clickCount = 0;

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

test('conta cliques corretamente', async ({ mount }) => {
  const component = await mount(
    <Button label="Clique" onClick={() => { clickCount++; }} />
  );
  
  await component.click();
  await component.click();
  await component.click();
  
  expect(clickCount).toBe(3);
});

Testes visuais com componentes

Os testes de componente funcionam bem com regressão visual:

test('variantes do botão aparecem corretamente', 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`);
  }
});

Isso é mais confiável do que screenshots de página inteira: menos peças móveis, mais fácil isolar o que mudou.

Vue e Svelte

Os mesmos conceitos se aplicam ao Vue e Svelte:

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

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

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

Componente vs E2E: quando usar cada um

| Cenário | Teste de Componente | Teste E2E |

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

| Handler de clique em botão | ✅ | ❌ |

| Dropdown abre/fecha | ✅ | ❌ |

| Mensagens de validação de formulário | ✅ | ❌ |

| Fluxo de login end-to-end | ❌ | ✅ |

| Navegação entre páginas | ❌ | ✅ |

| Integração com API | ❌ | ✅ |

| Regressão visual de componente | ✅ | Às vezes |

Resumo

# Setup
npm install --save-dev @playwright/experimental-ct-react

# Config: playwright-ct.config.ts
# Testes: ComponentName.ct.spec.tsx

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

Operações principais:

  • mount() — renderiza um componente em um navegador real
  • component.getByTestId(...) — locators padrão do Playwright
  • component.click() — ações padrão do Playwright
  • component.update() — re-renderiza com novas props
  • expect(component).toHaveScreenshot() — regressão visual

Os testes de componente preenchem a lacuna entre testes unitários (muito isolados) e testes E2E (muito lentos). Para aplicações com muita UI e componentes complexos, vale adicionar à sua estratégia de testes.

→ Veja também: A Pirâmide de Testes Explicada para Engenheiros QA | Assertions no Playwright: O Guia Completo | Testes de Regressão Visual com Playwright: toHaveScreenshot sem Applitools