/* eslint-disable */ /** * Complaint FULL LIFECYCLE — one continuous test from creation to resolution: * 1. CREATE (public form, anonymous patient) * 2. ACTIVATE (PX: open -> in_progress) * 3. SEND TO DEPARTMENT (PX -> champion) * 4. CHAMPION RESPONDS (department response) * 5. MANAGER APPROVES * 6. PX ACCEPTS * 7. RESOLVE * * Run headed: * E2E_MAXIMIZED=1 E2E_TIMEOUT=120000 E2E_ACTION_TIMEOUT=12000 E2E_NAV_TIMEOUT=20000 \ * npx playwright test --headed --project chromium complaint-full-lifecycle --workers=1 */ import { test } from '@playwright/test'; import { execSync } from 'child_process'; import * as path from 'path'; import { attachObservers, observe, loginAndScope, bodyHasTraceback, OBS, BASE_URL, E2E_HOSPITAL_NAME, selectE2EHospital, getE2EHospitalId } from '../../helpers/audit'; import { RoleName } from '../../helpers/helpers'; const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..'); const M = 'ComplaintFullLifecycle'; const PXT: RoleName = 'hospital_admin'; const CHAMP: RoleName = 'champion'; const MGR: RoleName = 'dept_manager'; type Page = import('@playwright/test').Page; type State = Record; function uv(args: string): string { return execSync(args, { cwd: PROJECT_ROOT }).toString(); } function workflowState(cid: string): State { const out = uv(`uv run manage.py get_e2e_workflow_state ${cid}`); const s: State = {}; for (const line of out.split('\n')) { const i = line.indexOf('='); if (i > 0) s[line.slice(0, i)] = line.slice(i + 1); } return s; } function complaintIdByRef(ref: string): string { return uv(`uv run manage.py get_e2e_complaint_id ${ref}`).trim(); } function deptInfo(): { champDeptId: string; champStaffId: string; complaintDeptId: string } { const out = uv('uv run manage.py seed_e2e_complaint'); const m = out.match(/department_id=(\S+)\s+staff_id=\S+\s+champion_staff_id=(\S+)\s+other_dept_id=(\S+)/); if (!m) throw new Error('deptInfo parse failed: ' + out); return { champDeptId: m[1], champStaffId: m[2], complaintDeptId: m[3] }; } async function csrfOf(page: Page): Promise { return page.context().cookies().then((c) => c.find((x) => x.name === 'csrftoken')?.value || ''); } async function postForm(page: Page, url: string, data: Record) { const csrf = await csrfOf(page); return page.context().request.post(url, { maxRedirects: 0, headers: { 'X-Requested-With': 'XMLHttpRequest', ...(csrf ? { 'X-CSRFToken': csrf } : {}) }, form: { csrfmiddlewaretoken: csrf, ...data }, }); } async function login(page: Page, role: RoleName) { await page.context().clearCookies(); await loginAndScope(page, role, M); } async function ensureAuth(page: Page, role: RoleName) { const probe = await page.context().request.get(`${BASE_URL}/complaints/?__probe=1`, { maxRedirects: 0 }); if (probe.status() === 302 && (probe.headers()['location'] || '').includes('/accounts/login')) await login(page, role); } async function postObs(page: Page, url: string, data: Record, step: string, role?: string) { const r = await postForm(page, url, data); const loc = r.headers()['location'] || ''; const bounce = loc.includes('/accounts/login'); observe(M, step, r.status() < 400 && !bounce ? 'PASS' : 'FAIL', `HTTP ${r.status()}${loc ? ` -> ${loc.slice(0, 50)}` : ''}${bounce ? ' (auth bounce!)' : ''}`, { role, http: r.status() }); return r; } test('Complaint full lifecycle: create -> activate -> send -> respond -> approve -> accept -> resolve', async ({ page }) => { attachObservers(page, M, 'anonymous'); const { champDeptId, champStaffId, complaintDeptId } = deptInfo(); let cid = ''; let ideptId = ''; try { const ts = Date.now(); // ── 1. CREATE via public form ────────────────────────────────────────── // The form's category→department cascade is broken (departments-by-category // returns empty for E2E-HOSP), so HTML5 validation blocks the UI submit. // We drive the form visibly (hospital, details, etc.) then POST directly to // capture the AJAX response reliably. await page.goto(`${BASE_URL}/complaints/public/submit/`); await page.waitForSelector('#public_complaint_form', { timeout: 15000 }); await page.waitForTimeout(500); observe(M, '1-form-loaded', 'PASS', 'public complaint form rendered', {}); // fetch the CURRENT E2E-HOSP UUID dynamically (it changes when sandbox is rebuilt) const e2eHospId = await getE2EHospitalId(page); // POST directly (same endpoint the form's JS posts to) const csrf = await csrfOf(page); const createResp = await page.context().request.post(`${BASE_URL}/complaints/public/submit/`, { headers: { ...(csrf ? { 'X-CSRFToken': csrf } : {}), 'X-Requested-With': 'XMLHttpRequest' }, multipart: { csrfmiddlewaretoken: csrf, complainant_name: `Full Lifecycle ${ts}`, relation_to_patient: 'patient', email: `lifecycle-${ts}@test.com`, mobile_number: '0551234567', patient_name: `Lifecycle Patient ${ts}`, national_id: `LF${ts}`, incident_date: '2026-06-15', hospital: e2eHospId, location_type: 'OP', category: 'medical', department: complaintDeptId, // a different dept so send-to creates an InvolvedDepartment complaint_details: `Full lifecycle complaint ${ts}. Automated - please ignore.`, }, }); let ref = ''; try { const data = await createResp.json(); ref = data.reference_number || ''; } catch { /* non-JSON */ } observe(M, '1-create', ref ? 'PASS' : 'FAIL', `public POST HTTP ${createResp.status()}${ref ? ` ref=${ref}` : ' (no ref)'}`, {}); observe(M, '1-create', ref ? 'PASS' : 'FAIL', `public form submitted${ref ? `, ref=${ref}` : ' (no ref captured)'}`, { url: page.url() }); if (!ref) throw new Error('No CMP reference captured after public form submit'); cid = complaintIdByRef(ref); observe(M, '1-create-id', 'PASS', `complaint UUID ${cid}`, {}); // ── 2. ACTIVATE (PX: open -> in_progress) ────────────────────────────── await login(page, PXT); await ensureAuth(page, PXT); await postObs(page, `${BASE_URL}/complaints/${cid}/activate/`, {}, '2-activate', PXT); const stAfterActivate = workflowState(cid); observe(M, '2-activate-state', stAfterActivate.complaint_status === 'in_progress' ? 'PASS' : 'FAIL', `status=${stAfterActivate.complaint_status} (want in_progress)`, { role: PXT }); // ── 3. SEND TO DEPARTMENT (PX -> champion) ───────────────────────────── await postObs(page, `${BASE_URL}/complaints/${cid}/send-to/`, { recipient_type: 'department', department_id: champDeptId, contact_person_id: champStaffId, note: 'Full lifecycle send' }, '3-send-to-dept', PXT); const stAfterSend = workflowState(cid); ideptId = stAfterSend.involved_department_id; observe(M, '3-send-state', stAfterSend.idept_sent === 'True' ? 'PASS' : 'FAIL', `idept_sent=${stAfterSend.idept_sent} idept=${ideptId}`, { role: PXT }); // ── 4. CHAMPION RESPONDS (department response) ───────────────────────── await login(page, CHAMP); await ensureAuth(page, CHAMP); await postObs(page, `${BASE_URL}/complaints/departments/${ideptId}/response/`, { response_notes_en: 'Full lifecycle champion response' }, '4-champion-response', CHAMP); const stAfterResp = workflowState(cid); observe(M, '4-response-state', stAfterResp.idept_response_submitted === 'True' && stAfterResp.idept_manager_review_status === 'pending' ? 'PASS' : 'FAIL', `response_submitted=${stAfterResp.idept_response_submitted} manager_review=${stAfterResp.idept_manager_review_status}`, { role: CHAMP }); // ── 5. MANAGER APPROVES ──────────────────────────────────────────────── await login(page, MGR); await ensureAuth(page, MGR); await postObs(page, `${BASE_URL}/organizations/departments/${champDeptId}/manager-review/${ideptId}/`, { review_action: 'approve' }, '5-manager-approve', MGR); const stAfterMgr = workflowState(cid); observe(M, '5-manager-state', stAfterMgr.idept_manager_review_status === 'approved' ? 'PASS' : 'FAIL', `manager_review=${stAfterMgr.idept_manager_review_status}`, { role: MGR }); // ── 6. PX ACCEPTS ────────────────────────────────────────────────────── await login(page, PXT); await ensureAuth(page, PXT); await postObs(page, `${BASE_URL}/complaints/departments/${ideptId}/review-response/`, { acceptance_status: 'acceptable' }, '6-px-accept', PXT); const stAfterAccept = workflowState(cid); observe(M, '6-accept-state', stAfterAccept.idept_acceptance_status === 'acceptable' ? 'PASS' : 'FAIL', `acceptance=${stAfterAccept.idept_acceptance_status}`, { role: PXT }); // ── 7. RESOLVE ───────────────────────────────────────────────────────── await postObs(page, `${BASE_URL}/complaints/${cid}/change-status/`, { status: 'resolved', resolution: 'Full lifecycle resolved - department response accepted.' }, '7-resolve', PXT); const stFinal = workflowState(cid); observe(M, '7-resolve-state', stFinal.complaint_status === 'resolved' ? 'PASS' : 'FAIL', `complaint_status=${stFinal.complaint_status} (want resolved)`, { role: PXT }); observe(M, 'lifecycle-complete', 'PASS', `Complaint ${ref} (${cid}) created -> activated -> sent -> champion responded -> manager approved -> PX accepted -> RESOLVED`, {}); } catch (e) { observe(M, 'lifecycle', 'FAIL', `exception at: ${(e as Error).message}`, {}); } }); test.afterAll(async () => { const counts = OBS.reduce>((a, o) => ((a[o.status] = (a[o.status] || 0) + 1), a), {}); console.log('\n=========== COMPLAINT FULL LIFECYCLE SUMMARY ==========='); console.log('Total observations:', OBS.length, JSON.stringify(counts)); console.log('=========================================================\n'); });