multiple verifications
This commit is contained in:
parent
58451f805e
commit
2093f12d0a
@ -22,7 +22,14 @@ AUTH_SECRET="dev-secret-do-not-use-in-production-please-change-me"
|
||||
# must consciously opt in by editing their .env. See README "Auth" section.
|
||||
AUTH_DEV_AUTOLOGIN="false"
|
||||
|
||||
# Base URL of the operator-pwa app — used by Auth.js for callback URLs.
|
||||
# Base URL Auth.js uses to build callback/redirect URLs. It MUST match the host
|
||||
# of the app being served. This shared .env can hold only ONE value, set here to
|
||||
# the operator-pwa (:3000). The admin-web (:3001) therefore needs its OWN
|
||||
# AUTH_URL whenever autologin is OFF (real-login dev, or production) — otherwise
|
||||
# Auth.js redirects admin users to :3000 and the admin login breaks.
|
||||
# - Local with autologin ON: this value is harmless (middleware never redirects).
|
||||
# - E2E real-login: e2e/playwright.auth.config.ts passes AUTH_URL=:3001 to admin.
|
||||
# - Production: give EACH app its own AUTH_URL (per-app env), not this shared file.
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
AUTH_URL="http://localhost:3000"
|
||||
|
||||
|
||||
70
README.md
70
README.md
@ -147,14 +147,65 @@ mc mirror local/fieldops ./backup-photos
|
||||
```sh
|
||||
# One-time browser install (downloads ~170 MB of Chromium)
|
||||
pnpm --filter @repo/e2e install-browsers
|
||||
|
||||
# Run the happy-path test (starts both dev servers automatically)
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
The Playwright config force-sets `AUTH_DEV_AUTOLOGIN=true` for the child
|
||||
servers, so the test does not depend on the developer's `.env`.
|
||||
Expected: **1 passed** in ~30 s.
|
||||
### `pnpm test:e2e` — happy-path + shift report (autologin ON)
|
||||
|
||||
Starts both dev servers with `AUTH_DEV_AUTOLOGIN=true`. Safe to run at any
|
||||
time — reuses servers already running on 3000/3001 if available.
|
||||
|
||||
Expected: **3 passed** (~45 s):
|
||||
- MAI CALL happy path: create → claim → resolve
|
||||
- Shift report: renders with seed data and reacts to window selection
|
||||
- Shift report: accessible from the maintenance queue link
|
||||
|
||||
### `pnpm test:e2e:auth` — real login (autologin OFF)
|
||||
|
||||
Tests that the login UI actually works — operator PIN keypad and admin
|
||||
email/password — without the dev back door.
|
||||
|
||||
**Precondition: no servers running on ports 3000 or 3001.** This command
|
||||
starts its own servers with `AUTH_DEV_AUTOLOGIN=false`. If ports are busy:
|
||||
|
||||
```sh
|
||||
# Windows
|
||||
Get-Process node | Stop-Process -Force
|
||||
```
|
||||
|
||||
Also requires `pnpm db:seed` to have been run.
|
||||
|
||||
Expected: **4 passed** (~60 s):
|
||||
- Operator: wrong PIN shows error, correct PIN enters the app
|
||||
- Operator: unauthenticated root redirects to picker
|
||||
- Admin: protected route redirects to /login without session
|
||||
- Admin: wrong password shows error, correct password enters the queue
|
||||
|
||||
---
|
||||
|
||||
### Manual smoke checklist (5 min — covers print + offline)
|
||||
|
||||
Automated tests don't cover the print PDF or offline sync. Run this when
|
||||
you want end-to-end confidence before a demo:
|
||||
|
||||
```
|
||||
1. docker compose up -d && pnpm db:seed
|
||||
|
||||
2. Operator (real login):
|
||||
pnpm --filter @repo/operator-pwa dev → http://localhost:3000/select-operator
|
||||
Try PIN 9999 → see error. Then op1 → PIN 1111 → enters app.
|
||||
|
||||
3. Admin (real login):
|
||||
pnpm --filter @repo/admin-web dev → http://localhost:3001/login
|
||||
admin@demo.local / admin1234 → maintenance queue.
|
||||
|
||||
4. Shift report:
|
||||
Click "Relatório de turno" in header → click Manhã/Tarde/Noite/Hoje
|
||||
→ numbers update each time.
|
||||
Click "Imprimir" → confirm PDF is clean (no buttons/selector/nav).
|
||||
|
||||
5. Offline (requires production build of operator-pwa — see Troubleshooting):
|
||||
DevTools → Network → Offline → create 2 requests → Online → they sync.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -248,6 +299,13 @@ by changing the endpoint env var.
|
||||
|
||||
**`Tenant not found`** — the seed was wiped. Run `pnpm db:seed`.
|
||||
|
||||
**Admin login redirects to `:3000` / the operator picker** — the shared `.env`
|
||||
sets `AUTH_URL=http://localhost:3000`, which is correct for the operator-pwa but
|
||||
wrong for the admin-web (:3001). With `AUTH_DEV_AUTOLOGIN=true` it never bites
|
||||
(the middleware doesn't redirect). With autologin OFF, give the admin-web its own
|
||||
`AUTH_URL=http://localhost:3001` (the E2E auth config does this). In production,
|
||||
each app must have its own `AUTH_URL`.
|
||||
|
||||
**`DATABASE_URL not found`** — `.env` is missing or Docker Postgres is not
|
||||
running. Run `docker compose up -d` then retry.
|
||||
|
||||
|
||||
168
docs/plans/e2e-login-and-report.md
Normal file
168
docs/plans/e2e-login-and-report.md
Normal file
@ -0,0 +1,168 @@
|
||||
# Plano — E2E de verificação: login real + relatório de turno
|
||||
|
||||
> Autor: Opus 4.8 (sessão de design, 2026-05-30). Destinado a implementação pelo Sonnet.
|
||||
> Pré-requisitos: MAI CALL v0.1 + Auth v0.2 + v0.3 (relatório), todos implementados e verificados ao nível de lógica/build. Estado do código verificado contra o repo.
|
||||
> **Motivo:** Pedro escolheu "travar features e verificar". Três camadas de interação nunca foram exercitadas por um browser: (a) a UI do relatório, (b) o **login real** (o E2E atual usa `AUTH_DEV_AUTOLOGIN=true` e **contorna** o login), (c) offline. Este plano fecha (a) e (b) com Playwright. (c) fica para um smoke manual.
|
||||
|
||||
## Objetivo numa frase
|
||||
|
||||
Acrescentar testes E2E que provem que **o relatório de turno renderiza e reage** e que o **login real funciona no browser** (operador PIN + admin password), sem depender do back door de autologin.
|
||||
|
||||
## Decisões fixadas (não revisitar sem motivo forte)
|
||||
|
||||
1. **Dois configs Playwright.** O atual ([`e2e/playwright.config.ts`](../../e2e/playwright.config.ts)) arranca ambos os servidores com `AUTH_DEV_AUTOLOGIN: 'true'` — mantém-se para os testes que não precisam de login (happy-path + **relatório**). Um **segundo config** arranca os servidores com o autologin **desligado** para os testes de **login real**.
|
||||
2. **Porquê dois servidores e não um:** o autologin é decidido server-side (`resolveUser()` + `middleware.ts`). Com o flag ligado no servidor, apagar cookies no browser não chega — o middleware volta a deixar entrar. A única forma de testar o login real é um servidor com o flag desligado.
|
||||
3. **Como desligar o flag no servidor de teste:** passar `AUTH_DEV_AUTOLOGIN: 'false'` em `webServer[].env`. Funciona porque o script de dev usa `dotenv -e ../../.env`, e **`dotenv` não sobrepõe variáveis já presentes** no processo — o valor do Playwright ganha. (É o mesmo mecanismo pelo qual o config atual força `'true'`.)
|
||||
4. **Mesmas portas (3000/3001), `reuseExistingServer: false` no config de auth.** Consequência: **`test:e2e:auth` não pode correr com servidores já a correr nessas portas** (dá "port in use"). Documentar. Em CI corre isolado, sem problema.
|
||||
5. **Não testar o lockout no E2E.** Já está coberto pelo `auth-smoke` (11/11). Testar lockout no browser exigiria 5 tentativas e deixaria `failedAttempts`/`lockedUntil` sujos por 5 min, podendo partir outros testes. O E2E de login cobre **sucesso** + **credencial errada mostra erro** (1 tentativa), e o **sucesso reseta** `failedAttempts`.
|
||||
6. **Asserções tolerantes a dados acumulados.** O DB é partilhado e sequencial (`workers: 1`); o happy-path cria pedidos extra. O teste do relatório usa asserções **estruturais** (elementos existem, valores não-vazios) e **≥**, nunca números exatos — mesma postura do `report-smoke`.
|
||||
|
||||
## Estrutura de ficheiros (nova)
|
||||
|
||||
```
|
||||
e2e/
|
||||
playwright.config.ts (existe — autologin ON, testDir ./tests)
|
||||
playwright.auth.config.ts (NOVO — autologin OFF, testDir ./tests-auth, reuseExistingServer:false)
|
||||
tests/
|
||||
mai-call.spec.ts (existe)
|
||||
report.spec.ts (NOVO — Passo 1)
|
||||
tests-auth/
|
||||
login.spec.ts (NOVO — Passo 2)
|
||||
```
|
||||
|
||||
> O config atual tem `testDir: './tests'`, por isso os specs em `./tests-auth` **não** são apanhados por ele. Sem colisão.
|
||||
|
||||
## Selectores confirmados (lidos do código — usar exatamente)
|
||||
|
||||
**Operador (operator-pwa, `/select-operator`):**
|
||||
- Lista: `<button>` com texto = email do operador, ex. `op1@demo.local`.
|
||||
- Keypad: `<button>` com texto `'1'`, `'2'`, … (dígitos). Botão final: texto **`Entrar`** (fica `A entrar…` enquanto submete; disabled até ≥4 dígitos).
|
||||
- Erro (PIN errado): texto **`PIN incorreto ou conta bloqueada. Tente novamente.`**
|
||||
- Sucesso: `signIn(..., { redirect:false })` → `router.push('/')`. Confirmar pela navegação para a home (`/`) e por um elemento da home autenticada.
|
||||
- Creds seed: op1 = **1111**, op2 = 2222, op3 = 3333.
|
||||
|
||||
**Admin (admin-web, `/login`):**
|
||||
- `input#email`, `input#password`, `button[type=submit]` (texto `Entrar`).
|
||||
- Erro: texto **`Email ou password incorretos. Tente novamente.`**
|
||||
- Sucesso: `router.push('/maintenance')`.
|
||||
- Creds seed: **admin@demo.local** / **admin1234**.
|
||||
|
||||
**Relatório (admin-web, `/maintenance/report`):**
|
||||
- Atalhos: `<button>` com texto `Hoje`, `Manhã`, `Tarde`, `Noite`, `Personalizado`.
|
||||
- Cartões: o componente `MetricCard` renderiza o label (`Pedidos`, `Resolvidos`, `Em aberto`, `Resposta média`, `Resolução média`, `Pior resposta`) e o valor num `<p class="text-2xl">`.
|
||||
- Secção "Por posto": `<table>` com `<tbody><tr>`.
|
||||
- Label da janela ativa: parágrafo que começa por `Hoje —` / `Turno da Manhã —` / etc.
|
||||
- Estado vazio: texto `Sem pedidos nesta janela.`
|
||||
- Link de entrada: na fila (`/maintenance`), `<a>` com texto `Relatório de turno`.
|
||||
|
||||
---
|
||||
|
||||
## Passo 1 — E2E do relatório (config existente, autologin ON)
|
||||
|
||||
**Ficheiro:** `e2e/tests/report.spec.ts`. Importa `ADMIN_BASE` de `../playwright.config`.
|
||||
|
||||
**Faz:**
|
||||
1. `await page.goto(\`\${ADMIN_BASE}/maintenance/report\`)`.
|
||||
2. **Sentinela do fetch storm:** `await page.waitForLoadState('networkidle')`. > Se o fix do `useMemo` tivesse regredido (janela "Hoje" a instabilizar a query key), a página faria refetch contínuo e **nunca** atingiria `networkidle` → o teste falha aqui. Esta linha é a verificação automática do fix.
|
||||
3. Título `Relatório de turno` visível.
|
||||
4. Como o seed cria 6 pedidos "hoje", o modo **Hoje** (default) deve mostrar dados:
|
||||
- Cartão `Pedidos` com valor numérico **> 0**.
|
||||
- Cartão `Resposta média` visível (valor não-vazio — pode ser uma duração ou `—`).
|
||||
- Secção `Por posto` com **≥1** `<tr>` no `<tbody>`.
|
||||
5. **Reatividade:** clicar `Tarde` → a label da janela ativa muda para começar por `Turno da Tarde —`. (Não asserir números — a Tarde pode estar vazia consoante a hora a que o teste corre; basta provar que o seletor reage.)
|
||||
6. Clicar de volta em `Hoje` → label volta a `Hoje —`.
|
||||
7. Botão `Imprimir` existe (`getByRole('button', { name: 'Imprimir' })`). **Não** chamar `window.print()` (abre diálogo nativo, frágil em headless).
|
||||
|
||||
**AC:** `pnpm --filter @repo/e2e test` continua verde com 2 testes (happy-path + relatório). O teste do relatório passa a partir dos dados do seed.
|
||||
|
||||
> **Nota de robustez:** se `Pedidos > 0` falhar por o seed não ter corrido, a mensagem deve ser clara — adicionar um comentário no topo do spec a lembrar a pré-condição `pnpm db:seed` (como faz `mai-call.spec.ts`).
|
||||
|
||||
---
|
||||
|
||||
## Passo 2 — Segundo config + E2E de login real (autologin OFF)
|
||||
|
||||
### 2a. `e2e/playwright.auth.config.ts`
|
||||
|
||||
Clonar o config principal e alterar **só** o necessário:
|
||||
- `testDir: './tests-auth'`
|
||||
- `webServer[].env: { AUTH_DEV_AUTOLOGIN: 'false' }` (ambos os servidores)
|
||||
- `webServer[].reuseExistingServer: false` (ambos)
|
||||
- Reexportar `ADMIN_BASE` (o spec precisa).
|
||||
|
||||
Tudo o resto igual (mesmas portas, `workers: 1`, `fullyParallel: false`).
|
||||
|
||||
### 2b. `e2e/tests-auth/login.spec.ts`
|
||||
|
||||
Importa `ADMIN_BASE`. **Ordem dentro de cada teste: erro primeiro, sucesso depois** (o sucesso reseta `failedAttempts`).
|
||||
|
||||
**Teste A — operador entra com PIN (operator-pwa):**
|
||||
1. `await page.goto('/select-operator')` (baseURL = operator).
|
||||
2. **Sem sessão e sem autologin**, a página do picker deve aparecer (o middleware **não** redireciona porque `/select-operator` é a página de login do operador). Confirmar que a lista de operadores está visível.
|
||||
3. Clicar no botão `op1@demo.local` → aparece o keypad (texto `Operador selecionado` + o email).
|
||||
4. **PIN errado:** clicar `9 9 9 9` → `Entrar` → espera o texto de erro `PIN incorreto ou conta bloqueada.…`. (Os dígitos são limpos no erro.)
|
||||
5. **PIN certo:** clicar `1 1 1 1` → `Entrar` → espera navegação para `/` e um elemento da home autenticada (ex.: o botão/link `Pedir manutenção`, ou o que a home do operador mostre — confirmar no código da home).
|
||||
6. (Opcional) recarregar `/` e confirmar que continua autenticado (cookie de sessão `fieldops-op.*` persistiu).
|
||||
|
||||
**Teste B — admin entra com password (admin-web):**
|
||||
1. `await page.goto(\`\${ADMIN_BASE}/login\`)`.
|
||||
2. **Sem autologin**, ir a `\${ADMIN_BASE}/maintenance` **sem sessão** deve **redirecionar para `/login`** — asserir este redirect primeiro (prova que o middleware protege a rota).
|
||||
3. Em `/login`: **password errada** → preencher `#email`=admin@demo.local, `#password`=errada, submeter → espera texto `Email ou password incorretos.…`.
|
||||
4. **Password certa:** `admin1234` → submeter → espera navegação para `/maintenance` e a fila visível (ex.: header `Fila de manutenção` ou os filtros de estado).
|
||||
|
||||
**AC:**
|
||||
- Com os servidores **parados**, `pnpm --filter @repo/e2e test:auth` arranca servidores sem autologin e passa os 2 testes.
|
||||
- Prova-se: rota protegida redireciona para login; PIN/password errados mostram erro; PIN/password certos entram.
|
||||
|
||||
> **Armadilha conhecida:** confirmar no código da home do operator-pwa e da fila do admin **qual** o elemento estável a asserir após o sucesso (evitar asserir texto que dependa de dados). Ler `apps/operator-pwa/app/page.tsx` (ou equivalente) e `apps/admin-web/app/maintenance/maintenance-queue.tsx` (header `Fila de manutenção`).
|
||||
|
||||
---
|
||||
|
||||
## Passo 3 — Scripts + README
|
||||
|
||||
1. **`e2e/package.json`:** adicionar `"test:auth": "playwright test --config playwright.auth.config.ts"`.
|
||||
2. **`package.json` (raiz):** adicionar `"test:e2e:auth": "pnpm --filter @repo/e2e test:auth"` (a par do `test:e2e` existente).
|
||||
3. **README:** na secção "Running the E2E tests", documentar:
|
||||
- `pnpm test:e2e` — happy-path + relatório (autologin ON).
|
||||
- `pnpm test:e2e:auth` — **login real** (autologin OFF). **Pré-condição: nenhum servidor a correr em 3000/3001** (este comando arranca os seus próprios). Requer `pnpm db:seed` feito.
|
||||
4. **Guião de smoke manual** (abaixo) — acrescentar ao README como checklist de demo, OU deixar só neste plano. Decisão do Sonnet: preferir o README (fica à mão do Pedro).
|
||||
|
||||
**AC do Passo 3:** ambos os comandos correm a partir da raiz; README descreve a pré-condição dos servidores parados.
|
||||
|
||||
---
|
||||
|
||||
## Guião de smoke manual (5 min — para o Pedro, complementa os E2E)
|
||||
|
||||
Cobre o que o E2E não cobre confortavelmente: o **PDF de impressão** e o **offline**.
|
||||
|
||||
```
|
||||
1. docker compose up -d && pnpm db:seed
|
||||
2. Operador (login real):
|
||||
pnpm --filter @repo/operator-pwa dev → http://localhost:3000/select-operator
|
||||
op1 → PIN 1111 → entra. (tenta 9999 primeiro → vê o erro)
|
||||
3. Admin (login real):
|
||||
pnpm --filter @repo/admin-web dev → http://localhost:3001/login
|
||||
admin@demo.local / admin1234 → fila.
|
||||
4. Relatório: header → "Relatório de turno"
|
||||
- clica Manhã/Tarde/Noite/Hoje → os números recalculam
|
||||
- botão Imprimir → confirma o PDF limpo (sem botões/seletor/nav)
|
||||
5. Offline (precisa de build de produção do operator-pwa — ver README Troubleshooting):
|
||||
DevTools → Network → Offline → cria 2 pedidos → Online → sincronizam.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cortes propositados — o que NÃO entra
|
||||
|
||||
| Cortado | Porquê |
|
||||
|---|---|
|
||||
| Lockout no E2E | Já no `auth-smoke`; poluiria estado por 5 min |
|
||||
| `window.print()` automatizado | Diálogo nativo; frágil em headless. Fica no smoke manual |
|
||||
| E2E offline | Requer build de produção do PWA + manipular Service Worker; melhor como smoke manual |
|
||||
| Portas dedicadas (3002/3003) para o config de auth | Os scripts têm porta fixa; mesmas portas + "parar servidores" é mais simples |
|
||||
| Testar SUPERVISOR | O seed só cria ADMIN + OPERATOR; `requireRole` já coberto no `report-smoke` (FORBIDDEN) |
|
||||
|
||||
## Sequência crítica
|
||||
|
||||
Passo 1 é independente e barato (config existente). Passo 2 é o de maior valor (login real, nunca testado) mas exige o segundo config — fazer com atenção à armadilha das portas. Passo 3 fecha. **Nada toca em código de produção** — só `e2e/` + scripts + README → risco zero de regressão funcional.
|
||||
|
||||
**Risco principal:** o config de auth colidir com servidores a correr. Mitigação: `reuseExistingServer: false` + nota explícita no README e no AC.
|
||||
@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:auth": "playwright test --config playwright.auth.config.ts",
|
||||
"test:headed": "playwright test --headed",
|
||||
"test:ui": "playwright test --ui",
|
||||
"report": "playwright show-report",
|
||||
|
||||
65
e2e/playwright.auth.config.ts
Normal file
65
e2e/playwright.auth.config.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
const OPERATOR_URL = 'http://localhost:3000';
|
||||
const ADMIN_URL = 'http://localhost:3001';
|
||||
|
||||
export const ADMIN_BASE = ADMIN_URL;
|
||||
|
||||
/**
|
||||
* Playwright config for real-login E2E tests.
|
||||
*
|
||||
* Key differences from playwright.config.ts:
|
||||
* - testDir: './tests-auth' (separate from the autologin tests in ./tests)
|
||||
* - AUTH_DEV_AUTOLOGIN: 'false' on both servers → middleware enforces login
|
||||
* - reuseExistingServer: false → always starts fresh servers without autologin
|
||||
*
|
||||
* IMPORTANT: this config starts its own dev servers on ports 3000 and 3001.
|
||||
* Do NOT run `pnpm test:e2e:auth` while those ports are already in use.
|
||||
* Stop any running dev servers first:
|
||||
* Windows: Get-Process node | Stop-Process -Force
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests-auth',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: [['list'], ['html', { open: 'never' }]],
|
||||
use: {
|
||||
baseURL: OPERATOR_URL,
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
webServer: [
|
||||
{
|
||||
command: 'pnpm --filter @repo/operator-pwa dev',
|
||||
cwd: '..',
|
||||
url: OPERATOR_URL,
|
||||
reuseExistingServer: false,
|
||||
timeout: 120_000,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
// 'false' → dotenv-cli does not override env vars already in the process,
|
||||
// so this wins over whatever AUTH_DEV_AUTOLOGIN is set in .env.
|
||||
env: { AUTH_DEV_AUTOLOGIN: 'false' },
|
||||
},
|
||||
{
|
||||
command: 'pnpm --filter @repo/admin-web dev',
|
||||
cwd: '..',
|
||||
url: ADMIN_URL,
|
||||
reuseExistingServer: false,
|
||||
timeout: 120_000,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
// AUTH_URL must point to the admin server — .env has it at 3000 (operator)
|
||||
// which causes Auth.js to redirect unauthenticated users to localhost:3000.
|
||||
env: { AUTH_DEV_AUTOLOGIN: 'false', AUTH_URL: ADMIN_URL },
|
||||
},
|
||||
],
|
||||
});
|
||||
86
e2e/tests-auth/login.spec.ts
Normal file
86
e2e/tests-auth/login.spec.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { ADMIN_BASE } from '../playwright.auth.config';
|
||||
|
||||
/**
|
||||
* Real-login E2E tests — runs with AUTH_DEV_AUTOLOGIN=false.
|
||||
* Both apps enforce authentication via middleware.
|
||||
*
|
||||
* Preconditions (`pnpm db:seed` + docker compose running):
|
||||
* - op1@demo.local PIN 1111
|
||||
* - admin@demo.local password admin1234
|
||||
*
|
||||
* Run: pnpm test:e2e:auth (no servers running on 3000/3001 — this starts its own)
|
||||
*/
|
||||
|
||||
// ── Operator — PIN login (baseURL = localhost:3000) ──────────────────────────
|
||||
|
||||
test('operator: wrong PIN shows error, correct PIN enters the app', async ({ page }) => {
|
||||
await page.goto('/select-operator');
|
||||
|
||||
// Picker is accessible without a session (it IS the login page)
|
||||
await expect(page.getByText('op1@demo.local')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Select op1
|
||||
await page.getByRole('button', { name: 'op1@demo.local' }).click();
|
||||
await expect(page.getByText('Operador selecionado')).toBeVisible();
|
||||
|
||||
// Wrong PIN: 9 9 9 9
|
||||
for (const d of ['9', '9', '9', '9']) {
|
||||
await page.getByRole('button', { name: d }).click();
|
||||
}
|
||||
await page.getByRole('button', { name: 'Entrar' }).click();
|
||||
await expect(
|
||||
page.getByText('PIN incorreto ou conta bloqueada. Tente novamente.'),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Digits are cleared after error — type correct PIN
|
||||
for (const d of ['1', '1', '1', '1']) {
|
||||
await page.getByRole('button', { name: d }).click();
|
||||
}
|
||||
await page.getByRole('button', { name: 'Entrar' }).click();
|
||||
|
||||
// Successful login → home page with "Pedir manutenção" CTA
|
||||
await expect(page.getByTestId('btn-request-maintenance')).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test('operator: unauthenticated root redirects to picker', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// Without autologin and no session, middleware redirects to /select-operator
|
||||
await expect(page).toHaveURL(/\/select-operator/, { timeout: 10_000 });
|
||||
});
|
||||
|
||||
// ── Admin — password login (separate describe gives an isolated browser context
|
||||
// with baseURL = localhost:3001, avoiding cross-port URL confusion) ──────────
|
||||
|
||||
test.describe('Admin password login', () => {
|
||||
test.use({ baseURL: ADMIN_BASE });
|
||||
|
||||
test('protected route redirects to /login without session', async ({ page }) => {
|
||||
await page.goto('/maintenance');
|
||||
// Middleware redirects to /login (may include ?callbackUrl= query param)
|
||||
await expect(page).toHaveURL(/\/login/, { timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('wrong password shows error, correct password enters the queue', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await expect(page.locator('input#email')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Wrong password
|
||||
await page.fill('input#email', 'admin@demo.local');
|
||||
await page.fill('input#password', 'wrongpassword');
|
||||
await page.getByRole('button', { name: 'Entrar' }).click();
|
||||
await expect(
|
||||
page.getByText('Email ou password incorretos. Tente novamente.'),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Correct password
|
||||
await page.fill('input#password', 'admin1234');
|
||||
await page.getByRole('button', { name: 'Entrar' }).click();
|
||||
|
||||
// Successful login → maintenance queue
|
||||
await expect(page).toHaveURL(/\/maintenance$/, { timeout: 15_000 });
|
||||
await expect(
|
||||
page.getByText('Fila de manutenção').or(page.getByText('pedidos abertos')),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
});
|
||||
67
e2e/tests/report.spec.ts
Normal file
67
e2e/tests/report.spec.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { ADMIN_BASE } from '../playwright.config';
|
||||
|
||||
/**
|
||||
* E2E for MAI CALL v0.3 — shift report page.
|
||||
*
|
||||
* Preconditions (met by `pnpm db:seed` + running docker compose):
|
||||
* - Demo Factory tenant with 6 sample maintenance requests created "today"
|
||||
* - AUTH_DEV_AUTOLOGIN=true on admin-web (set in playwright.config.ts)
|
||||
*
|
||||
* The `waitForLoadState('networkidle')` call is also a regression guard for
|
||||
* the fetch-storm fix: if the 'today' window ever starts recomputing on every
|
||||
* render again (new query key → continuous refetch), networkidle is never
|
||||
* reached and this test fails at that line.
|
||||
*/
|
||||
test('shift report: renders with seed data and reacts to window selection', async ({ page }) => {
|
||||
await page.goto(`${ADMIN_BASE}/maintenance/report`);
|
||||
|
||||
// networkidle = fetch-storm sentinel: continuous refetch would never settle
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Title visible
|
||||
await expect(page.getByRole('heading', { name: 'Relatório de turno' })).toBeVisible();
|
||||
|
||||
// Default window is "Hoje" — label starts with "Hoje —"
|
||||
await expect(page.getByText(/^Hoje —/)).toBeVisible();
|
||||
|
||||
// Seed creates 6 requests "today" → Pedidos card must show > 0
|
||||
const pedidosCard = page.locator('div.rounded-xl').filter({ hasText: /^Pedidos/ }).first();
|
||||
await expect(pedidosCard).toBeVisible();
|
||||
const pedidosText = await pedidosCard.locator('p.text-2xl').textContent();
|
||||
expect(parseInt(pedidosText ?? '0')).toBeGreaterThan(0);
|
||||
|
||||
// "Resposta média" card must be present and non-empty
|
||||
const respostaCard = page.locator('div.rounded-xl').filter({ hasText: /^Resposta média/ }).first();
|
||||
await expect(respostaCard).toBeVisible();
|
||||
const respostaValue = respostaCard.locator('p.text-2xl');
|
||||
const respostaText = await respostaValue.textContent();
|
||||
expect(respostaText?.trim().length).toBeGreaterThan(0);
|
||||
|
||||
// "Por posto" table must have at least one data row
|
||||
await expect(page.locator('table tbody tr').first()).toBeVisible();
|
||||
|
||||
// Imprimir button exists (don't click — triggers native print dialog)
|
||||
await expect(page.getByRole('button', { name: 'Imprimir' })).toBeVisible();
|
||||
|
||||
// ── Reactivity: switching to Tarde changes the window label ──────────────
|
||||
await page.getByRole('button', { name: 'Tarde' }).click();
|
||||
await expect(page.getByText(/^Turno da Tarde —/)).toBeVisible();
|
||||
|
||||
// Switch back to Hoje
|
||||
await page.getByRole('button', { name: 'Hoje' }).click();
|
||||
await expect(page.getByText(/^Hoje —/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('shift report: accessible from the maintenance queue link', async ({ page }) => {
|
||||
await page.goto(`${ADMIN_BASE}/maintenance`);
|
||||
await expect(page.getByRole('link', { name: 'Relatório de turno' })).toBeVisible();
|
||||
|
||||
await page.getByRole('link', { name: 'Relatório de turno' }).click();
|
||||
await expect(page).toHaveURL(`${ADMIN_BASE}/maintenance/report`);
|
||||
await expect(page.getByRole('heading', { name: 'Relatório de turno' })).toBeVisible();
|
||||
|
||||
// "← Fila" back link works
|
||||
await page.getByRole('link', { name: 'Fila' }).click();
|
||||
await expect(page).toHaveURL(`${ADMIN_BASE}/maintenance`);
|
||||
});
|
||||
@ -15,6 +15,7 @@
|
||||
"typecheck": "turbo run typecheck",
|
||||
"test": "turbo run test",
|
||||
"test:e2e": "pnpm --filter @repo/e2e test",
|
||||
"test:e2e:auth": "pnpm --filter @repo/e2e test:auth",
|
||||
"db:generate": "pnpm --filter @repo/db exec prisma generate",
|
||||
"db:migrate": "pnpm --filter @repo/db exec prisma migrate dev",
|
||||
"db:migrate:deploy": "pnpm --filter @repo/db exec prisma migrate deploy",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user