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.
147 lines
6.6 KiB
JavaScript
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/);
|
|
});
|
|
});
|