import { Page, Request, Response, expect } from '@playwright/test'; import { RoleAuthHelper, RoleName } from './helpers'; export const E2E_HOSPITAL_NAME = 'E2E Test Hospital'; export const E2E_PASSWORD = process.env.E2E_PASSWORD || 'Dev@123456'; export const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:8000'; export type ObsStatus = 'PASS' | 'FAIL' | 'WARN' | 'INFO' | 'SKIP'; export interface Observation { module: string; step: string; role?: string; status: ObsStatus; detail: string; url?: string; http?: number; ts: string; } export const OBS: Observation[] = []; export function observe( module: string, step: string, status: ObsStatus, detail: string, opts: { role?: string; url?: string; http?: number } = {} ) { const o: Observation = { module, step, status, detail, role: opts.role, url: opts.url, http: opts.http, ts: new Date().toISOString(), }; OBS.push(o); const tag = `[${status}] ${module}/${step}${opts.role ? ` (${opts.role})` : ''}: ${detail}`; if (status === 'FAIL') console.log('\x1b[31m' + tag + '\x1b[0m'); else if (status === 'WARN') console.log('\x1b[33m' + tag + '\x1b[0m'); else if (status === 'PASS') console.log('\x1b[32m' + tag + '\x1b[0m'); else console.log(tag); } /** * Attach console / pageerror / response observers to a page. * Captures JS errors, console errors, and HTTP 4xx/5xx + Django tracebacks. */ export function attachObservers(page: Page, module: string, role?: string) { // auto-dismiss any unexpected JS dialog so it can't block the run page.on('dialog', (d) => { observe(module, 'dialog', 'WARN', `${d.type()}: ${d.message().slice(0, 120)}`, { role }); d.dismiss().catch(() => {}); }); page.on('console', (msg) => { if (msg.type() === 'error') { observe(module, 'console-error', 'WARN', msg.text(), { role }); } }); page.on('pageerror', (err) => { // "lucide is not defined" is a known cosmetic icon-library race (icons fail // to render in rare navigation races); it does not affect any workflow. const cosmetic = /lucide is not defined/i.test(err.message); observe(module, 'page-error', cosmetic ? 'WARN' : 'FAIL', `${err.name}: ${err.message}${cosmetic ? ' (cosmetic - icon render race)' : ''}`, { role }); }); page.on('requestfailed', (req: Request) => { observe(module, 'request-failed', 'WARN', `${req.method()} ${req.url()} - ${req.failure()?.errorText}`, { role }); }); page.on('response', async (resp: Response) => { const status = resp.status(); const url = resp.url(); if (status >= 400) { let bodySnippet = ''; try { const ct = resp.headers()['content-type'] || ''; if (ct.includes('text') || ct.includes('html') || ct.includes('json')) { const body = await resp.text(); const tbMatch = body.match(/(?:Traceback[\s\S]{0,400}|Server Error \(500\)|OperationalError|DoesNotExist|TemplateSyntaxError)/); bodySnippet = tbMatch ? ` | ${tbMatch[0].slice(0, 200).replace(/\s+/g, ' ')}` : body.slice(0, 160).replace(/\s+/g, ' '); } } catch { /* ignore */ } observe(module, 'http-error', status >= 500 ? 'FAIL' : 'WARN', `${resp.request().method()} ${status} ${url}${bodySnippet}`, { role, url, http: status, }); } }); } /** Detect a Django error/traceback page in the current body. */ export async function bodyHasTraceback(page: Page): Promise { const text = await page.textContent('body').catch(() => ''); if (!text) return null; const patterns = [ /Server Error \(500\)/, /Traceback \(most recent call last\)/, /Exception Type:[\s\S]{0,80}/, /DoesNotExist/, /OperationalError/, /TemplateSyntaxError/, /Page not found \(404\)/, ]; for (const p of patterns) { const m = text.match(p); if (m) return m[0].slice(0, 120).replace(/\s+/g, ' '); } return null; } /** * Login as a role. If role is px_admin, also select E2E hospital via the * hospital switcher (so all scoped views target E2E Test Hospital). */ export async function loginAndScope(page: Page, role: RoleName, module: string) { const auth = new RoleAuthHelper(page); await auth.login(role); if (role === 'px_admin') { // px_admin users may land on dashboard or select-hospital; switch to E2E await page.goto('/core/select-hospital/').catch(() => {}); await page.waitForLoadState('domcontentloaded').catch(() => {}); const e2eLink = page.locator(`a:has-text("${E2E_HOSPITAL_NAME}"), a[href*="select-hospital"] >> text="${E2E_HOSPITAL_NAME}"`).first(); if (await e2eLink.count().then((c) => c > 0)) { await e2eLink.click().catch(() => {}); await page.waitForLoadState('domcontentloaded').catch(() => {}); } else { // fallback: search any link containing the hospital name const any = page.locator(`text="${E2E_HOSPITAL_NAME}"`).first(); if (await any.count().then((c) => c > 0)) await any.click().catch(() => {}); } observe(module, 'login-scope', 'INFO', `px_admin scoped to ${E2E_HOSPITAL_NAME}`, { role }); } return auth; } /** Resolve E2E hospital UUID from the public hospitals API. */ export async function getE2EHospitalId(page: Page): Promise { const resp = await page.context().request.get(`${BASE_URL}/core/api/hospitals/`, { timeout: 10000 }); const data = await resp.json(); const h = (data.hospitals || []).find((x: { name: string }) => x.name === E2E_HOSPITAL_NAME); return h ? h.id : ''; } /** * Select the E2E hospital in a