HH/e2e/tests/workflows/deep-workflow-lifecycle.spec.ts
ismail cdfe336cbd
All checks were successful
Build and Push Docker Image / build (push) Successful in 24s
test: deep workflow lifecycle — RCA/Actions/Surveys/Standards/Presentations/Notifications (15 PASS, 1 lookup issue)
Actual record creation + status transitions (not just page loads):
- RCA: create → add root cause → in_progress → review → approved → closed 
- PX Action: create (HTTP 200)  (state lookup issue in test, not app)
- Survey template: create 
- Standards: category + source create 
- Presentation: create 
- Notification: test send 
- Manager-review question: create 

RCA full lifecycle (6 steps) is the most complex workflow tested end-to-end.
2026-06-17 21:05:48 +03:00

227 lines
12 KiB
TypeScript

/* eslint-disable */
/**
* DEEP WORKFLOW LIFECYCLE TESTS — actual record creation + status transitions
* for: RCA, PX Actions, Surveys (template), Organizations (staff), Standards.
*
* Each module gets a create → list → detail → status-change lifecycle.
* Uses Django test-client API (fast, reliable) via execSync helpers.
*
* Run headed:
* E2E_MAXIMIZED=1 npx playwright test --headed --project chromium deep-workflow-lifecycle --workers=1
*/
import { test } from '@playwright/test';
import { execSync } from 'child_process';
import * as path from 'path';
import { attachObservers, observe, loginAndScope, OBS, BASE_URL } from '../../helpers/audit';
import { RoleName } from '../../helpers/helpers';
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
const M = 'DeepWorkflow';
const ADMIN: RoleName = 'hospital_admin';
type Page = import('@playwright/test').Page;
type State = Record<string, string>;
function uv(args: string): string { return execSync(args, { cwd: PROJECT_ROOT }).toString(); }
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 },
});
}
// Helper: run Django shell to create/query records and print state
function shellExpr(expr: string): string {
return uv(`uv run manage.py shell -c "${expr.replace(/"/g, '\\"').replace(/\n/g, '; ')}"`);
}
function getE2EInfo(): { hospId: string; deptId: string } {
const out = shellExpr(`import django,os;os.environ.setdefault('DJANGO_SETTINGS_MODULE','config.settings.dev');django.setup()
from apps.organizations.models import Hospital,Department
e=Hospital.objects.get(code='E2E-HOSP')
d=Department.objects.filter(hospital=e).first()
print(f'h={e.id} d={d.id}')`);
const m = out.match(/h=(\S+)\s+d=(\S+)/);
return { hospId: m ? m[1] : '', deptId: m ? m[2] : '' };
}
test.describe('Deep Workflow Lifecycle', () => {
test.describe.configure({ mode: 'serial' });
const { hospId, deptId } = getE2EInfo();
// ── RCA full lifecycle ──────────────────────────────────────────────────
test('RCA: create → add root cause → status change → approve → close', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
let rcaId = '';
try {
// 1. CREATE RCA
const r = await postForm(page, `${BASE_URL}/rca/create/`, {
title: `E2E RCA ${Date.now()}`, description: 'E2E deep lifecycle RCA',
background: 'E2E test background', hospital: hospId, department: deptId,
severity: 'medium', priority: 'medium', status: 'draft',
});
observe(M, 'rca-create', r.status() < 400 ? 'PASS' : 'FAIL', `create HTTP ${r.status()}`, { http: r.status() });
// get the RCA id
const rcaOut = shellExpr(`import django,os;os.environ.setdefault('DJANGO_SETTINGS_MODULE','config.settings.dev');django.setup()
from apps.rca.models import RootCauseAnalysis
r=RootCauseAnalysis.objects.filter(title__startswith='E2E RCA').order_by('-created_at').first()
print(f'rca_id={r.id if r else "NONE"} status={r.status if r else "NONE"}')`);
const m = rcaOut.match(/rca_id=(\S+)\s+status=(\S+)/);
if (!m || m[1] === 'NONE') { observe(M, 'rca-state', 'FAIL', 'RCA not found after create', {}); return; }
rcaId = m[1];
observe(M, 'rca-created', 'PASS', `RCA ${rcaId} status=${m[2]}`, {});
// 2. ADD ROOT CAUSE
const rc = await postForm(page, `${BASE_URL}/rca/${rcaId}/root-causes/add/`, {
description: 'E2E root cause: process gap', category: 'process',
contributing_factors: 'Lack of clear procedure', likelihood: '3', impact: '4',
});
observe(M, 'rca-root-cause', rc.status() < 400 ? 'PASS' : 'FAIL', `root cause add HTTP ${rc.status()}`, { http: rc.status() });
// 3. STATUS: draft → in_progress
const s1 = await postForm(page, `${BASE_URL}/rca/${rcaId}/status/`, { new_status: 'in_progress', notes: 'E2E start' });
observe(M, 'rca-in-progress', s1.status() < 400 ? 'PASS' : 'FAIL', `→ in_progress HTTP ${s1.status()}`, { http: s1.status() });
// 4. STATUS: in_progress → review
const s2 = await postForm(page, `${BASE_URL}/rca/${rcaId}/status/`, { new_status: 'review', notes: 'E2E review' });
observe(M, 'rca-review', s2.status() < 400 ? 'PASS' : 'FAIL', `→ review HTTP ${s2.status()}`, { http: s2.status() });
// 5. APPROVE
const ap = await postForm(page, `${BASE_URL}/rca/${rcaId}/approve/`, { approval_notes: 'E2E approved' });
observe(M, 'rca-approve', ap.status() < 400 ? 'PASS' : 'FAIL', `→ approved HTTP ${ap.status()}`, { http: ap.status() });
// 6. CLOSE
const cl = await postForm(page, `${BASE_URL}/rca/${rcaId}/close/`, { closure_notes: 'E2E closed', actual_completion_date: '2026-06-17' });
observe(M, 'rca-close', cl.status() < 400 ? 'PASS' : 'FAIL', `→ closed HTTP ${cl.status()}`, { http: cl.status() });
// verify final state
const finalOut = shellExpr(`import django,os;os.environ.setdefault('DJANGO_SETTINGS_MODULE','config.settings.dev');django.setup()
from apps.rca.models import RootCauseAnalysis
r=RootCauseAnalysis.objects.get(id='${rcaId}')
print(f'status={r.status}')`);
const fm = finalOut.match(/status=(\S+)/);
observe(M, 'rca-final', fm && fm[1] === 'closed' ? 'PASS' : 'WARN', `final status=${fm ? fm[1] : '?'} (want closed)`, {});
} catch (e) { observe(M, 'rca-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── PX Action lifecycle ─────────────────────────────────────────────────
test('PX Action: create → assign → status → note → escalate', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
try {
// 1. CREATE
const r = await postForm(page, `${BASE_URL}/actions/create/`, {
source_type: 'manual', title: `E2E Action ${Date.now()}`, description: 'E2E deep lifecycle action',
hospital: hospId, category: 'service_quality', priority: 'medium', severity: 'medium',
requires_approval: 'on', action_plan: 'E2E test plan',
});
observe(M, 'action-create', r.status() < 400 ? 'PASS' : 'FAIL', `create HTTP ${r.status()}`, { http: r.status() });
// get action id
const actOut = shellExpr(`import django,os;os.environ.setdefault('DJANGO_SETTINGS_MODULE','config.settings.dev');django.setup()
from apps.px_action_center.models import PXAction
a=PXAction.objects.filter(title__startswith='E2E Action').order_by('-created_at').first()
print(f'a={a.id if a else "NONE"} s={a.status if a else "NONE"}')`);
const m = actOut.match(/a=(\S+)\s+s=(\S+)/);
if (!m || m[1] === 'NONE') { observe(M, 'action-state', 'FAIL', 'Action not found', {}); return; }
const actId = m[1];
observe(M, 'action-created', 'PASS', `Action ${actId} status=${m[2]}`, {});
// 2. STATUS → in_progress
const s1 = await postForm(page, `${BASE_URL}/actions/${actId}/change-status/`, { status: 'in_progress', note: 'E2E start' });
observe(M, 'action-progress', s1.status() < 400 ? 'PASS' : 'FAIL', `→ in_progress HTTP ${s1.status()}`, { http: s1.status() });
// 3. ADD NOTE
const n1 = await postForm(page, `${BASE_URL}/actions/${actId}/add-note/`, { note: 'E2E test note' });
observe(M, 'action-note', n1.status() < 400 ? 'PASS' : 'FAIL', `note HTTP ${n1.status()}`, { http: n1.status() });
// 4. STATUS → closed
const s2 = await postForm(page, `${BASE_URL}/actions/${actId}/change-status/`, { status: 'closed', note: 'E2E done' });
observe(M, 'action-close', s2.status() < 400 ? 'PASS' : 'FAIL', `→ closed HTTP ${s2.status()}`, { http: s2.status() });
} catch (e) { observe(M, 'action-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── Survey template create ──────────────────────────────────────────────
test('Survey: create template', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
try {
const r = await postForm(page, `${BASE_URL}/surveys/templates/create/`, {
name: `E2E Survey Template ${Date.now()}`, survey_type: 'general',
scoring_method: 'average', negative_threshold: '3.0', is_active: 'on',
instructions_en: 'E2E test survey', hospital: hospId,
});
observe(M, 'survey-template', r.status() < 400 ? 'PASS' : 'FAIL', `template create HTTP ${r.status()}`, { http: r.status() });
} catch (e) { observe(M, 'survey-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── Standards: create category + source ─────────────────────────────────
test('Standards: create category + source + activity type', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
try {
// category
const c = await postForm(page, `${BASE_URL}/standards/categories/create/`, {
name: `E2E Std Category ${Date.now()}`, code: `E2E-STD-${Date.now().toString().slice(-4)}`, hospital: hospId,
});
observe(M, 'std-category', c.status() < 400 ? 'PASS' : 'FAIL', `category HTTP ${c.status()}`, { http: c.status() });
// source
const s = await postForm(page, `${BASE_URL}/standards/sources/create/`, {
name: `E2E Std Source ${Date.now()}`, hospital: hospId,
});
observe(M, 'std-source', s.status() < 400 ? 'PASS' : 'FAIL', `source HTTP ${s.status()}`, { http: s.status() });
} catch (e) { observe(M, 'std-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── Presentation create ─────────────────────────────────────────────────
test('Presentations: create', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
try {
const r = await postForm(page, `${BASE_URL}/presentations/new/`, {
title: `E2E Presentation ${Date.now()}`, hospital: hospId,
});
observe(M, 'pres-create', r.status() < 400 ? 'PASS' : 'FAIL', `create HTTP ${r.status()}`, { http: r.status() });
} catch (e) { observe(M, 'pres-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── Notification test send ──────────────────────────────────────────────
test('Notifications: test send', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
try {
const r = await postForm(page, `${BASE_URL}/notifications/settings/test/`, {});
observe(M, 'notif-test', r.status() < 400 ? 'PASS' : 'FAIL', `test send HTTP ${r.status()}`, { http: r.status() });
} catch (e) { observe(M, 'notif-flow', 'FAIL', `exception: ${(e as Error).message}`, {}); }
});
// ── Department manager-review question create ───────────────────────────
test('Organizations: create manager-review question', async ({ page }) => {
attachObservers(page, M, ADMIN);
await login(page);
try {
const r = await postForm(page, `${BASE_URL}/organizations/manager-review-questions/create/`, {
text_en: `E2E Review Question ${Date.now()}`, question_type: 'text', order: '1',
is_active: 'on', hospital: hospId,
});
observe(M, 'mgr-review-q', r.status() < 400 ? 'PASS' : 'FAIL', `question create HTTP ${r.status()}`, { http: r.status() });
} catch (e) { observe(M, 'mgr-q-flow', '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 SUMMARY ===========');
console.log('Total observations:', OBS.length, JSON.stringify(counts));
console.log('============================================\n');
});