/* eslint-disable */ /** * QI Projects workflow — cross-department team + task management. * * Tests: * 1. Admin creates a project with team from 2+ departments. * 2. Tasks assigned to each team member. * 3. Team member A (Contact Center) logs in → toggles their task → verified. * 4. Team member B (different dept) logs in → toggles their task → verified cross-dept. * 5. My Tasks view → team member sees their assigned tasks. * 6. Admin exports Excel. * 7. Admin closes project. * * Run headed: * E2E_MAXIMIZED=1 E2E_TIMEOUT=120000 E2E_ACTION_TIMEOUT=12000 \ * npx playwright test --headed --project chromium qi-projects-workflow --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 = 'QIProjects'; const ADMIN: RoleName = 'hospital_admin'; type Page = import('@playwright/test').Page; function uv(args: string): string { return execSync(args, { cwd: PROJECT_ROOT }).toString(); } interface SeedData { projectId: string; taskAId: string; taskBId: string; staffA: string; staffB: string; deptA: string; deptB: string; } function seed(): SeedData { const out = uv('uv run manage.py seed_e2e_project'); const m = out.match(/project_id=(\S+)\s+task_a_id=(\S+)\s+task_b_id=(\S+)\s+staff_a=(\S+)\s+staff_b=(\S+)\s+dept_a=(\S+)\s+dept_b=(\S+)/); if (!m) throw new Error('seed parse failed: ' + out); return { projectId: m[1], taskAId: m[2], taskBId: m[3], staffA: m[4], staffB: m[5], deptA: m[6], deptB: m[7] }; } function projectState(pid: string): Record { const out = uv(`uv run manage.py get_e2e_project_state ${pid}`); const s: Record = {}; for (const line of out.split('\n')) { const i = line.indexOf('='); if (i > 0) s[line.slice(0, i)] = line.slice(i + 1); } return s; } 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 }, }); } async function login(page: Page, role: RoleName) { await page.context().clearCookies(); await loginAndScope(page, role, M); } // Role name by email for the e2e accounts function roleForEmail(email: string): RoleName { if (email.includes('staff')) return 'staff'; if (email.includes('nurse')) return 'nurse'; return 'staff'; } test('QI Projects: cross-dept team members can toggle their tasks', async ({ page }) => { attachObservers(page, M, ADMIN); const s = seed(); observe(M, 'seed', 'INFO', `project ${s.projectId}, tasks A=${s.taskAId.slice(0,8)} B=${s.taskBId.slice(0,8)}`, {}); try { // ── 1. Admin views project + verifies team + tasks ───────────────────── await login(page, ADMIN); await page.goto(`${BASE_URL}/projects/${s.projectId}/`); await page.waitForLoadState('domcontentloaded'); const tb1 = await page.textContent('body').catch(() => ''); const projectLoads = tb1!.includes('E2E QI Project') || !page.url().includes('login'); observe(M, '1-admin-view', projectLoads ? 'PASS' : 'FAIL', `project detail loads for admin`, { role: ADMIN, url: page.url() }); const st0 = projectState(s.projectId); observe(M, '1-state', st0.team_count === '2' ? 'PASS' : 'FAIL', `team_count=${st0.team_count} (want 2)`, { role: ADMIN }); observe(M, '1-tasks', st0.task_count === '2' ? 'PASS' : 'FAIL', `task_count=${st0.task_count} (want 2)`, { role: ADMIN }); // ── 2. Team member A (Contact Center) toggles their task ─────────────── const roleA = roleForEmail(s.staffA); await login(page, roleA); await page.goto(`${BASE_URL}/projects/${s.projectId}/`); await page.waitForLoadState('domcontentloaded'); await page.waitForTimeout(500); const aLoads = !page.url().includes('login'); observe(M, '2-staffA-view', aLoads ? 'PASS' : 'FAIL', `${roleA} can view project (cross-dept OK)`, { role: roleA, url: page.url() }); // toggle task A via POST (the htmx endpoint or standard endpoint) const toggleA = await postForm(page, `${BASE_URL}/projects/${s.projectId}/htmx/tasks/${s.taskAId}/toggle/`, {}); observe(M, '2-staffA-toggle', toggleA.status() < 400 ? 'PASS' : 'FAIL', `staff A toggling their task: HTTP ${toggleA.status()}`, { role: roleA, http: toggleA.status() }); const stA = projectState(s.projectId); const taskAKey = `task_${s.taskAId.slice(0,8)}_status`; observe(M, '2-staffA-state', stA[taskAKey] === 'completed' ? 'PASS' : 'FAIL', `task A status=${stA[taskAKey]} (want completed)`, { role: roleA }); // ── 3. Team member B (different dept) toggles their task ─────────────── const roleB = roleForEmail(s.staffB); await login(page, roleB); await page.goto(`${BASE_URL}/projects/${s.projectId}/`); await page.waitForLoadState('domcontentloaded'); await page.waitForTimeout(500); const bLoads = !page.url().includes('login'); observe(M, '3-staffB-view', bLoads ? 'PASS' : 'FAIL', `${roleB} from a DIFFERENT dept can view project`, { role: roleB, url: page.url() }); const toggleB = await postForm(page, `${BASE_URL}/projects/${s.projectId}/htmx/tasks/${s.taskBId}/toggle/`, {}); observe(M, '3-staffB-toggle', toggleB.status() < 400 ? 'PASS' : 'FAIL', `staff B toggling their task: HTTP ${toggleB.status()}`, { role: roleB, http: toggleB.status() }); const stB = projectState(s.projectId); const taskBKey = `task_${s.taskBId.slice(0,8)}_status`; observe(M, '3-staffB-state', stB[taskBKey] === 'completed' ? 'PASS' : 'FAIL', `task B status=${stB[taskBKey]} (want completed)`, { role: roleB }); // ── 4. My Tasks view ─────────────────────────────────────────────────── await page.goto(`${BASE_URL}/projects/my-tasks/`); await page.waitForLoadState('domcontentloaded'); const myTasksBody = await page.textContent('body').catch(() => ''); const hasMyTasks = !page.url().includes('login') && (myTasksBody!.includes('QI') || myTasksBody!.includes('task') || myTasksBody!.includes('no QI')); observe(M, '4-my-tasks', hasMyTasks ? 'PASS' : 'FAIL', `My Tasks page loads for ${roleB}`, { role: roleB, url: page.url() }); // ── 5. Admin exports Excel ───────────────────────────────────────────── await login(page, ADMIN); const exportResp = await page.context().request.get(`${BASE_URL}/projects/${s.projectId}/export/excel/`); const exportOk = exportResp.status() === 200; const ct = exportResp.headers()['content-type'] || ''; observe(M, '5-export', exportOk ? 'PASS' : 'FAIL', `Excel export: HTTP ${exportResp.status()} ct=${ct}`, { role: ADMIN, http: exportResp.status() }); // ── 6. Admin closes project ──────────────────────────────────────────── // Edit the project status to completed const csrf = await csrfOf(page); await page.context().request.post(`${BASE_URL}/projects/${s.projectId}/edit/`, { maxRedirects: 0, headers: { ...(csrf ? { 'X-CSRFToken': csrf } : {}) }, form: { csrfmiddlewaretoken: csrf, name: `E2E QI Project (closed)`, description: 'Closed by E2E test', hospital: projectState(s.projectId).project_status || '', status: 'completed', start_date: '', target_completion_date: '', outcome_description: 'E2E test completed successfully', }, }).catch(() => {}); const stFinal = projectState(s.projectId); observe(M, '6-close', stFinal.project_status === 'completed' ? 'PASS' : 'WARN', `project_status=${stFinal.project_status} (want completed)`, { role: ADMIN }); observe(M, 'flow-complete', 'PASS', `QI project ${s.projectId}: cross-dept team toggled tasks, My Tasks viewed, Excel exported`, {}); } catch (e) { observe(M, 'flow', '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=========== QI PROJECTS SUMMARY ==========='); console.log('Total observations:', OBS.length, JSON.stringify(counts)); console.log('===========================================\n'); });