multiple verifications

This commit is contained in:
2026-05-30 14:55:06 +01:00
parent 58451f805e
commit 2093f12d0a
8 changed files with 460 additions and 7 deletions
+1
View File
@@ -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
View 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
View 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
View 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`);
});