test: deep workflow V4 — reopen, involved staff/dept, convert-to-action, KPI report, PDF (7 PASS)
All checks were successful
Build and Push Docker Image / build (push) Successful in 29s
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)
This commit is contained in:
parent
26d20f3c36
commit
846af9a8c7
312
e2e/tests/workflows/deep-workflow-v4.spec.ts
Normal file
312
e2e/tests/workflows/deep-workflow-v4.spec.ts
Normal file
@ -0,0 +1,312 @@
|
||||
/* 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');
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user