Testing E2E — Padrões para os frontends EloFiel
Documento normativo. Toda PR que adiciona ou modifica testes E2E nos frontends do EloFiel deve aderir a estes padrões. Agentes de IA leem este arquivo antes de gerar qualquer teste.
1. Escopo e quando usar E2E
E2E é caro (lento, frágil, pesado em CI). Use apenas para:
- Smoke tests: fluxos críticos que, se quebrarem, impedem o usuário de usar o produto. Exemplos: login, checkout, handoff SSO clube↔marketplace.
- Fluxos cross-app: qualquer coisa que cruze 2+ frontends ou 2+ APIs. Unit tests não cobrem isso.
- Regressões pós-incidente: bug que escapou prod e tem alta chance de voltar.
NÃO use E2E para:
- Lógica de componente isolada → unit test (Jasmine/Jest no próprio app).
- Validação de form → integration test do componente.
- Cobertura ampla de regras de negócio → backend tem testes próprios em
EloFiel.Tests.
Regra prática: se você consegue cobrir com unit, cubra com unit. E2E é o último recurso.
2. Stack obrigatória
| Item | Escolha | Versão mínima |
|---|---|---|
| Runner | Playwright | @playwright/test ^1.49 |
| Linguagem | TypeScript estrito | matching tsconfig.json do monorepo |
| Browsers | Chromium (default), Firefox e WebKit em CI nightly | — |
| Reporter | html local + github em CI | — |
Por que Playwright e não Cypress:
- Multi-contexto nativo (essencial para validar handoff entre 2 origens diferentes).
- Auto-wait robusto, sem
cy.wait(ms). - Trace viewer + screenshots + vídeo em falha — debug muito superior.
- Suporte a múltiplos browsers num único runner.
Cypress, Protractor, Selenium, WebdriverIO são vetados.
3. Localização e estrutura
Todos os testes E2E vivem em um único projeto isolado na raiz do monorepo frontend:
frontend/
├── elofiel-clube/
├── elofiel-marketplace/
├── elofiel-app/
├── elofiel-backoffice/
└── e2e-smoke/ ← projeto Playwright
├── package.json
├── playwright.config.ts
├── tsconfig.json
├── .env.example ← BASE_URL_CLUBE, BASE_URL_SHOP, TEST_USER_CPF…
├── fixtures/
│ ├── auth.fixture.ts ← login programático via API
│ └── handoff.fixture.ts ← gera handoff token + abre marketplace logado
├── pages/ ← Page Object Model
│ ├── clube/
│ │ ├── login.page.ts
│ │ └── dashboard.page.ts
│ └── marketplace/
│ ├── catalogo.page.ts
│ ├── produto-detalhe.page.ts
│ ├── checkout.page.ts
│ └── pedidos.page.ts
├── tests/
│ ├── smoke/ ← @smoke — roda em todo PR
│ │ ├── handoff-sso.spec.ts
│ │ └── checkout-pontos.spec.ts
│ └── regression/ ← @regression — roda nightly
└── README.md ← como rodar local
Regras:
- NÃO criar pasta
e2e/dentro de cada app individual. Centraliza-se eme2e-smoke/para evitar duplicação de configuração e dependências pesadas. - O
package.jsondoe2e-smoke/é independente — não herda do workspace de nenhum app. tsconfig.jsonreferencia tipos compartilhados defrontend/shared/quando necessário, mas nunca importa código dos apps (testes devem ser caixa-preta).
4. Nomenclatura
| Artefato | Convenção | Exemplo |
|---|---|---|
| Arquivo de teste | kebab-case.spec.ts | handoff-sso.spec.ts |
| Page Object | kebab-case.page.ts exportando classe PascalCasePage | catalogo.page.ts → CatalogoPage |
| Fixture | kebab-case.fixture.ts | auth.fixture.ts |
| Tag de teste | @smoke, @regression, @flaky (no nome do test) | test('login flow @smoke', …) |
data-testid | kebab-case semântico, sem prefixo de framework | data-testid="btn-ir-marketplace" |
5. Seletores — ordem de preferência
Use a primeira opção possível desta lista. Subir na lista é falha de teste, não otimização:
getByTestId('btn-checkout')— sempre que possível. Adicionedata-testidno componente Angular se não existir.getByRole('button', { name: 'Confirmar' })— segundo melhor (acessibilidade-friendly).getByLabel('CPF')— para inputs com label associado.getByText('Conectando ao marketplace')— apenas para verificar conteúdo, nunca para clicar.- CSS class / nth-child — PROIBIDO. Tailwind classes mudam, índices quebram.
- XPath — PROIBIDO. Sem exceção.
Adicionar data-testid é parte do trabalho do teste, não tarefa de outra PR. Se o teste precisa, a mesma PR adiciona o atributo no template Angular.
6. Page Object Model (POM)
Toda interação com a UI passa por uma classe Page. Testes nunca chamam page.locator(...) direto — só consomem métodos do POM.
Estrutura mínima de uma Page
// pages/clube/dashboard.page.ts
import { Page, Locator, expect } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly btnIrMarketplace: Locator;
readonly saldoPontos: Locator;
constructor(page: Page) {
this.page = page;
this.btnIrMarketplace = page.getByTestId('btn-ir-marketplace');
this.saldoPontos = page.getByTestId('saldo-pontos');
}
async goto() {
await this.page.goto('/dashboard');
await expect(this.btnIrMarketplace).toBeVisible();
}
async irParaMarketplace(): Promise<string> {
// Captura a URL pós-redirect (deve ser shop.elofiel.com.br/auth/handoff?token=...)
const [response] = await Promise.all([
this.page.waitForURL(/\/auth\/handoff\?token=/),
this.btnIrMarketplace.click(),
]);
return this.page.url();
}
}
Regras de POM
- Uma Page por rota navegável. Não criar Page para componentes internos.
- Métodos imperativos (
fazerLogin,adicionarAoCarrinho), não getters. - Nunca expor
Locatorpublicamente se ele não for usado para asserção. Se virou público, vire método. - Asserções vivem no teste, não na Page. Exceção: pré-condições óbvias (
goto()espera a página renderizar).
7. Fixtures
Use fixtures do Playwright (test.extend) para setup reutilizável. Não coloque setup em beforeEach global.
Fixture canônica: usuarioLogado
// fixtures/auth.fixture.ts
import { test as base, request as pwRequest } from '@playwright/test';
type AuthFixtures = {
usuarioLogadoToken: string;
};
export const test = base.extend<AuthFixtures>({
usuarioLogadoToken: async ({}, use) => {
// Login PROGRAMÁTICO via API — nunca pelo formulário (lento e flaky)
const ctx = await pwRequest.newContext({ baseURL: process.env.BASE_URL_API_CLUBE });
const res = await ctx.post('/api/v1/clube/auth/login', {
data: { cpf: process.env.TEST_USER_CPF, pin: process.env.TEST_USER_PIN },
});
const { token } = (await res.json()).dados;
await use(token);
await ctx.dispose();
},
});
export { expect } from '@playwright/test';
Regra de ouro: autenticação sempre via API, nunca via UI. Login pelo formulário é só testado uma vez no smoke login.spec.ts.
8. Ambientes
O config do Playwright lê de process.env. Nunca hardcode URLs.
// playwright.config.ts (trecho)
export default defineConfig({
use: {
baseURL: process.env.BASE_URL_CLUBE,
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
});
Targets suportados
| Ambiente | BASE_URL_CLUBE | BASE_URL_SHOP | Uso |
|---|---|---|---|
local | http://localhost:4200 | http://localhost:4204 | Dev local com ng serve em ambos |
staging | (futuro) | (futuro) | Pré-prod, ainda não existe |
prod | https://clube.elofiel.com.br | https://shop.elofiel.com.br | Smoke pós-deploy. Usuário de teste dedicado obrigatório. |
Regras críticas:
- NUNCA rodar testes destrutivos em prod (criar pedido real, debitar saldo real). Em prod só smoke não-mutativos: navegar, ler, validar layout.
- Para mutativos em prod (ex: criar pedido), use conta de teste com flag
is_test_user=trueque tem cleanup automático em job separado. .envnunca commitado. Só.env.example.
9. Determinismo — regras invioláveis
Testes flaky são pior que não ter teste. Toda PR de teste E2E que introduzir flakiness é bloqueada.
Proibido
await page.waitForTimeout(N)— usa auto-wait dos locators ouwaitForResponse/waitForURL.setTimeout,Promise.resolve(setTimeout…)— mesma coisa.- Asserção em texto traduzido livre (use
data-testid). - Depender de ordem de testes (cada teste deve ser executável isoladamente).
- Compartilhar estado entre testes (cookies, localStorage, banco) — use
test.beforeEachpara reset. - Loops
whileesperando condição.
Obrigatório
- Toda navegação que dispara request:
Promise.all([page.waitForResponse(url), action()]). - Teste isolado: cada
test()cria seu próprio contexto se mexer com auth. - Re-run automático em CI: máximo 1 retry (
retries: 1). Se precisar de mais, o teste é flaky e deve ser corrigido ou taggeado@flaky(não roda em PR, só nightly).
10. CI/CD
Quando rodar
| Trigger | Tag | Workflow |
|---|---|---|
PR para main que mexe em frontend | @smoke | e2e-smoke.yml (novo) |
Merge para main | @smoke em prod | e2e-smoke-prod.yml (novo) |
| Nightly 03:00 BRT | @smoke + @regression | e2e-nightly.yml (futuro) |
Estrutura mínima do workflow
# .github/workflows/e2e-smoke.yml
name: E2E Smoke
on:
pull_request:
paths: ['frontend/**']
jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm ci
working-directory: frontend/e2e-smoke
- run: npx playwright install --with-deps chromium
working-directory: frontend/e2e-smoke
- run: npm run test:smoke
working-directory: frontend/e2e-smoke
env:
BASE_URL_CLUBE: ${{ secrets.E2E_BASE_URL_CLUBE_STAGING }}
BASE_URL_SHOP: ${{ secrets.E2E_BASE_URL_SHOP_STAGING }}
TEST_USER_CPF: ${{ secrets.E2E_TEST_USER_CPF }}
TEST_USER_PIN: ${{ secrets.E2E_TEST_USER_PIN }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: frontend/e2e-smoke/playwright-report/
Limites:
- Smoke completo deve rodar em < 5 minutos. Se passar disso, dividir em shards.
- Nightly pode ir até 30 minutos.
11. Como adicionar um novo teste — checklist
Quando você (humano ou agente) for criar um novo teste E2E:
- Justifique E2E. Pode ser unit ou integration? Se sim, faça isso em vez disso.
- Identifique a Page (cria novo POM se a rota não existir; reusa se já existe).
- Adicione
data-testidnos elementos que o teste vai tocar — no template Angular, na mesma PR. - Login via fixture (
usuarioLogadoToken), nunca via UI. - Use auto-wait dos locators. Zero
waitForTimeout. - Tag o teste (
@smokeou@regression). - Rode local 3x seguidas sem falhar antes de commitar (
npx playwright test --repeat-each=3). - Atualize
e2e-smoke/README.mdse introduzir nova fixture, env var ou padrão. - PR pequena — 1 cenário por PR. Não fazer "10 testes de uma vez".
12. Anti-padrões comuns (não faça)
| ❌ Errado | ✅ Certo |
|---|---|
await page.waitForTimeout(2000) | await page.waitForResponse(/api\/v1\/auth/) |
await page.locator('.btn-primary').click() | await page.getByTestId('btn-confirmar').click() |
| Login pelo form em todo teste | Fixture com login programático |
expect(true).toBe(true) "smoke" | Asserção real do estado pós-ação |
Dependência entre testes (test1 cria, test2 usa) | Cada teste é independente |
Hardcode https://shop.elofiel.com.br | process.env.BASE_URL_SHOP |
| Testar lógica de cálculo de cashback no E2E | Cobrir em unit do backend |
| Asserção em mensagem traduzida | data-testid + estado semântico |
13. Referências
- Playwright docs
- Playwright best practices
docs/elofiel-marketplace-b2b2c.mdseção 12 — fluxo de handoff SSOdocs/roteiro-teste-marketplace.md— roteiro manual (será automatizado pelo E2E)frontend/elofiel-clube/src/app/features/dashboard/dashboard.component.ts— botão de handofffrontend/elofiel-marketplace/src/app/features/auth/handoff/handoff.component.ts— consumo de handoff