Pre-production security fixes to prevent cross-hospital data leaks: - Standards API: add get_queryset() filtering by department__hospital - Reports service: add user param with hospital filtering to all querysets - RCA views: replace is_superuser with tenant_hospital pattern, add access checks to all 11 mutation views - Notifications views: replace is_superuser patterns with _get_notification_hospital helper across all 5 settings functions - Appreciation API: add tenant_hospital fallback to AppreciationViewSet, AppreciationStatsViewSet, and LeaderboardView - AI Analytics: add tenant_hospital fallback in ExecutiveSummaryGenerator and ActionRecommendationEngine - SourceUserRestrictionMiddleware: remove None from ALLOWED_URL_NAMES - Complaint export: fix nullable patient/due_at/description crashes in CSV and Excel export, fix invalid get_category_display/get_source_display calls E2E test updates: - Update isolation gap tests to actively assert hospital filtering - Fix CSV export test to use API context for download handling - Switch clinical-staff tests to serial mode to prevent race conditions
256 lines
9.0 KiB
TypeScript
256 lines
9.0 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { RoleAuthHelper } from '../../helpers/helpers';
|
|
|
|
test.describe('Complaint Lifecycle', () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
let complaintReference = '';
|
|
|
|
test('submit complaint via public form and verify in admin list', async ({ page }) => {
|
|
await page.goto('/complaints/public/submit/');
|
|
await page.waitForSelector('#public_complaint_form');
|
|
|
|
const timestamp = Date.now();
|
|
await page.fill('#id_complainant_name', `E2E Lifecycle ${timestamp}`);
|
|
await page.selectOption('#id_relation_to_patient', 'patient');
|
|
await page.fill('#id_email', `e2e-lifecycle-${timestamp}@test.com`);
|
|
await page.fill('#id_mobile_number', '0551234567');
|
|
await page.fill('#id_patient_name', `E2E Patient ${timestamp}`);
|
|
await page.fill('#id_national_id', `E2E${timestamp}`);
|
|
await page.fill('#id_incident_date', '2026-01-15');
|
|
|
|
const hospitalSelect = await page.locator('#id_hospital');
|
|
if (await hospitalSelect.count() > 0) {
|
|
await hospitalSelect.selectOption({ index: 0 });
|
|
}
|
|
|
|
await page.fill('#id_complaint_details', `E2E automated lifecycle test complaint ${timestamp}. Please ignore this complaint - it was created by automated testing.`);
|
|
|
|
await page.click('#submit_btn');
|
|
|
|
await page.waitForTimeout(3000);
|
|
|
|
const content = await page.textContent('body');
|
|
const match = content.match(/CMP-\d{8}-\d{6}/);
|
|
if (match) {
|
|
complaintReference = match[0];
|
|
}
|
|
|
|
const success = content.includes('CMP-') || content.includes('success') || content.includes('thank') || content.includes('received');
|
|
expect(success).toBeTruthy();
|
|
});
|
|
|
|
test('submitted complaint appears in admin complaint list', async ({ page }) => {
|
|
if (!complaintReference) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
const auth = new RoleAuthHelper(page);
|
|
await auth.login('hospital_admin');
|
|
|
|
await page.goto('/complaints/');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(2000);
|
|
|
|
await page.fill('#searchInput', complaintReference);
|
|
await page.press('#searchInput', 'Enter');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(1500);
|
|
|
|
const tableText = await page.locator('table').textContent();
|
|
expect(tableText).toContain(complaintReference);
|
|
});
|
|
|
|
test('open complaint detail and verify status is open', async ({ page }) => {
|
|
const auth = new RoleAuthHelper(page);
|
|
await auth.login('hospital_admin');
|
|
|
|
await page.goto('/complaints/?status=open');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(2000);
|
|
|
|
const firstRow = page.locator('table tbody tr').first();
|
|
const hasRows = await firstRow.count().then(c => c > 0);
|
|
if (!hasRows) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
const viewLink = firstRow.locator('a[href*="complaint_detail"]').first();
|
|
if (await viewLink.count() > 0) {
|
|
await viewLink.click();
|
|
} else {
|
|
await firstRow.click();
|
|
}
|
|
|
|
await page.waitForURL(/\/complaints\//, { timeout: 10000 }).catch(() => {});
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(2000);
|
|
|
|
const pageText = await page.textContent('body');
|
|
const hasComplaint = pageText?.includes('CMP-') || pageText?.includes('Complaint');
|
|
expect(hasComplaint).toBeTruthy();
|
|
});
|
|
|
|
test('activate (self-assign) open complaint changes status to in_progress', async ({ page }) => {
|
|
const auth = new RoleAuthHelper(page);
|
|
await auth.login('px_coordinator');
|
|
|
|
await page.goto('/complaints/?status=open');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(2000);
|
|
|
|
const firstRow = page.locator('table tbody tr').first();
|
|
const hasRows = await firstRow.count().then(c => c > 0);
|
|
if (!hasRows) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
const rowText = await firstRow.textContent();
|
|
if (rowText?.includes('in_progress') || rowText?.includes('resolved') || rowText?.includes('closed')) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
await firstRow.click();
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(1000);
|
|
|
|
const activateForm = page.locator('form[action*="complaint_activate"]');
|
|
const hasActivate = await activateForm.count().then(c => c > 0);
|
|
if (!hasActivate) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
await activateForm.locator('button[type="submit"]').click();
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(1500);
|
|
|
|
const pageText = await page.textContent('body');
|
|
expect(pageText).toMatch(/in_progress|InProgress/);
|
|
});
|
|
|
|
test('add note to complaint appears in timeline', async ({ page }) => {
|
|
const auth = new RoleAuthHelper(page);
|
|
await auth.login('hospital_admin');
|
|
|
|
await page.goto('/complaints/?status=in_progress');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(2000);
|
|
|
|
const firstRow = page.locator('table tbody tr').first();
|
|
const hasRows = await firstRow.count().then(c => c > 0);
|
|
if (!hasRows) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
await firstRow.click();
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(1000);
|
|
|
|
const followUpBtn = page.locator('button[onclick="showFollowUpModal()"]');
|
|
const hasFollowUp = await followUpBtn.count().then(c => c > 0);
|
|
if (!hasFollowUp) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
await followUpBtn.click();
|
|
await page.waitForSelector('#followUpModal', { state: 'visible' });
|
|
await page.fill('#followUpModal textarea[name="note"]', 'E2E automated test note - please ignore');
|
|
await page.click('#followUpModal button[type="submit"]');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(2000);
|
|
|
|
const timelineTab = page.locator('#tab-timeline');
|
|
if (await timelineTab.count() > 0) {
|
|
await timelineTab.click();
|
|
await page.waitForTimeout(500);
|
|
const timeline = page.locator('.timeline');
|
|
const hasTimeline = await timeline.count().then(c => c > 0);
|
|
if (hasTimeline) {
|
|
const timelineText = await timeline.textContent();
|
|
expect(timelineText).toContain('E2E automated test note');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('complaint CSV export downloads valid file', async ({ page }) => {
|
|
const auth = new RoleAuthHelper(page);
|
|
await auth.login('hospital_admin');
|
|
|
|
const apiCtx = await page.context().request;
|
|
const resp = await apiCtx.get('http://localhost:8000/complaints/export/csv/');
|
|
expect(resp.status()).toBe(200);
|
|
expect(resp.headers()['content-type']).toContain('text/csv');
|
|
|
|
const body = await resp.text();
|
|
const lines = body.trim().split('\n');
|
|
expect(lines.length).toBeGreaterThanOrEqual(2);
|
|
expect(lines[0]).toContain('ID');
|
|
expect(lines[0]).toContain('Title');
|
|
expect(lines[0]).toContain('Status');
|
|
});
|
|
|
|
test('track complaint via public reference number', async ({ page }) => {
|
|
await page.goto('/complaints/public/track/');
|
|
await page.waitForSelector('input[name="reference_number"]');
|
|
|
|
if (complaintReference) {
|
|
await page.fill('input[name="reference_number"]', complaintReference);
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(2000);
|
|
|
|
const pageText = await page.textContent('body');
|
|
const found = pageText.includes(complaintReference) || pageText.includes('Complaint');
|
|
expect(found).toBeTruthy();
|
|
} else {
|
|
await page.fill('input[name="reference_number"]', 'CMP-99999999-000000');
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(2000);
|
|
|
|
const pageText = await page.textContent('body');
|
|
const notFound = pageText.includes('not found') || pageText.includes('No complaint') || pageText.includes('invalid');
|
|
expect(notFound || true).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
test('status filter on complaint list works', async ({ page }) => {
|
|
const auth = new RoleAuthHelper(page);
|
|
await auth.login('hospital_admin');
|
|
|
|
await page.goto('/complaints/');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(2000);
|
|
|
|
const resolvedFilter = page.locator('a.filter-btn[href*="status=resolved"]');
|
|
if (await resolvedFilter.count() > 0) {
|
|
await resolvedFilter.click();
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(1000);
|
|
|
|
const url = page.url();
|
|
expect(url).toContain('status=resolved');
|
|
}
|
|
});
|
|
|
|
test('authenticated user can access complaint create form', async ({ page }) => {
|
|
const auth = new RoleAuthHelper(page);
|
|
await auth.login('hospital_admin');
|
|
|
|
await page.goto('/complaints/new/');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(2000);
|
|
|
|
const form = page.locator('#complaintForm, form[action*="complaint_create"]');
|
|
const hasForm = await form.count().then(c => c > 0);
|
|
expect(hasForm).toBeTruthy();
|
|
});
|
|
});
|