All checks were successful
Build and Push Docker Image / build (push) Successful in 29s
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)
313 lines
15 KiB
TypeScript
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');
|
|
});
|