/* eslint-disable */ /** * Reports + KPIs / Analytics — page-load + data-render + export tests. * * Tests the read-heavy reporting layer: analytics dashboard (charts/KPIs), * command center, report builder, saved reports, exports. Verifies pages * render without 500s and show real data from the E2E-HOSP sandbox. * * Run headed: * E2E_MAXIMIZED=1 E2E_TIMEOUT=120000 E2E_ACTION_TIMEOUT=12000 \ * npx playwright test --headed --project chromium reports-kpis-workflow --workers=1 */ import { test } from '@playwright/test'; import { attachObservers, observe, bodyHasTraceback, loginAndScope, OBS, BASE_URL } from '../../helpers/audit'; import { RoleName } from '../../helpers/helpers'; const M = 'ReportsKPIs'; const ADMIN: RoleName = 'hospital_admin'; type Page = import('@playwright/test').Page; async function login(page: Page, role: RoleName) { await page.context().clearCookies(); await loginAndScope(page, role, M); } /** Visit a URL, check it loads (no login redirect, no traceback), report findings. */ async function visitPage(page: Page, url: string, label: string, role: RoleName, checks?: (p: Page) => Promise) { attachObservers(page, M, role); try { await page.goto(`${BASE_URL}${url}`); await page.waitForLoadState('domcontentloaded'); await page.waitForTimeout(1000); const tb = await bodyHasTraceback(page); const onLogin = page.url().includes('/accounts/login/'); const body = (await page.textContent('body')) || ''; const hasData = body.length > 500 && !body.includes('No data') && !body.includes('no records'); if (tb) { observe(M, label, 'FAIL', `traceback: ${tb}`, { role, url: page.url() }); } else if (onLogin) { observe(M, label, 'WARN', `redirected to login`, { role, url: page.url() }); } else { observe(M, label, 'PASS', `loaded (${body.length} chars)${hasData ? ' + has data' : ' (minimal data)'}`, { role, url: page.url() }); } if (checks && !tb && !onLogin) await checks(page); } catch (e) { observe(M, label, 'FAIL', `exception: ${(e as Error).message}`, { role, url }); } } test.describe('Reports + KPIs / Analytics', () => { test.describe.configure({ mode: 'serial' }); test('Analytics dashboard + command center + KPIs', async ({ page }) => { await login(page, ADMIN); // 1. Analytics Dashboard await visitPage(page, '/analytics/dashboard/', 'analytics-dashboard', ADMIN, async (p) => { const body = (await p.textContent('body')) || ''; const hasCharts = await p.locator('canvas, svg, .chart, .apexcharts-canvas, [id*="chart"]').count() > 0; observe(M, 'dashboard-charts', hasCharts ? 'PASS' : 'WARN', `chart elements: ${hasCharts}`, { role: ADMIN }); const hasCounts = /complaint|inquiry|observation/i.test(body); observe(M, 'dashboard-counts', hasCounts ? 'PASS' : 'WARN', `mentions complaint/inquiry/observation: ${hasCounts}`, { role: ADMIN }); }); // 2. KPI List await visitPage(page, '/analytics/kpis/', 'kpi-list', ADMIN, async (p) => { const body = (await p.textContent('body')) || ''; const kpiCount = await p.locator('table tbody tr, .kpi-card, [class*="kpi"]').count(); observe(M, 'kpi-count', kpiCount > 0 ? 'PASS' : 'WARN', `KPI rows/cards: ${kpiCount}${kpiCount === 0 ? ' (no KPIs seeded)' : ''}`, { role: ADMIN }); }); // 3. Command Center await visitPage(page, '/', 'command-center', ADMIN, async (p) => { const body = (await p.textContent('body')) || ''; const hasCards = await p.locator('.card, .rounded-2xl, .bg-white, [class*="stat"]').count(); observe(M, 'command-center-cards', hasCards > 0 ? 'PASS' : 'WARN', `overview cards: ${hasCards}`, { role: ADMIN }); }); // 4. KPI Reports list await visitPage(page, '/analytics/kpi-reports/', 'kpi-reports', ADMIN, async (p) => { const reportCount = await p.locator('table tbody tr, .report-card, [class*="report"]').count(); observe(M, 'kpi-reports-count', reportCount > 0 ? 'PASS' : 'WARN', `KPI report rows: ${reportCount}`, { role: ADMIN }); }); // 5. Ask Your Data await visitPage(page, '/analytics/ask-your-data/', 'ask-your-data', ADMIN, async (p) => { const hasInput = await p.locator('textarea, input[type="text"], [class*="query"], [class*="ask"]').count(); observe(M, 'ask-data-input', hasInput > 0 ? 'PASS' : 'WARN', `query input present: ${hasInput > 0}`, { role: ADMIN }); }); }); test('Report builder + saved reports + exports', async ({ page }) => { await login(page, ADMIN); // 6. Report Builder await visitPage(page, '/reports/', 'report-builder', ADMIN, async (p) => { const body = (await p.textContent('body')) || ''; const hasDataSources = /complaint|inquiry|observation|action|survey/i.test(body); observe(M, 'builder-sources', hasDataSources ? 'PASS' : 'WARN', `data sources visible: ${hasDataSources}`, { role: ADMIN }); }); // 7. Report Builder preview API (complaints data source) try { const csrf = await page.context().cookies().then((c) => c.find((x) => x.name === 'csrftoken')?.value || ''); const previewResp = await page.context().request.post(`${BASE_URL}/reports/preview/`, { headers: { 'X-Requested-With': 'XMLHttpRequest', ...(csrf ? { 'X-CSRFToken': csrf } : {}) }, data: { data_source: 'complaints', fields: ['title', 'status', 'severity'], filters: {}, page: 1, page_size: 5 }, }); const previewOk = previewResp.status() === 200; let previewData = ''; try { const j = await previewResp.json(); previewData = `${j.total || j.count || (j.data || []).length || 0} records`; } catch { /* */ } observe(M, 'builder-preview', previewOk ? 'PASS' : 'FAIL', `preview API: HTTP ${previewResp.status()} ${previewData}`, { role: ADMIN, http: previewResp.status() }); } catch (e) { observe(M, 'builder-preview', 'FAIL', `exception: ${(e as Error).message}`, { role: ADMIN }); } // 8. Saved Reports await visitPage(page, '/reports/saved/', 'saved-reports', ADMIN, async (p) => { const reportCount = await p.locator('table tbody tr, .report-item, [class*="saved"]').count(); observe(M, 'saved-count', reportCount > 0 ? 'PASS' : 'WARN', `saved reports: ${reportCount}`, { role: ADMIN }); }); // 9. Report Templates await visitPage(page, '/reports/templates/', 'report-templates', ADMIN); }); test('Role access: px_employee + viewer can view, source_user blocked', async ({ page }) => { // px_employee await login(page, 'px_employee'); await visitPage(page, '/analytics/dashboard/', 'pxe-analytics', 'px_employee'); await visitPage(page, '/reports/', 'pxe-reports', 'px_employee'); // viewer await login(page, 'viewer'); await visitPage(page, '/analytics/dashboard/', 'viewer-analytics', 'viewer'); // source_user (should be blocked) await login(page, 'source_user'); await page.goto(`${BASE_URL}/analytics/dashboard/`); await page.waitForLoadState('domcontentloaded'); const blocked = page.url().includes('login') || page.url().includes('px-sources') || (await page.textContent('body')).includes('Permission'); observe(M, 'source-user-blocked', blocked ? 'PASS' : 'WARN', `source_user on analytics: ${blocked ? 'blocked' : 'ALLOWED'}`, { role: 'source_user', url: page.url() }); }); }); test.afterAll(async () => { const counts = OBS.reduce>((a, o) => ((a[o.status] = (a[o.status] || 0) + 1), a), {}); console.log('\n=========== REPORTS + KPIs SUMMARY ==========='); console.log('Total observations:', OBS.length, JSON.stringify(counts)); console.log('==============================================\n'); });