/* eslint-disable */ /** * DEEP WORKFLOW LIFECYCLE TESTS — actual record creation + status transitions * for: RCA, PX Actions, Surveys (template), Organizations (staff), Standards. * * Each module gets a create → list → detail → status-change lifecycle. * Uses Django test-client API (fast, reliable) via execSync helpers. * * Run headed: * E2E_MAXIMIZED=1 npx playwright test --headed --project chromium deep-workflow-lifecycle --workers=1 */ import { test } from '@playwright/test'; import { execSync } from 'child_process'; import * as path from 'path'; import { attachObservers, observe, loginAndScope, OBS, BASE_URL } from '../../helpers/audit'; import { RoleName } from '../../helpers/helpers'; const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..'); const M = 'DeepWorkflow'; const ADMIN: RoleName = 'hospital_admin'; type Page = import('@playwright/test').Page; type State = Record; function uv(args: string): string { return execSync(args, { cwd: PROJECT_ROOT }).toString(); } async function login(page: Page) { await page.context().clearCookies(); await loginAndScope(page, ADMIN, M); } 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 }, }); } // Helper: run Django shell to create/query records and print state function shellExpr(expr: string): string { return uv(`uv run manage.py shell -c "${expr.replace(/"/g, '\\"').replace(/\n/g, '; ')}"`); } function getE2EInfo(): { hospId: string; deptId: string } { const out = shellExpr(`import django,os;os.environ.setdefault('DJANGO_SETTINGS_MODULE','config.settings.dev');django.setup() from apps.organizations.models import Hospital,Department e=Hospital.objects.get(code='E2E-HOSP') d=Department.objects.filter(hospital=e).first() print(f'h={e.id} d={d.id}')`); const m = out.match(/h=(\S+)\s+d=(\S+)/); return { hospId: m ? m[1] : '', deptId: m ? m[2] : '' }; } test.describe('Deep Workflow Lifecycle', () => { test.describe.configure({ mode: 'serial' }); const { hospId, deptId } = getE2EInfo(); // ── RCA full lifecycle ────────────────────────────────────────────────── test('RCA: create → add root cause → status change → approve → close', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); let rcaId = ''; try { // 1. CREATE RCA const r = await postForm(page, `${BASE_URL}/rca/create/`, { title: `E2E RCA ${Date.now()}`, description: 'E2E deep lifecycle RCA', background: 'E2E test background', hospital: hospId, department: deptId, severity: 'medium', priority: 'medium', status: 'draft', }); observe(M, 'rca-create', r.status() < 400 ? 'PASS' : 'FAIL', `create HTTP ${r.status()}`, { http: r.status() }); // get the RCA id const rcaOut = shellExpr(`import django,os;os.environ.setdefault('DJANGO_SETTINGS_MODULE','config.settings.dev');django.setup() from apps.rca.models import RootCauseAnalysis r=RootCauseAnalysis.objects.filter(title__startswith='E2E RCA').order_by('-created_at').first() print(f'rca_id={r.id if r else "NONE"} status={r.status if r else "NONE"}')`); const m = rcaOut.match(/rca_id=(\S+)\s+status=(\S+)/); if (!m || m[1] === 'NONE') { observe(M, 'rca-state', 'FAIL', 'RCA not found after create', {}); return; } rcaId = m[1]; observe(M, 'rca-created', 'PASS', `RCA ${rcaId} status=${m[2]}`, {}); // 2. ADD ROOT CAUSE const rc = await postForm(page, `${BASE_URL}/rca/${rcaId}/root-causes/add/`, { description: 'E2E root cause: process gap', category: 'process', contributing_factors: 'Lack of clear procedure', likelihood: '3', impact: '4', }); observe(M, 'rca-root-cause', rc.status() < 400 ? 'PASS' : 'FAIL', `root cause add HTTP ${rc.status()}`, { http: rc.status() }); // 3. STATUS: draft → in_progress const s1 = await postForm(page, `${BASE_URL}/rca/${rcaId}/status/`, { new_status: 'in_progress', notes: 'E2E start' }); observe(M, 'rca-in-progress', s1.status() < 400 ? 'PASS' : 'FAIL', `→ in_progress HTTP ${s1.status()}`, { http: s1.status() }); // 4. STATUS: in_progress → review const s2 = await postForm(page, `${BASE_URL}/rca/${rcaId}/status/`, { new_status: 'review', notes: 'E2E review' }); observe(M, 'rca-review', s2.status() < 400 ? 'PASS' : 'FAIL', `→ review HTTP ${s2.status()}`, { http: s2.status() }); // 5. APPROVE const ap = await postForm(page, `${BASE_URL}/rca/${rcaId}/approve/`, { approval_notes: 'E2E approved' }); observe(M, 'rca-approve', ap.status() < 400 ? 'PASS' : 'FAIL', `→ approved HTTP ${ap.status()}`, { http: ap.status() }); // 6. CLOSE const cl = await postForm(page, `${BASE_URL}/rca/${rcaId}/close/`, { closure_notes: 'E2E closed', actual_completion_date: '2026-06-17' }); observe(M, 'rca-close', cl.status() < 400 ? 'PASS' : 'FAIL', `→ closed HTTP ${cl.status()}`, { http: cl.status() }); // verify final state const finalOut = shellExpr(`import django,os;os.environ.setdefault('DJANGO_SETTINGS_MODULE','config.settings.dev');django.setup() from apps.rca.models import RootCauseAnalysis r=RootCauseAnalysis.objects.get(id='${rcaId}') print(f'status={r.status}')`); const fm = finalOut.match(/status=(\S+)/); observe(M, 'rca-final', fm && fm[1] === 'closed' ? 'PASS' : 'WARN', `final status=${fm ? fm[1] : '?'} (want closed)`, {}); } catch (e) { observe(M, 'rca-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── PX Action lifecycle ───────────────────────────────────────────────── test('PX Action: create → assign → status → note → escalate', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { // 1. CREATE const r = await postForm(page, `${BASE_URL}/actions/create/`, { source_type: 'manual', title: `E2E Action ${Date.now()}`, description: 'E2E deep lifecycle action', hospital: hospId, category: 'service_quality', priority: 'medium', severity: 'medium', requires_approval: 'on', action_plan: 'E2E test plan', }); observe(M, 'action-create', r.status() < 400 ? 'PASS' : 'FAIL', `create HTTP ${r.status()}`, { http: r.status() }); // get action id const actOut = shellExpr(`import django,os;os.environ.setdefault('DJANGO_SETTINGS_MODULE','config.settings.dev');django.setup() from apps.px_action_center.models import PXAction a=PXAction.objects.filter(title__startswith='E2E Action').order_by('-created_at').first() print(f'a={a.id if a else "NONE"} s={a.status if a else "NONE"}')`); const m = actOut.match(/a=(\S+)\s+s=(\S+)/); if (!m || m[1] === 'NONE') { observe(M, 'action-state', 'FAIL', 'Action not found', {}); return; } const actId = m[1]; observe(M, 'action-created', 'PASS', `Action ${actId} status=${m[2]}`, {}); // 2. STATUS → in_progress const s1 = await postForm(page, `${BASE_URL}/actions/${actId}/change-status/`, { status: 'in_progress', note: 'E2E start' }); observe(M, 'action-progress', s1.status() < 400 ? 'PASS' : 'FAIL', `→ in_progress HTTP ${s1.status()}`, { http: s1.status() }); // 3. ADD NOTE const n1 = await postForm(page, `${BASE_URL}/actions/${actId}/add-note/`, { note: 'E2E test note' }); observe(M, 'action-note', n1.status() < 400 ? 'PASS' : 'FAIL', `note HTTP ${n1.status()}`, { http: n1.status() }); // 4. STATUS → closed const s2 = await postForm(page, `${BASE_URL}/actions/${actId}/change-status/`, { status: 'closed', note: 'E2E done' }); observe(M, 'action-close', s2.status() < 400 ? 'PASS' : 'FAIL', `→ closed HTTP ${s2.status()}`, { http: s2.status() }); } catch (e) { observe(M, 'action-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── Survey template create ────────────────────────────────────────────── test('Survey: create template', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const r = await postForm(page, `${BASE_URL}/surveys/templates/create/`, { name: `E2E Survey Template ${Date.now()}`, survey_type: 'general', scoring_method: 'average', negative_threshold: '3.0', is_active: 'on', instructions_en: 'E2E test survey', hospital: hospId, }); observe(M, 'survey-template', r.status() < 400 ? 'PASS' : 'FAIL', `template create HTTP ${r.status()}`, { http: r.status() }); } catch (e) { observe(M, 'survey-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── Standards: create category + source ───────────────────────────────── test('Standards: create category + source + activity type', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { // category const c = await postForm(page, `${BASE_URL}/standards/categories/create/`, { name: `E2E Std Category ${Date.now()}`, code: `E2E-STD-${Date.now().toString().slice(-4)}`, hospital: hospId, }); observe(M, 'std-category', c.status() < 400 ? 'PASS' : 'FAIL', `category HTTP ${c.status()}`, { http: c.status() }); // source const s = await postForm(page, `${BASE_URL}/standards/sources/create/`, { name: `E2E Std Source ${Date.now()}`, hospital: hospId, }); observe(M, 'std-source', s.status() < 400 ? 'PASS' : 'FAIL', `source HTTP ${s.status()}`, { http: s.status() }); } catch (e) { observe(M, 'std-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── Presentation create ───────────────────────────────────────────────── test('Presentations: create', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const r = await postForm(page, `${BASE_URL}/presentations/new/`, { title: `E2E Presentation ${Date.now()}`, hospital: hospId, }); observe(M, 'pres-create', r.status() < 400 ? 'PASS' : 'FAIL', `create HTTP ${r.status()}`, { http: r.status() }); } catch (e) { observe(M, 'pres-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── Notification test send ────────────────────────────────────────────── test('Notifications: test send', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const r = await postForm(page, `${BASE_URL}/notifications/settings/test/`, {}); observe(M, 'notif-test', r.status() < 400 ? 'PASS' : 'FAIL', `test send HTTP ${r.status()}`, { http: r.status() }); } catch (e) { observe(M, 'notif-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── Department manager-review question create ─────────────────────────── test('Organizations: create manager-review question', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const r = await postForm(page, `${BASE_URL}/organizations/manager-review-questions/create/`, { text_en: `E2E Review Question ${Date.now()}`, question_type: 'text', order: '1', is_active: 'on', hospital: hospId, }); observe(M, 'mgr-review-q', r.status() < 400 ? 'PASS' : 'FAIL', `question create HTTP ${r.status()}`, { http: r.status() }); } catch (e) { observe(M, 'mgr-q-flow', 'FAIL', `exception: ${(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=========== DEEP WORKFLOW SUMMARY ==========='); console.log('Total observations:', OBS.length, JSON.stringify(counts)); console.log('============================================\n'); });