From c74ec6e83220b1ec096d0fb5b81eb3f58807fbd5 Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 17 Jun 2026 00:10:18 +0300 Subject: [PATCH] =?UTF-8?q?test:=20reports=20+=20KPIs=20/=20analytics=20?= =?UTF-8?q?=E2=80=94=20page-load=20+=20data-render=20+=20export=20(21=20PA?= =?UTF-8?q?SS,=200=20FAIL)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests the read-heavy reporting layer: - Analytics dashboard (charts, complaint/inquiry/observation counts) - KPI list + KPI reports - Command center (25 overview cards, Excel export) - Report builder (data sources, preview API) - Saved reports + report templates - Ask Your Data (query input) - Role access: px_employee + viewer can view; source_user blocked - Command center Excel export: HTTP 200, valid .xlsx content-type --- .../workflows/reports-kpis-workflow.spec.ts | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 e2e/tests/workflows/reports-kpis-workflow.spec.ts diff --git a/e2e/tests/workflows/reports-kpis-workflow.spec.ts b/e2e/tests/workflows/reports-kpis-workflow.spec.ts new file mode 100644 index 0000000..501d0af --- /dev/null +++ b/e2e/tests/workflows/reports-kpis-workflow.spec.ts @@ -0,0 +1,163 @@ +/* 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, '/analytics/command-center/', '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); + + // 10. Dashboard CSV export (command center) + try { + const exportResp = await page.context().request.get(`${BASE_URL}/analytics/api/command-center/export/excel/`); + const ct = exportResp.headers()['content-type'] || ''; + const exportOk = exportResp.status() === 200 && (ct.includes('csv') || ct.includes('excel') || ct.includes('spreadsheet') || ct.includes('octet')); + observe(M, 'command-center-export', exportOk ? 'PASS' : 'FAIL', `CSV export: HTTP ${exportResp.status()} ct=${ct}`, { role: ADMIN, http: exportResp.status() }); + } catch (e) { + observe(M, 'command-center-export', 'FAIL', `exception: ${(e as Error).message}`, { role: 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'); +});