How to Test Healthcare Patient Portals
Short answer
Patient portals fail on wrong-chart access, PHI in URLs and logs, proxy/caregiver ACL bugs, and appointment policy violations—not on whether a dashboard loads. Use synthetic patients only, probe patient_id on every API call, never ship real PHI to TrueCoverage RUM, and pair scheduling tests with timezone-aware calendar specs.
Part of Testing Guides by industry.
Who this is for
Teams shipping patient portals, telehealth schedulers, lab results viewers, messaging with clinicians, and caregiver proxy access under HIPAA (US) and often GDPR for EU patients. EHR integrations, FHIR APIs, and identity verification common.
Why testing healthcare portals matters
Healthcare E2E mistakes become breach notifications, not bug tickets:
- Wrong patient chart — user A sees user B lab results; minimum necessary rule violated; OCR investigation.
- PHI in URLs/logs — MRN or diagnosis in query string, Playwright trace, or RUM event; impermissible disclosure.
- Caregiver/proxy overreach — teen parent sees restricted notes; proxy sees billing not authorized.
- Appointment policy — double-booked clinician; telehealth link sent for in-person slot.
- Break-glass audit — emergency access without immutable audit row; fails hospital compliance review.
Never use real PHI in automated tests. Synthetic MRNs, fake names, fictional diagnoses only.
HIPAA notes on test data
| Rule | Testing implication |
|---|---|
| Minimum necessary | Test users access only synthetic records seeded for their role |
| No real PHI in non-prod | Staging must not copy prod dumps without de-identification |
| Audit controls | Probe audit log on chart view and break-glass |
| Transmission security | TLS on test env; no PHI in HTTP query params |
| Business associates | Vendor sandboxes (eRx, labs) with synthetic orders only |
TrueCoverage RUM: instrument portal_section and patient_role with internal opaque ids—never diagnosis text, names, MRNs, or DOB in event payloads.
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Caregiver proxy | Wrong chart | ACL bug | Probe patient_id on API |
| Teen privacy | Parent blocked from notes | Policy | Negative probe 403 |
| Lab results | Preliminary vs final | Wrong release | Probe result_status |
| Messaging | Attachments PHI | Leak in notif | Probe redacted preview |
| Telehealth join | Token scoped | Cross visit | Probe visit_id match |
| Break-glass | Emergency access | No audit | Probe audit + reason |
| Session timeout | PHI on screen | Should lock | Idle clock spec |
| Download CCD | Export scope | Extra records | Manifest entity check |
| Pay bill | Gateway | Wrong guarantor | Probe account_id |
| Proxy revoke | Still access | Stale ACL | Probe after revoke |
| ID verification | Failed KBA | Should block | Probe verification_state |
| Multi-facility | Wrong site records | Data segregation | Probe facility_id |
Synthetic patient seed
await request.post('/api/test/seed-patient', {
data: {
runId,
mrn: `TEST-${runId}`, // clearly synthetic
displayName: 'Test Patient Alpha',
dob: '1990-01-01',
labs: [{ code: 'TEST-GLU', value: '95', status: 'final' }],
},
});
Block CI if fixtures match real MRN patterns your org uses in prod.
Wrong-chart negative (critical)
await request.post('/api/test/seed-patient', { data: { runId, mrn: `TEST-A-${runId}` } });
await request.post('/api/test/seed-patient', { data: { runId: runId + '-b', mrn: `TEST-B-${runId}-b` } });
// Login as patient A
const probe = await request.get(`/api/test/chart-summary?patientId=TEST-B-${runId}-b`);
expect(probe.status()).toBe(403);
await page.goto('/records'); // patient A session
await expect(page.getByText(`TEST-B`)).toHaveCount(0);
Appointments
Pair with calendar scheduling and datetime/timezones—telehealth slots across DST boundaries are high risk.
await page.getByRole('button', { name: /Book telehealth/i }).click();
// ... select slot ...
const booking = await request.get(`/api/test/bookings?runId=${runId}`).then(r => r.json());
expect(booking[0].modality).toBe('telehealth');
expect(booking[0].joinUrl).toContain(`visit-${runId}`); // not another patient's token
Requirement slices to cover
patient_role— patient, caregiver, proxy, clinician (if dual portal)portal_section— labs, messages, billing, appointments, documents
Compare prod section usage to test scenarios; run /testchimp evolve for undertested caregiver proxy paths.
CI checklist
- Zero real PHI in repo, traces, or RUM
- Wrong-chart negative on every release candidate
- Audit probe on chart view and break-glass
- Synthetic MRNs prefixed TEST or use UUID
- No diagnosis strings in URLs—assert route uses opaque ids
- GDPR export/delete overlap with GDPR guide for EU patients
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Prod DB snapshot in staging | PHI exposure | Synthetic seed API |
| MRN in URL | HIPAA leak | Opaque session token |
| TrueCoverage with lab values | Breach | Section enums only |
| Skip proxy ACL | Wrong chart | 403 probe |
| Real patient E2E videos | PHI in recording | Synthetic only |
| Ignore audit | Compliance fail | Probe view events |
Example scenario
Situation: Caregiver proxy for Patient A attempts to open Patient B lab results via URL guessing.
Expected outcome: 403 on API; no lab values in DOM; audit unauthorized_access attempt.
Why UI-only automation breaks: Generic error page but API leaked B results in network tab—breach.
- Arrange: Seed patients A and B with distinct synthetic MRNs; proxy linked to A only.
- Act: Navigate to /labs?patientId=B opaque id as proxy session.
- Assert: Probe 403; DOM no B lab codes; audit probe unauthorized attempt.
TestChimp workflow: Track portal_section views by patient_role; evolve caregiver paths when prod proxy usage rises.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- Calendar scheduling — appointments
- RBAC permissions — role separation
- GDPR privacy — EU patient rights
- Audit compliance — immutable trails
- WebSockets — live result notifications
External references
Frequently asked questions
Can I use TrueCoverage RUM on patient portals?
Yes with strict metadata hygiene—instrument portal_section and patient_role using opaque ids only. Never send diagnoses, names, MRNs, or lab values in RUM payloads; align with HIPAA minimum necessary.
Which portal sections do patients use most?
Compare patient_role × portal_section prod vs test in TrueCoverage; run /testchimp evolve for undertested caregiver or messaging paths from anonymized usage.
Can I use de-identified prod data in tests?
Only if properly de-identified under HIPAA expert determination or safe harbor—not casual masking. Default to fully synthetic seed routes for E2E.
How do I test caregiver proxy access?
Seed proxy relationship, assert authorized sections via probe, negative test other patient ids return 403, audit both success and denied attempts.
How do telehealth links stay scoped?
Book as patient, probe joinUrl visit_id matches booking; attempt token reuse for another visit id—expect 403.
What about break-glass emergency access?
Simulate break-glass with reason code, assert chart access granted temporarily, probe mandatory audit row with actor and justification.
How does GDPR overlap HIPAA tests?
EU patients may need export/delete per GDPR guide; HIPAA adds stricter PHI handling in logs and BA agreements—combine probes without real clinical data.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.