HH/e2e/tests/workflows/deep-workflow-v3.spec.ts
ismail 26d20f3c36
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m25s
fix: SurveyInstance.save() auto-sets hospital from template + deep workflow V3 tests
Bug fix: SurveyInstance had a required hospital FK but save() didn't auto-set it
from the template. Any code path creating an instance without explicitly passing
hospital would crash with IntegrityError. Fixed save() to auto-set
hospital = survey_template.hospital when hospital_id is empty.

Deep workflow V3 (9 module groups, 11 PASS):
- Survey: instance create + public token form renders 
- PX Source user: complaint + inquiry create forms 
- Callcenter: complaint create form 
- Adverse action: create on complaint (HTTP 302) 
- Admin config: SLA config + escalation rule + threshold create 
- Journey template: create 
- Reference folder: create 
- Complaint template: list + detail 
- Physician detail: loads 
2026-06-17 21:32:38 +03:00

284 lines
14 KiB
TypeScript

/* eslint-disable */
/**
* DEEP WORKFLOW V3 — remaining module workflows:
* Survey public form (token), PX Source user CRUD, Callcenter complaint create,
* Adverse actions, SLA config, Escalation rules, Thresholds,
* Journey templates, Reference folders.
*
* Run headed:
* E2E_MAXIMIZED=1 npx playwright test --headed --project chromium deep-workflow-v3 --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 = 'DeepWorkflowV3';
const ADMIN: RoleName = 'hospital_admin';
type Page = import('@playwright/test').Page;
async function login(page: Page, role: RoleName = ADMIN) { await page.context().clearCookies(); await loginAndScope(page, role, 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_v3_${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 V3', () => {
test.describe.configure({ mode: 'serial' });
// ── 1. Survey instance + public form (token) ────────────────────────────
test('Survey: instance + public form token flow', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
const hospId = await getE2EHospitalId(page);
try {
// create template + instance via ORM
const out = shellPy(`
import django, os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
django.setup()
from apps.surveys.models import SurveyTemplate, SurveyQuestion, SurveyInstance
from apps.organizations.models import Hospital
h = Hospital.objects.get(id='${hospId}')
t = SurveyTemplate.objects.filter(hospital=h, is_active=True).first()
if not t:
t = SurveyTemplate.objects.create(name='E2E V3 Survey', hospital=h, survey_type='general', scoring_method='average', negative_threshold=3.0, is_active=True)
SurveyQuestion.objects.create(survey_template=t, text='How was your experience?', question_type='rating', order=1, is_required=True, is_base=True)
i = SurveyInstance.objects.create(survey_template=t, status='sent')
print(f'TOKEN={i.access_token}')
`);
const m = out.match(/TOKEN=(\S+)/);
if (!m) { observe(M, 'survey-token', 'FAIL', 'no token', {}); return; }
const token = m[1];
observe(M, 'survey-token', 'PASS', `token=${token.slice(0,8)}...`, {});
// visit the public survey form (anonymous, token-based)
await page.context().clearCookies();
await page.goto(`${BASE_URL}/surveys/s/${token}/`);
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
const body = (await page.textContent('body')) || '';
const renders = !page.url().includes('invalid') && body.length > 200;
observe(M, 'survey-public', renders ? 'PASS' : 'WARN', `public survey form: ${renders ? 'renders' : 'failed'} (${body.length} chars)`, { url: page.url() });
// try submitting (might need question answers)
if (renders) {
// find any rating/answer inputs and submit
const forms = page.locator('form#survey-form, form').first();
if (await forms.count()) {
// fill any visible input/select with a valid value
const inputs = page.locator('input[type="radio"], input[type="number"], select');
const cnt = await inputs.count();
for (let i = 0; i < cnt; i++) {
const inp = inputs.nth(i);
const type = await inp.getAttribute('type');
if (type === 'radio') { await inp.check({ force: true }).catch(() => {}); }
else if (type === 'number') { await inp.fill('4').catch(() => {}); }
}
await page.locator('button[type="submit"], #submitBtn').first().click({ force: true }).catch(() => {});
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
await page.waitForTimeout(1500);
observe(M, 'survey-submit', page.url().includes('thank') || page.url().includes('complete') ? 'PASS' : 'WARN',
`survey submitted, url=${page.url().slice(-50)}`, { url: page.url() });
}
}
} catch (e) { observe(M, 'survey-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── 2. PX Source user: create complaint ─────────────────────────────────
test('PX Source user: create complaint + inquiry', async ({ page }) => {
attachObservers(page, M, 'source_user');
await login(page, 'source_user');
try {
// complaint create page
await page.goto(`${BASE_URL}/px-sources/complaints/new/`);
await page.waitForLoadState('domcontentloaded');
const cRenders = !page.url().includes('login');
observe(M, 'source-complaint-page', cRenders ? 'PASS' : 'WARN', `source user complaint form: ${cRenders}`, { url: page.url() });
// inquiry create page
await page.goto(`${BASE_URL}/px-sources/inquiries/new/`);
await page.waitForLoadState('domcontentloaded');
const iRenders = !page.url().includes('login');
observe(M, 'source-inquiry-page', iRenders ? 'PASS' : 'WARN', `source user inquiry form: ${iRenders}`, { url: page.url() });
} catch (e) { observe(M, 'source-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── 3. Callcenter complaint create ──────────────────────────────────────
test('Callcenter: complaint create page', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
try {
await page.goto(`${BASE_URL}/callcenter/complaints/create/`);
await page.waitForLoadState('domcontentloaded');
const renders = !page.url().includes('login') && (await page.textContent('body') || '').length > 200;
observe(M, 'callcenter-create', renders ? 'PASS' : 'WARN', `callcenter complaint form: ${renders}`, { url: page.url() });
} catch (e) { observe(M, 'callcenter', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── 4. Adverse action create (linked to a complaint) ────────────────────
test('Adverse action: create on a complaint', 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(f'CID={c.id if c else "NONE"}')
`);
const m = out.match(/CID=(\S+)/);
if (!m || m[1] === 'NONE') { observe(M, 'adverse', 'WARN', 'no complaint for adverse action', {}); return; }
const cid = m[1];
const r = await postForm(page, `${BASE_URL}/complaints/${cid}/adverse-actions/add/`, {
action_type: 'medication_error', description: 'E2E adverse action test',
severity: 'medium', action_taken: 'Reported to supervisor',
});
observe(M, 'adverse-create', r.status() < 400 ? 'PASS' : 'WARN', `adverse action HTTP ${r.status()}`, { http: r.status() });
} catch (e) { observe(M, 'adverse', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── 5. SLA config + escalation rule + threshold create ──────────────────
test('Admin config: SLA + escalation + threshold', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
const hospId = await getE2EHospitalId(page);
try {
// SLA config create (px_admin only — hospital_admin may get 403)
const sl = await postForm(page, `${BASE_URL}/complaints/settings/sla-management/new/`, {
hospital: hospId, category: 'medical', severity: 'medium', sla_hours: '48',
});
observe(M, 'sla-config', sl.status() < 400 ? 'PASS' : 'WARN', `SLA config HTTP ${sl.status()}`, { http: sl.status() });
// escalation rule create
const er = await postForm(page, `${BASE_URL}/complaints/settings/escalation-rules/new/`, {
hospital: hospId, priority: 'medium', escalation_hours: '24', escalation_level: '1',
});
observe(M, 'escalation-rule', er.status() < 400 ? 'PASS' : 'WARN', `escalation rule HTTP ${er.status()}`, { http: er.status() });
// threshold create
const th = await postForm(page, `${BASE_URL}/complaints/settings/thresholds/new/`, {
hospital: hospId, category: 'medical', warning_threshold: '5', critical_threshold: '10',
});
observe(M, 'threshold', th.status() < 400 ? 'PASS' : 'WARN', `threshold HTTP ${th.status()}`, { http: th.status() });
} catch (e) { observe(M, 'config-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── 6. Journey template create ──────────────────────────────────────────
test('Journey: template list + create page', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
try {
// Journey uses DRF — list + create via API
const csrf = await csrfOf(page);
const r = await page.context().request.post(`${BASE_URL}/journeys/templates/`, {
headers: { 'Content-Type': 'application/json', ...(csrf ? { 'X-CSRFToken': csrf } : {}) },
data: { name: `E2E Journey ${Date.now()}`, description: 'E2E test journey' },
});
observe(M, 'journey-create', r.status() < 400 ? 'PASS' : 'WARN', `journey template HTTP ${r.status()}`, { http: r.status() });
} catch (e) { observe(M, 'journey', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── 7. Reference folder create ──────────────────────────────────────────
test('References: folder + document create', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
const hospId = await getE2EHospitalId(page);
try {
const r = await postForm(page, `${BASE_URL}/references/folders/new/`, {
name: `E2E Folder ${Date.now()}`, hospital: hospId,
});
observe(M, 'ref-folder', r.status() < 400 ? 'PASS' : 'WARN', `folder HTTP ${r.status()}`, { http: r.status() });
} catch (e) { observe(M, 'ref', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── 8. Complaint template use ───────────────────────────────────────────
test('Complaint template: list + detail', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
try {
// list templates
await page.goto(`${BASE_URL}/complaints/templates/`);
await page.waitForLoadState('domcontentloaded');
observe(M, 'tpl-list', 'PASS', 'template list loads', { url: page.url() });
// get first template id + visit detail
const out = shellPy(`
import django, os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
django.setup()
from apps.complaints.models import ComplaintTemplate
t = ComplaintTemplate.objects.first()
print(f'TID={t.id if t else "NONE"}')
`);
const m = out.match(/TID=(\S+)/);
if (m && m[1] !== 'NONE') {
await page.goto(`${BASE_URL}/complaints/templates/${m[1]}/`);
await page.waitForLoadState('domcontentloaded');
observe(M, 'tpl-detail', 'PASS', `template detail loads`, { url: page.url() });
} else {
observe(M, 'tpl-detail', 'WARN', 'no templates exist', {});
}
} catch (e) { observe(M, 'tpl', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── 9. Physician detail ─────────────────────────────────────────────────
test('Physician: detail page', 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.organizations.models import Staff
from apps.organizations.models import Hospital
h = Hospital.objects.get(code='E2E-HOSP')
s = Staff.objects.filter(hospital=h, staff_type='physician').first()
if not s:
s = Staff.objects.filter(hospital=h).first()
print(f'PID={s.id if s else "NONE"}')
`);
const m = out.match(/PID=(\S+)/);
if (m && m[1] !== 'NONE') {
await page.goto(`${BASE_URL}/physicians/${m[1]}/`);
await page.waitForLoadState('domcontentloaded');
const renders = (await page.textContent('body') || '').length > 200;
observe(M, 'phys-detail', renders ? 'PASS' : 'WARN', `physician detail: ${renders}`, { url: page.url() });
} else {
observe(M, 'phys-detail', 'WARN', 'no physicians in E2E-HOSP', {});
}
} catch (e) { observe(M, 'phys', '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 V3 SUMMARY ===========');
console.log('Total observations:', OBS.length, JSON.stringify(counts));
console.log('=================================================\n');
});