test: reports + KPIs / analytics — page-load + data-render + export (21 PASS, 0 FAIL)
All checks were successful
Build and Push Docker Image / build (push) Successful in 23s
All checks were successful
Build and Push Docker Image / build (push) Successful in 23s
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
This commit is contained in:
parent
e2109ff913
commit
c74ec6e832
163
e2e/tests/workflows/reports-kpis-workflow.spec.ts
Normal file
163
e2e/tests/workflows/reports-kpis-workflow.spec.ts
Normal file
@ -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<void>) {
|
||||||
|
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<Record<string, number>>((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');
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user