diff --git a/e2e/tests/workflows/deep-workflow-v5.spec.ts b/e2e/tests/workflows/deep-workflow-v5.spec.ts new file mode 100644 index 0000000..0d45ab1 --- /dev/null +++ b/e2e/tests/workflows/deep-workflow-v5.spec.ts @@ -0,0 +1,238 @@ +/* eslint-disable */ +/** + * DEEP WORKFLOW V5 — exports, source user creation, OVR, satisfaction, + * appreciation leaderboard/badges, government ticket import, presentation generate. + * + * Run headed: + * E2E_MAXIMIZED=1 npx playwright test --headed --project chromium deep-workflow-v5 --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 = 'DeepWorkflowV5'; +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 { + return page.context().cookies().then((c) => c.find((x) => x.name === 'csrftoken')?.value || ''); +} +async function postForm(page: Page, url: string, data: Record) { + 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_v5_${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 V5', () => { + test.describe.configure({ mode: 'serial' }); + + // ── 1. Complaint CSV + Excel export ───────────────────────────────────── + test('Complaint exports: CSV + Excel', async ({ page }) => { + attachObservers(page, M, ADMIN); + await login(page); + try { + const csv = await page.context().request.get(`${BASE_URL}/complaints/export/csv/`); + const csvOk = csv.status() === 200 && (csv.headers()['content-type'] || '').includes('csv'); + observe(M, 'complaint-csv', csvOk ? 'PASS' : 'FAIL', `CSV HTTP ${csv.status()} ct=${csv.headers()['content-type']}`, { http: csv.status() }); + + const xls = await page.context().request.get(`${BASE_URL}/complaints/export/excel/`); + const xlsCt = xls.headers()['content-type'] || ''; + const xlsOk = xls.status() === 200 && (xlsCt.includes('spreadsheet') || xlsCt.includes('excel') || xlsCt.includes('octet')); + observe(M, 'complaint-excel', xlsOk ? 'PASS' : 'FAIL', `Excel HTTP ${xls.status()} ct=${xlsCt}`, { http: xls.status() }); + } catch (e) { observe(M, 'complaint-exports', 'FAIL', `exception: ${(e as Error).message}`, {}); } + }); + + // ── 2. Inquiry exports ────────────────────────────────────────────────── + test('Inquiry exports: incoming + outgoing', async ({ page }) => { + attachObservers(page, M, ADMIN); + await login(page); + try { + const inc = await page.context().request.get(`${BASE_URL}/inquiries/export/incoming/`); + observe(M, 'inq-export-incoming', inc.status() < 400 ? 'PASS' : 'WARN', `incoming HTTP ${inc.status()}`, { http: inc.status() }); + const out = await page.context().request.get(`${BASE_URL}/inquiries/export/outgoing/`); + observe(M, 'inq-export-outgoing', out.status() < 400 ? 'PASS' : 'WARN', `outgoing HTTP ${out.status()}`, { http: out.status() }); + } catch (e) { observe(M, 'inq-exports', 'FAIL', `exception: ${(e as Error).message}`, {}); } + }); + + // ── 3. Complaint update satisfaction ───────────────────────────────────── + test('Complaint: update satisfaction', 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, 'satisfaction', 'WARN', 'no complaint', {}); return; } + const r = await postForm(page, `${BASE_URL}/complaints/${m[1]}/update-satisfaction/`, { satisfaction: 'satisfied' }); + observe(M, 'complaint-satisfaction', r.status() < 400 ? 'PASS' : 'WARN', `satisfaction HTTP ${r.status()}`, { http: r.status() }); + } catch (e) { observe(M, 'satisfaction', 'FAIL', `exception: ${(e as Error).message}`, {}); } + }); + + // ── 4. Appreciation: leaderboard + badges ─────────────────────────────── + test('Appreciation: leaderboard + my badges + admin badges', async ({ page }) => { + attachObservers(page, M, ADMIN); + await login(page); + try { + await page.goto(`${BASE_URL}/appreciation/leaderboard/`); + await page.waitForLoadState('domcontentloaded'); + observe(M, 'apr-leaderboard', 'PASS', 'leaderboard page loads', { url: page.url() }); + + await page.goto(`${BASE_URL}/appreciation/badges/`); + await page.waitForLoadState('domcontentloaded'); + observe(M, 'apr-my-badges', 'PASS', 'my badges page loads', { url: page.url() }); + + await page.goto(`${BASE_URL}/appreciation/admin/badges/`); + await page.waitForLoadState('domcontentloaded'); + observe(M, 'apr-admin-badges', 'PASS', 'admin badges page loads', { url: page.url() }); + } catch (e) { observe(M, 'apr-extra', 'FAIL', `exception: ${(e as Error).message}`, {}); } + }); + + // ── 5. Presentation generate (with longer timeout) ────────────────────── + test('Presentations: generate page + create', async ({ page }) => { + attachObservers(page, M, ADMIN); + await login(page); + const hospId = await getE2EHospitalId(page); + try { + // generate page (may take a while rendering charts) + await page.goto(`${BASE_URL}/presentations/generate/`, { timeout: 30000 }).catch(() => {}); + await page.waitForLoadState('domcontentloaded').catch(() => {}); + await page.waitForTimeout(1000); + const body = (await page.textContent('body').catch(() => '')) || ''; + const renders = body.length > 200; + observe(M, 'pres-gen-page', renders ? 'PASS' : 'WARN', `generate page: ${renders} (${body.length} chars)`, { url: page.url() }); + + // try POST to generate (may timeout on data processing — that's OK) + if (renders) { + const r = await postForm(page, `${BASE_URL}/presentations/generate/`, { + hospital: hospId, year: '2026', report_type: 'complaints', + }); + observe(M, 'pres-generate', r.status() < 500 ? 'PASS' : 'WARN', `generate POST HTTP ${r.status()}`, { http: r.status() }); + } + } catch (e) { observe(M, 'pres', 'FAIL', `exception: ${(e as Error).message}`, {}); } + }); + + // ── 6. Government ticket import page ──────────────────────────────────── + test('Government ticket import page', async ({ page }) => { + attachObservers(page, M, ADMIN); + await login(page); + try { + await page.goto(`${BASE_URL}/complaints/government-tickets/import/`); + await page.waitForLoadState('domcontentloaded'); + const renders = (await page.textContent('body') || '').length > 200; + observe(M, 'gov-import', renders ? 'PASS' : 'WARN', `import page: ${renders}`, { url: page.url() }); + } catch (e) { observe(M, 'gov-import', 'FAIL', `exception: ${(e as Error).message}`, {}); } + }); + + // ── 7. Complaint OVR toggle ───────────────────────────────────────────── + test('Complaint: OVR toggle', 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, 'ovr', 'WARN', 'no complaint', {}); return; } + const cid = m[1]; + const r = await postForm(page, `${BASE_URL}/complaints/${cid}/toggle-escalated-ovr/`, {}); + observe(M, 'ovr-toggle', r.status() < 400 ? 'PASS' : 'WARN', `OVR toggle HTTP ${r.status()}`, { http: r.status() }); + } catch (e) { observe(M, 'ovr', 'FAIL', `exception: ${(e as Error).message}`, {}); } + }); + + // ── 8. Complaint analytics page ───────────────────────────────────────── + test('Complaint analytics page', async ({ page }) => { + attachObservers(page, M, ADMIN); + await login(page); + try { + await page.goto(`${BASE_URL}/complaints/analytics/`); + await page.waitForLoadState('domcontentloaded'); + const body = (await page.textContent('body')) || ''; + const hasCharts = await page.locator('canvas, svg, .apexcharts-canvas, [id*="chart"]').count() > 0; + observe(M, 'complaint-analytics', body.length > 500 ? 'PASS' : 'WARN', `analytics: ${body.length} chars, charts=${hasCharts}`, { url: page.url() }); + } catch (e) { observe(M, 'analytics', 'FAIL', `exception: ${(e as Error).message}`, {}); } + }); + + // ── 9. Source user: actually create a complaint ───────────────────────── + test('PX Source user: create complaint', async ({ page }) => { + attachObservers(page, M, 'source_user'); + await login(page, 'source_user'); + const hospId = await getE2EHospitalId(page); + try { + const out = shellPy(` +import django, os +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') +django.setup() +from apps.organizations.models import Department +from apps.organizations.models import Hospital +h = Hospital.objects.get(code='E2E-HOSP') +d = Department.objects.filter(hospital=h).first() +print('DID=' + str(d.id)) +`); + const m = out.match(/DID=(\S+)/); + const did = m ? m[1] : ''; + const r = await postForm(page, `${BASE_URL}/px-sources/complaints/new/`, { + complainant_name: `E2E Source ${Date.now()}`, + mobile_number: '0551234567', patient_name: 'E2E Patient', + national_id: `E2E-SRC-${Date.now()}`, incident_date: '2026-06-17', + hospital: hospId, department: did, + complaint_details: 'E2E source user complaint test', + severity: 'medium', priority: 'medium', + }); + observe(M, 'source-complaint-create', r.status() < 400 ? 'PASS' : 'WARN', `source complaint HTTP ${r.status()}`, { http: r.status() }); + } catch (e) { observe(M, 'source-create', 'FAIL', `exception: ${(e as Error).message}`, {}); } + }); + + // ── 10. Appreciation category create ──────────────────────────────────── + test('Appreciation: admin category create', async ({ page }) => { + attachObservers(page, M, ADMIN); + await login(page); + const hospId = await getE2EHospitalId(page); + try { + const r = await postForm(page, `${BASE_URL}/appreciation/admin/categories/create/`, { + code: `E2E-CAT-${Date.now().toString().slice(-6)}`, + name_en: 'E2E Test Category', name_ar: 'E2E', + hospital: hospId, is_active: 'on', + }); + observe(M, 'apr-category', r.status() < 400 ? 'PASS' : 'WARN', `category create HTTP ${r.status()}`, { http: r.status() }); + } catch (e) { observe(M, 'apr-cat', 'FAIL', `exception: ${(e as Error).message}`, {}); } + }); +}); + +test.afterAll(async () => { + const counts = OBS.reduce>((a, o) => ((a[o.status] = (a[o.status] || 0) + 1), a), {}); + console.log('\n=========== DEEP WORKFLOW V5 SUMMARY ==========='); + console.log('Total observations:', OBS.length, JSON.stringify(counts)); + console.log('=================================================\n'); +});