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
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-reactIsso 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.tsTestando 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.tsOperações principais:
mount(— renderiza um componente em um navegador real) component.getByTestId(...)— locators padrão do Playwrightcomponent.click()— ações padrão do Playwrightcomponent.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