pediatric-ai-scribe-v3/e2e/tests/bedside-smoke.spec.js
Daniel 1024b4741b feat: A1+A2 — Playwright smoke suite + index.html mtime-based caching
Safety net for upcoming refactors:
- 26 Playwright smoke tests via @playwright/test 1.50.0 in an official
  Playwright container (no host Node needed). Covers every Bedside sub-pill,
  the age→weight estimator, dose calculators (seizure/sepsis/anaphylaxis/
  burns/airway), and interactive widgets (lightbox, vent SVG).
- `npm run e2e` wrapper runs tests inside mcr.microsoft.com/playwright:v1.50.0-noble
  on the ped-ai_default Docker network so no host port mapping is needed.
- public/e2e-harness.html + public/js/e2e-bootstrap.js load the calculators
  component without the SPA auth wall (scripts external to satisfy CSP).

Server:
- server.js now re-reads public/index.html on mtime change instead of
  caching at boot. Fixes the "edit HTML, restart container" friction.
- CSP upgradeInsecureRequests disabled in helmet config; Caddy still
  enforces HTTPS at the reverse-proxy layer in production.
2026-04-20 03:51:22 +02:00

147 lines
6.6 KiB
JavaScript

// Bedside module smoke tests.
// Each test navigates to the Calculators tab, opens Bedside, clicks the target
// sub-pill, and asserts a known string is rendered. Catches "button doesn't
// fire / section goes blank" regressions.
const { test, expect } = require('@playwright/test');
// Helper — open Calculators tab + Bedside panel + (optionally) a sub-pill
async function openCalculators(page) {
await page.goto('/e2e-harness.html');
await page.waitForFunction(() => window.__harnessReady === true);
// Give the async innerHTML injection + JS init one more microtask tick
await page.waitForSelector('button.calc-nav-pill[data-calc="bedside"]');
}
async function openBedside(page, subPill) {
await openCalculators(page);
await page.click('button.calc-nav-pill[data-calc="bedside"]');
if (subPill) {
await page.click(`button.calc-pill[data-em="${subPill}"]`);
}
}
test.describe('Bedside — top-level', () => {
test('Calculators tab shows age-weight estimator', async ({ page }) => {
await openCalculators(page);
await expect(page.locator('#bedside-age')).toBeVisible();
await expect(page.locator('#bedside-formula')).toBeVisible();
await expect(page.locator('#bedside-weight')).toBeVisible();
});
test('Age → Weight: typing 3y auto-fills weight (APLS)', async ({ page }) => {
await openCalculators(page);
await page.fill('#bedside-age', '3y');
await expect(page.locator('#bedside-weight')).toHaveValue('14');
await expect(page.locator('#bedside-estimate-note')).toContainText('APLS');
});
test('Formula switch to Best Guess updates weight', async ({ page }) => {
await openCalculators(page);
await page.fill('#bedside-age', '3y');
await page.selectOption('#bedside-formula', 'bestguess');
await expect(page.locator('#bedside-weight')).toHaveValue('16'); // 2 * (3+5) = 16
await expect(page.locator('#bedside-estimate-note')).toContainText('Best Guess');
});
test('Clear button resets the estimator', async ({ page }) => {
await openCalculators(page);
await page.fill('#bedside-age', '5y');
await page.click('#btn-bedside-clear');
await expect(page.locator('#bedside-age')).toHaveValue('');
await expect(page.locator('#bedside-weight')).toHaveValue('');
});
});
test.describe('Bedside — every sub-pill renders', () => {
const subPills = [
{ key: 'neonatal', expected: /Neonatal Assessment/i },
{ key: 'airway', expected: /Airway Management/i },
{ key: 'cardiac', expected: /Cardiac Arrest/i },
{ key: 'respiratory', expected: /Respiratory Management/i },
{ key: 'ventilation', expected: /Oxygen & Ventilation/i },
{ key: 'seizure', expected: /Status Epilepticus/i },
{ key: 'sepsis', expected: /Sepsis & Fever/i },
{ key: 'anaphylaxis', expected: /Anaphylaxis/i },
{ key: 'sedation', expected: /Procedural Sedation/i },
{ key: 'agitation', expected: /Acute Agitation/i },
{ key: 'antiemetics', expected: /Antiemetics/i },
{ key: 'antimicrobials', expected: /Empiric Antimicrobials/i },
{ key: 'burns', expected: /Burn Management/i },
{ key: 'toxicology', expected: /Toxicology/i },
{ key: 'trauma', expected: /Trauma/i },
];
for (const { key, expected } of subPills) {
test(`${key} sub-pill shows header`, async ({ page }) => {
await openBedside(page, key);
await expect(page.locator(`#em-${key}`)).toContainText(expected);
});
}
});
test.describe('Bedside — dose calculators fire', () => {
test('Status Epilepticus: Show Pathway renders timeline with weight', async ({ page }) => {
await openBedside(page, 'seizure');
await page.fill('#seizure-weight', '20');
await page.click('#btn-seizure-calc');
await expect(page.locator('#seizure-result')).toContainText(/20 kg/);
await expect(page.locator('#seizure-result')).toContainText(/Lorazepam/);
await expect(page.locator('#seizure-result')).toContainText(/0\.1 mg\/kg/); // per-kg visible
});
test('Sepsis: Show Approach renders Phoenix criteria + first-hour bundle', async ({ page }) => {
await openBedside(page, 'sepsis');
await page.fill('#sepsis-weight', '25');
await page.click('#btn-sepsis-show');
await expect(page.locator('#sepsis-result')).toContainText(/Phoenix/);
await expect(page.locator('#sepsis-result')).toContainText(/first-hour/i);
});
test('Anaphylaxis: Calculate Doses shows weight-based epinephrine', async ({ page }) => {
await openBedside(page, 'anaphylaxis');
await page.fill('#anaph-weight', '25');
await page.click('#btn-anaph-calc');
await expect(page.locator('#anaph-result')).toContainText(/Epinephrine/);
await expect(page.locator('#anaph-result')).toContainText(/0\.25 mg/); // 25*0.01
});
test('Burns: body-parts calculator + Parkland', async ({ page }) => {
await openBedside(page, 'burns');
await page.fill('#burn-weight', '20');
await page.fill('input[data-burn-region="head"]', '50'); // 50% of 13 (young) = 6.5
await page.fill('input[data-burn-region="ant_trunk"]', '100'); // 100% of 13 = 13
await page.click('#btn-burn-calc');
await expect(page.locator('#burn-result')).toContainText(/Parkland/);
await expect(page.locator('#burn-result')).toContainText(/20 kg/);
});
test('Airway: Calculate renders RSI drugs with per-kg', async ({ page }) => {
await openBedside(page, 'airway');
await page.fill('#airway-weight', '20');
await page.fill('#airway-age', '5');
await page.click('#btn-airway-calc');
await expect(page.locator('#airway-result')).toContainText(/Ketamine/);
await expect(page.locator('#airway-result')).toContainText(/mg\/kg/); // per-kg visible
await expect(page.locator('#airway-result')).toContainText(/ETT/);
});
});
test.describe('Bedside — interactive widgets', () => {
test('Lightbox: View pathway image button opens modal', async ({ page }) => {
await openBedside(page, 'seizure');
await page.click('button[data-img-src="/img/epilepsy_eiic_pathway.png"]');
await expect(page.locator('#img-lightbox')).toBeVisible();
await page.click('#img-lightbox-close');
await expect(page.locator('#img-lightbox')).toBeHidden();
});
test('Ventilation: Show Reference renders pressure-time SVG', async ({ page }) => {
await openBedside(page, 'ventilation');
await page.fill('#vent-weight', '20');
await page.click('#btn-vent-show');
await expect(page.locator('#vent-result svg')).toBeVisible();
await expect(page.locator('#vent-result')).toContainText(/Target SpO2/);
await expect(page.locator('#vent-result')).toContainText(/PEEP/);
});
});