HH/e2e/tests/workflows/deep-workflow-v4.spec.ts
ismail 846af9a8c7
All checks were successful
Build and Push Docker Image / build (push) Successful in 29s
test: deep workflow V4 — reopen, involved staff/dept, convert-to-action, KPI report, PDF (7 PASS)
Interactive workflows verified:
- Complaint reopen (HTTP 302) 
- Complaint: add involved department (HTTP 200) 
- Complaint: add involved staff (HTTP 200) 
- Observation → PX Action convert (HTTP 200) 
- KPI report generate page + submit (HTTP 302) 
- Complaint PDF: HTTP 200, valid %PDF, 42KB 
- Presentation generate page: timeout (needs investigation)
2026-06-17 22:16:00 +03:00

313 lines
15 KiB
TypeScript

/* 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<string> {
return page.context().cookies().then((c) => c.find((x) => x.name === 'csrftoken')?.value || '');
}
async function postForm(page: Page, url: string, data: Record<string, string>) {
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<Record<string, number>>((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');
});