---
name: e2e-zoppy
description: Regras e padrões para escrever testes E2E Playwright no projeto Zoppy FE. Use ao criar, revisar ou corrigir testes E2E. Foco em independência entre testes, resistência a flaky tests e padrões do projeto.
---
# E2E Zoppy — Regras para Playwright
Padrões obrigatórios para testes E2E no zoppy-FE. Foco em **independência**, **resistência a flaky** e **manutenção**.
## Regra #1 — Cada teste é independente
**Cada `test()` deve funcionar sozinho, sem depender de outro teste ter rodado antes.**
```typescript
// ERRADO — testes acoplados em serial
test.describe.serial('Fluxo', () => {
test('navegar para a página', async () => {
/* navega */
});
test('verificar conteúdo', async () => {
/* assume que já navegou */
});
});
// CORRETO — cada teste navega por conta própria
test.describe('Fluxo', () => {
test('deve exibir conteúdo na página', async ({ page }) => {
await page.goto('/dashboard/feature');
await expect(page.getByTestId('content')).toBeVisible();
});
test('deve navegar entre abas', async ({ page }) => {
await page.goto('/dashboard/feature');
await page.getByText('Aba 2').click();
await expect(page).toHaveURL(/\/aba-2/);
});
});
```
**Exceção:** Use `test.describe.serial` apenas quando testar um fluxo CRUD completo onde cada passo depende do anterior (criar → editar → deletar) e limpar estado entre testes não é viável.
## Regra #2 — Nunca usar `networkidle`
`waitForLoadState('networkidle')` causa flaky tests porque qualquer polling, websocket ou request contínuo impede o estado de ser atingido.
```typescript
// ERRADO — trava se há polling
await page.waitForLoadState('networkidle');
// CORRETO — aguardar o elemento que indica que a página carregou
await page.goto('/dashboard/feature');
await page.getByTestId('feature-content').waitFor({ state: 'visible', timeout: 15000 });
```
## Regra #3 — Seletores por `data-testid`
Usar `data-testid` como seletor primário. Nunca depender de classes CSS, tags HTML ou texto que pode mudar.
```typescript
// CORRETO
await page.getByTestId('provider-list');
await page.getByTestId('sak-card').getByRole('button', { name: 'Configurar' });
await page.getByRole('button', { name: /salvar/i });
// ERRADO — frágil
await page.getByText('Integrações'); // texto pode mudar
await page.locator('.flex.gap-6 > div:nth-child(2) button');
await page.locator('ui-button[type="primary"]');
```
### `getByText` — nunca usar como seletor de interação
`getByText` é frágil: textos mudam com i18n ou redesign. **Sempre usar `data-testid`.**
### Angular `[routerLink]` em `
` — precisa de `data-testid`
Elementos com `[routerLink]` em `
` **não geram atributo `href`**, portanto não funcionam com `getByRole('link')` nem com `[href]` selector. Adicionar `[attr.data-testid]` no template:
```html
```
```typescript
// teste — funciona
const item: Locator = page.getByTestId('menu-item-integrations');
```
### `getByRole('link')` — cuidado com nome acessível
`getByRole('link', { name: 'Texto' })` pode falhar quando o `
` contém ícones (`ps-icon`, `mat-icon`) — o nome acessível inclui o texto do ícone concatenado ao label. Preferir `getByTestId`:
```typescript
// ERRADO — falha se contém junto ao texto
const item = page.getByRole('link', { name: 'Integrações' });
// CORRETO — adicionar data-testid="main-menu-configurations" no template
const item: Locator = page.getByTestId('main-menu-configurations');
```
## Regra #4 — Aguardar antes de agir
Sempre aguardar o elemento estar visível antes de interagir. Usar auto-waiting do Playwright (`.click()`, `.fill()` já aguardam), mas para verificações de estado, usar `waitFor()`.
```typescript
// CORRETO — aguarda o modal antes de verificar conteúdo
await page.getByTestId('sak-card').getByRole('button', { name: 'Configurar' }).click();
await page.locator('.modal-content').waitFor({ state: 'visible', timeout: 5000 });
await expect(page.locator('.modal-content')).toContainText('SAK');
// ERRADO — verifica imediatamente sem aguardar
await page.getByRole('button', { name: 'Configurar' }).click();
await expect(page.locator('.modal-content')).toContainText('SAK'); // pode falhar se modal demora
```
## Regra #5 — Timeouts explícitos e curtos
Não depender do timeout global (120s). Usar timeouts explícitos e curtos para falhar rápido.
```typescript
// CORRETO — timeout explícito
await page.getByTestId('content').waitFor({ state: 'visible', timeout: 10000 });
// ERRADO — depende do timeout global de 120s
await page.getByTestId('content').waitFor({ state: 'visible' });
```
## Regra #6 — Capturar erros de runtime
Testes que validam que uma feature funciona sem erros devem capturar `pageerror`.
```typescript
test('should open modal without runtime errors', async ({ page }) => {
const pageErrors: string[] = [];
page.on('pageerror', error => pageErrors.push(error.message));
await page.goto('/dashboard/feature');
await page.getByRole('button', { name: 'Abrir' }).click();
await page.locator('.modal-content').waitFor({ state: 'visible', timeout: 5000 });
expect(pageErrors).toEqual([]);
});
```
## Regra #7 — Estrutura do projeto E2E
```
e2e/
└── /
├── pages/
│ └── .page.ts # Page Object — locators e ações
├── selectors/
│ └── .selectors.ts # data-testid constants
└── tests/
└── -.spec.ts # Testes agrupados por fluxo
```
## Regra #8 — Page Object sem estado
Page Objects encapsulam **locators e ações**, não estado. Nunca guardar dados de teste no Page Object.
```typescript
// CORRETO
export class IntegrationPage {
constructor(private readonly page: Page) {}
async goToIntegrations(tab: string): Promise {
await this.page.goto(`/dashboard/configurations-v2/new-integration/${tab}`);
await this.page.getByTestId('provider-list').waitFor({ state: 'visible', timeout: 15000 });
}
}
// ERRADO — guarda estado
export class IntegrationPage {
currentTab: string = 'erp'; // NÃO
}
```
## Regra #9 — Contexto isolado por teste
Quando os testes são independentes, usar fixtures do Playwright que criam contexto isolado automaticamente. Não compartilhar `sharedPage` entre testes independentes.
```typescript
// CORRETO — cada teste tem sua própria page
test('deve exibir providers', async ({ page }) => {
await page.goto('/dashboard/configurations-v2/new-integration/erp');
// ...
});
// ACEITÁVEL — shared context APENAS para serial
test.describe.serial('CRUD completo', () => {
let sharedPage: Page;
test.beforeAll(async ({ browser }) => {
const context = await browser.newContext({ storageState: 'e2e/fixtures/auth/.auth.json' });
sharedPage = await context.newPage();
});
});
```
## Regra #10 — Testes devem ser rápidos
- Cada teste deve completar em **< 30s** (exceto fluxos CRUD completos)
- Se um teste demora mais, provavelmente está esperando algo desnecessário
- Use `timeout` curto para falhar rápido e identificar o problema
## Padrão de autenticação do projeto
```typescript
// Todos os testes E2E dependem do setup de autenticação
// O storageState é gerado pelo auth.setup.ts e reutilizado
// Configurado no playwright.config.ts via project dependencies
```
## Regra #11 — Tipos explícitos em variáveis
Todas as variáveis declaradas em testes devem ter tipo explícito após o nome. Nunca depender de inferência.
```typescript
// CORRETO
const item: Locator = page.getByTestId('menu-item');
const response: Response | null = await page.goto('/dashboard');
const errors: string[] = [];
// ERRADO — lint falha
const item = page.getByTestId('menu-item');
const response = await page.goto('/dashboard');
```
Importar tipos do `@playwright/test` separadamente das fixtures do projeto:
```typescript
import { test, expect } from '../../shared/fixtures';
import type { Locator, Response } from '@playwright/test';
```
---
## Checklist ao criar testes E2E
- [ ] Cada teste navega para a página por conta própria (`page.goto(...)`)
- [ ] Nenhum `waitForLoadState('networkidle')`
- [ ] Seletores usam `data-testid` — nunca `getByText` para interação
- [ ] Elementos Angular com `[routerLink]` em `