multiple verifications
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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`);
|
||||
});
|
||||
Reference in New Issue
Block a user