Pular para o conteúdo principal

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

ItemEscolhaVersão mínima
RunnerPlaywright@playwright/test ^1.49
LinguagemTypeScript estritomatching tsconfig.json do monorepo
BrowsersChromium (default), Firefox e WebKit em CI nightly
Reporterhtml 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 em e2e-smoke/ para evitar duplicação de configuração e dependências pesadas.
  • O package.json do e2e-smoke/ é independente — não herda do workspace de nenhum app.
  • tsconfig.json referencia tipos compartilhados de frontend/shared/ quando necessário, mas nunca importa código dos apps (testes devem ser caixa-preta).

4. Nomenclatura

ArtefatoConvençãoExemplo
Arquivo de testekebab-case.spec.tshandoff-sso.spec.ts
Page Objectkebab-case.page.ts exportando classe PascalCasePagecatalogo.page.tsCatalogoPage
Fixturekebab-case.fixture.tsauth.fixture.ts
Tag de teste@smoke, @regression, @flaky (no nome do test)test('login flow @smoke', …)
data-testidkebab-case semântico, sem prefixo de frameworkdata-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:

  1. getByTestId('btn-checkout') — sempre que possível. Adicione data-testid no componente Angular se não existir.
  2. getByRole('button', { name: 'Confirmar' }) — segundo melhor (acessibilidade-friendly).
  3. getByLabel('CPF') — para inputs com label associado.
  4. getByText('Conectando ao marketplace') — apenas para verificar conteúdo, nunca para clicar.
  5. CSS class / nth-childPROIBIDO. Tailwind classes mudam, índices quebram.
  6. XPathPROIBIDO. 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 Locator publicamente 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

AmbienteBASE_URL_CLUBEBASE_URL_SHOPUso
localhttp://localhost:4200http://localhost:4204Dev local com ng serve em ambos
staging(futuro)(futuro)Pré-prod, ainda não existe
prodhttps://clube.elofiel.com.brhttps://shop.elofiel.com.brSmoke 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=true que tem cleanup automático em job separado.
  • .env nunca 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 ou waitForResponse/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.beforeEach para reset.
  • Loops while esperando 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

TriggerTagWorkflow
PR para main que mexe em frontend@smokee2e-smoke.yml (novo)
Merge para main@smoke em prode2e-smoke-prod.yml (novo)
Nightly 03:00 BRT@smoke + @regressione2e-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:

  1. Justifique E2E. Pode ser unit ou integration? Se sim, faça isso em vez disso.
  2. Identifique a Page (cria novo POM se a rota não existir; reusa se já existe).
  3. Adicione data-testid nos elementos que o teste vai tocar — no template Angular, na mesma PR.
  4. Login via fixture (usuarioLogadoToken), nunca via UI.
  5. Use auto-wait dos locators. Zero waitForTimeout.
  6. Tag o teste (@smoke ou @regression).
  7. Rode local 3x seguidas sem falhar antes de commitar (npx playwright test --repeat-each=3).
  8. Atualize e2e-smoke/README.md se introduzir nova fixture, env var ou padrão.
  9. 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 testeFixture 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.brprocess.env.BASE_URL_SHOP
Testar lógica de cálculo de cashback no E2ECobrir em unit do backend
Asserção em mensagem traduzidadata-testid + estado semântico

13. Referências

  • Playwright docs
  • Playwright best practices
  • docs/elofiel-marketplace-b2b2c.md seção 12 — fluxo de handoff SSO
  • docs/roteiro-teste-marketplace.md — roteiro manual (será automatizado pelo E2E)
  • frontend/elofiel-clube/src/app/features/dashboard/dashboard.component.ts — botão de handoff
  • frontend/elofiel-marketplace/src/app/features/auth/handoff/handoff.component.ts — consumo de handoff