/* eslint-disable */ /** * DEEP WORKFLOW V4 — interactive workflows: * Complaint reopen, involved staff/dept, convert-to-action, * KPI report generate, presentation generate, bulk actions, * observation convert-to-action, complaint PDF quality. * * Run headed: * E2E_MAXIMIZED=1 npx playwright test --headed --project chromium deep-workflow-v4 --workers=1 */ import { test } from '@playwright/test'; import { execSync } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import { attachObservers, observe, loginAndScope, OBS, BASE_URL, getE2EHospitalId } from '../../helpers/audit'; import { RoleName } from '../../helpers/helpers'; const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..'); const M = 'DeepWorkflowV4'; const ADMIN: RoleName = 'hospital_admin'; type Page = import('@playwright/test').Page; 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 }, }); } function uv(args: string): string { return execSync(args, { cwd: PROJECT_ROOT }).toString(); } function shellPy(code: string): string { const tmpFile = `/tmp/e2e_v4_${Date.now()}_${Math.random().toString(36).slice(2,6)}.py`; fs.writeFileSync(tmpFile, code); const out = uv(`uv run manage.py shell < ${tmpFile}`); try { fs.unlinkSync(tmpFile); } catch {} return out; } test.describe('Deep Workflow V4', () => { test.describe.configure({ mode: 'serial' }); // ── 1. Complaint reopen ───────────────────────────────────────────────── test('Complaint: reopen a resolved complaint', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { // find a resolved complaint const out = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.complaints.models import Complaint from apps.organizations.models import Hospital h = Hospital.objects.get(code='E2E-HOSP') c = Complaint.objects.filter(hospital=h, status='resolved').first() if c is None: c = Complaint.objects.filter(hospital=h).first() if c: c.status = 'resolved' c.save(update_fields=['status']) print('CID=' + str(c.id) if c else 'CID=NONE') `); const m = out.match(/CID=(\S+)/); if (!m || m[1] === 'NONE') { observe(M, 'reopen', 'WARN', 'no complaint to reopen', {}); return; } const cid = m[1]; const r = await postForm(page, `${BASE_URL}/complaints/${cid}/reopen/`, { reopen_reason: 'E2E V4 test reopen' }); observe(M, 'complaint-reopen', r.status() < 400 ? 'PASS' : 'WARN', `reopen HTTP ${r.status()}`, { http: r.status() }); } catch (e) { observe(M, 'reopen', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 2. Involved staff + department add ────────────────────────────────── test('Complaint: add involved staff + department', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const out = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.complaints.models import Complaint from apps.organizations.models import Hospital, Staff, Department h = Hospital.objects.get(code='E2E-HOSP') c = Complaint.objects.filter(hospital=h).first() d = Department.objects.filter(hospital=h).first() s = Staff.objects.filter(hospital=h).first() print('CID=' + str(c.id) + ' DID=' + str(d.id) + ' SID=' + str(s.id)) `); const m = out.match(/CID=(\S+)\s+DID=(\S+)\s+SID=(\S+)/); if (!m) { observe(M, 'involved', 'WARN', 'no data', {}); return; } const [, cid, did, sid] = m; // add involved department const dr = await postForm(page, `${BASE_URL}/complaints/${cid}/departments/add/`, { department: did, role: 'secondary', is_primary: '', }); observe(M, 'involved-dept', dr.status() < 400 ? 'PASS' : 'WARN', `dept add HTTP ${dr.status()}`, { http: dr.status() }); // add involved staff const sr = await postForm(page, `${BASE_URL}/complaints/${cid}/staff/add/`, { staff: sid, role: 'witness', }); observe(M, 'involved-staff', sr.status() < 400 ? 'PASS' : 'WARN', `staff add HTTP ${sr.status()}`, { http: sr.status() }); } catch (e) { observe(M, 'involved', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 3. Convert observation to action ──────────────────────────────────── test('Observation: convert to PX Action', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const out = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.observations.models import Observation from apps.organizations.models import Hospital h = Hospital.objects.get(code='E2E-HOSP') o = Observation.objects.filter(hospital=h).first() print('OID=' + str(o.id) if o else 'OID=NONE') `); const m = out.match(/OID=(\S+)/); if (!m || m[1] === 'NONE') { observe(M, 'obs-convert', 'WARN', 'no observation', {}); return; } const oid = m[1]; const r = await postForm(page, `${BASE_URL}/actions/create-from-observation-ai/${oid}/`, { action: 'E2E V4: follow up on this observation', priority: 'medium', category: 'patient_safety', }); observe(M, 'obs-to-action', r.status() < 400 ? 'PASS' : 'WARN', `convert HTTP ${r.status()}`, { http: r.status() }); } catch (e) { observe(M, 'obs-convert', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 4. KPI report generate ────────────────────────────────────────────── test('Analytics: KPI report generate page', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { // visit the generate page await page.goto(`${BASE_URL}/analytics/kpi-reports/generate/`); await page.waitForLoadState('domcontentloaded'); const renders = (await page.textContent('body') || '').length > 200; observe(M, 'kpi-gen-page', renders ? 'PASS' : 'WARN', `KPI report generate page: ${renders}`, { url: page.url() }); // try the submit endpoint const hospId = await getE2EHospitalId(page); const r = await postForm(page, `${BASE_URL}/analytics/kpi-reports/generate/submit/`, { hospital: hospId, report_type: 'overview', period: 'monthly', year: '2026', month: '6', }); observe(M, 'kpi-gen-submit', r.status() < 400 ? 'PASS' : 'WARN', `KPI generate HTTP ${r.status()}`, { http: r.status() }); } catch (e) { observe(M, 'kpi', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 5. Complaint PDF download quality ─────────────────────────────────── test('Complaint: PDF generates valid file', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const out = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.complaints.models import Complaint from apps.organizations.models import Hospital h = Hospital.objects.get(code='E2E-HOSP') c = Complaint.objects.filter(hospital=h).first() print('CID=' + str(c.id) if c else 'CID=NONE') `); const m = out.match(/CID=(\S+)/); if (!m || m[1] === 'NONE') { observe(M, 'pdf', 'WARN', 'no complaint', {}); return; } const cid = m[1]; const csrf = await csrfOf(page); const r = await page.context().request.get(`${BASE_URL}/complaints/${cid}/pdf/`, { headers: { ...(csrf ? { 'X-CSRFToken': csrf } : {}) }, }); const ct = r.headers()['content-type'] || ''; const body = await r.body(); const isPdf = ct.includes('pdf') || (body.length > 1000 && body[0] === 0x25 && body[1] === 0x50); // %P = PDF magic observe(M, 'complaint-pdf', r.status() === 200 && isPdf ? 'PASS' : 'WARN', `PDF: HTTP ${r.status()} ct=${ct} size=${body.length} magic=${body[0] === 0x25 ? '%PDF' : '?'}`, { http: r.status() }); } catch (e) { observe(M, 'pdf', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 6. Presentation generate ──────────────────────────────────────────── test('Presentations: generate page', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { await page.goto(`${BASE_URL}/presentations/generate/`); await page.waitForLoadState('domcontentloaded'); const renders = (await page.textContent('body') || '').length > 200; observe(M, 'pres-gen-page', renders ? 'PASS' : 'WARN', `presentation generate page: ${renders}`, { url: page.url() }); } catch (e) { observe(M, 'pres-gen', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 7. Dashboard bulk action (list) ───────────────────────────────────── test('Dashboard: bulk action endpoint', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const r = await postForm(page, `${BASE_URL}/bulk-action/`, { action: 'export_csv', item_ids: '', }); observe(M, 'bulk-action', r.status() < 500 ? 'PASS' : 'FAIL', `bulk action HTTP ${r.status()}`, { http: r.status() }); } catch (e) { observe(M, 'bulk', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 8. Survey manual send page ────────────────────────────────────────── test('Survey: manual send form + enhanced report generate', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { // manual send page await page.goto(`${BASE_URL}/surveys/send/`); await page.waitForLoadState('domcontentloaded'); observe(M, 'survey-send-page', 'PASS', 'manual send page loads', { url: page.url() }); // enhanced report generate page await page.goto(`${BASE_URL}/surveys/enhanced-reports/generate/`); await page.waitForLoadState('domcontentloaded'); const renders = (await page.textContent('body') || '').length > 200; observe(M, 'survey-enhanced-report', renders ? 'PASS' : 'WARN', `enhanced report page: ${renders}`, { url: page.url() }); } catch (e) { observe(M, 'survey-extra', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 9. Convert PX Action to QI Project ────────────────────────────────── test('QI Projects: convert from action', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const out = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.px_action_center.models import PXAction from apps.organizations.models import Hospital h = Hospital.objects.get(code='E2E-HOSP') a = PXAction.objects.filter(hospital=h).first() print('AID=' + str(a.id) if a else 'AID=NONE') `); const m = out.match(/AID=(\S+)/); if (!m || m[1] === 'NONE') { observe(M, 'convert-action', 'WARN', 'no action to convert', {}); return; } const aid = m[1]; const r = await postForm(page, `${BASE_URL}/projects/convert-action/${aid}/`, { name: `E2E Converted Project ${Date.now()}`, }); observe(M, 'action-to-project', r.status() < 400 ? 'PASS' : 'WARN', `convert HTTP ${r.status()}`, { http: r.status() }); } catch (e) { observe(M, 'convert', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 10. Complaint status change via detail ────────────────────────────── test('Complaint: change status to closed', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const out = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.complaints.models import Complaint from apps.organizations.models import Hospital h = Hospital.objects.get(code='E2E-HOSP') c = Complaint.objects.filter(hospital=h, status='resolved').first() if c is None: c = Complaint.objects.filter(hospital=h, status='in_progress').first() print('CID=' + str(c.id) + ' ST=' + str(c.status) if c else 'CID=NONE ST=NONE') `); const m = out.match(/CID=(\S+)\s+ST=(\S+)/); if (!m || m[1] === 'NONE') { observe(M, 'close', 'WARN', 'no complaint', {}); return; } const cid = m[1]; const r = await postForm(page, `${BASE_URL}/complaints/${cid}/change-status/`, { status: 'closed', note: 'E2E V4 close test', }); observe(M, 'complaint-close', r.status() < 400 ? 'PASS' : 'WARN', `close HTTP ${r.status()}`, { http: r.status() }); } catch (e) { observe(M, 'close', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 11. Inquiry change status ─────────────────────────────────────────── test('Inquiry: change status', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const out = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.complaints.models import Inquiry from apps.organizations.models import Hospital h = Hospital.objects.get(code='E2E-HOSP') i = Inquiry.objects.filter(hospital=h).first() print('IID=' + str(i.id) if i else 'IID=NONE') `); const m = out.match(/IID=(\S+)/); if (!m || m[1] === 'NONE') { observe(M, 'inq-status', 'WARN', 'no inquiry', {}); return; } const iid = m[1]; const r = await postForm(page, `${BASE_URL}/inquiries/${iid}/change-status/`, { status: 'in_progress', note: 'E2E V4 inquiry status change', }); observe(M, 'inq-status-change', r.status() < 400 ? 'PASS' : 'WARN', `status HTTP ${r.status()}`, { http: r.status() }); } catch (e) { observe(M, 'inq-status', '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 V4 SUMMARY ==========='); console.log('Total observations:', OBS.length, JSON.stringify(counts)); console.log('================================================\n'); });