diff --git a/e2e/tests/top-calculators.spec.js b/e2e/tests/top-calculators.spec.js new file mode 100644 index 0000000..e93d264 --- /dev/null +++ b/e2e/tests/top-calculators.spec.js @@ -0,0 +1,220 @@ +// Smoke tests for the 10 top-row calculator tabs (everything except Bedside). +// Each test navigates to its tab, fills inputs, clicks Calculate/Assess, +// and asserts a known string appears in the result. Catches the "tab loads +// but button does nothing" class of regression. + +const { test, expect } = require('@playwright/test'); + +async function openCalculators(page) { + await page.goto('/e2e-harness.html'); + await page.waitForFunction(() => window.__harnessReady === true); + await page.waitForSelector('button.calc-nav-pill[data-calc="bp"]'); +} + +async function selectTab(page, tabName) { + await page.click(`button.calc-nav-pill[data-calc="${tabName}"]`); + await expect(page.locator(`#calc-${tabName}`)).toBeVisible(); +} + +test.describe('Top-level calculators — panel loads', () => { + const tabs = ['bp', 'bmi', 'growth', 'bili', 'vitals', 'bsa', 'dose', 'resus', 'gcs', 'equipment']; + for (const tab of tabs) { + test(`${tab} panel becomes visible when pill clicked`, async ({ page }) => { + await openCalculators(page); + await selectTab(page, tab); + }); + } +}); + +test.describe('Blood Pressure percentile', () => { + test('5 yr, male, height 110, BP 105/65 → produces a result', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'bp'); + await page.fill('#bp-age', '5'); + await page.selectOption('#bp-sex', 'male'); + await page.fill('#bp-height', '110'); + await page.fill('#bp-systolic', '105'); + await page.fill('#bp-diastolic', '65'); + await page.click('#btn-calc-bp'); + await expect(page.locator('#bp-result')).not.toHaveClass(/hidden/); + // Result should mention a percentile or classification + await expect(page.locator('#bp-result')).toContainText(/percentile|Normal|Elevated|HTN|Stage/i); + }); + + test('Clear button hides result', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'bp'); + await page.fill('#bp-age', '5'); + await page.selectOption('#bp-sex', 'male'); + await page.fill('#bp-height', '110'); + await page.fill('#bp-systolic', '105'); + await page.fill('#bp-diastolic', '65'); + await page.click('#btn-calc-bp'); + await page.click('#btn-clear-bp'); + await expect(page.locator('#bp-result')).toHaveClass(/hidden/); + await expect(page.locator('#bp-age')).toHaveValue(''); + }); +}); + +test.describe('BMI percentile', () => { + test('7 yr, male, 25 kg, 120 cm → BMI computed', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'bmi'); + await page.fill('#bmi-age-yr', '7'); + await page.selectOption('#bmi-age-mo', '0'); + await page.selectOption('#bmi-sex', 'male'); + await page.fill('#bmi-weight', '25'); + await page.fill('#bmi-height', '120'); + await page.click('#btn-calc-bmi'); + await expect(page.locator('#bmi-result')).not.toHaveClass(/hidden/); + // BMI = 25 / 1.20² = 17.36 — expect something recognizable + await expect(page.locator('#bmi-result')).toContainText(/17\.|BMI|percentile/i); + }); +}); + +test.describe('Body Surface Area (Mosteller)', () => { + test('20 kg, 110 cm → BSA shown', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'bsa'); + await page.fill('#bsa-weight', '20'); + await page.fill('#bsa-height', '110'); + await page.click('#btn-calc-bsa'); + await expect(page.locator('#bsa-result')).not.toHaveClass(/hidden/); + // sqrt(110*20/3600) = 0.782 + await expect(page.locator('#bsa-result')).toContainText(/0\.78|BSA|m²|m2/i); + }); +}); + +test.describe('Weight-based dosing', () => { + test('15 kg × 10 mg/kg → 150 mg shown', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'dose'); + await page.fill('#dose-weight', '15'); + await page.fill('#dose-per-kg', '10'); + await page.click('#btn-calc-dose'); + await expect(page.locator('#dose-result')).not.toHaveClass(/hidden/); + await expect(page.locator('#dose-result')).toContainText(/150/); + }); + + test('Max cap respected: 15 kg × 100 mg/kg capped at 500 mg', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'dose'); + await page.fill('#dose-weight', '15'); + await page.fill('#dose-per-kg', '100'); + await page.fill('#dose-max', '500'); + await page.click('#btn-calc-dose'); + await expect(page.locator('#dose-result')).toContainText(/500/); + }); +}); + +test.describe('Growth charts', () => { + test('3 yr male, 14 kg → Weight-for-Age result', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'growth'); + await page.selectOption('#growth-sex', 'male'); + await page.fill('#growth-age-yr', '3'); + await page.fill('#growth-weight', '14'); + await page.click('#btn-calc-growth'); + await expect(page.locator('#growth-result')).not.toHaveClass(/hidden/); + await expect(page.locator('#growth-result')).toContainText(/percentile|z-score|%ile|z=/i); + }); + + test('Sub-pill switches to Length-for-Age', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'growth'); + await page.click('button.calc-pill[data-growth="lfa"]'); + await expect(page.locator('button.calc-pill[data-growth="lfa"]')).toHaveClass(/active/); + }); +}); + +test.describe('Bilirubin (AAP 2022)', () => { + test('GA 38, age 48h, TSB 12.5, no risk factors → AAP assessment', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'bili'); + await page.selectOption('#bili-ga', '38'); + await page.fill('#bili-age-hours', '48'); + await page.fill('#bili-tsb', '12.5'); + await page.selectOption('#bili-risk', 'none'); + await page.click('#btn-calc-bili-aap'); + await expect(page.locator('#bili-result')).not.toHaveClass(/hidden/); + await expect(page.locator('#bili-result')).toContainText(/phototherapy|threshold|AAP|bilirubin/i); + }); + + test('Bhutani sub-pill: age 48h, TSB 8.5 → risk zone', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'bili'); + await page.click('button.calc-pill[data-bili="bhutani"]'); + await page.fill('#bhutani-age', '48'); + await page.fill('#bhutani-tsb', '8.5'); + await page.click('#btn-calc-bhutani'); + await expect(page.locator('#bili-result')).not.toHaveClass(/hidden/); + await expect(page.locator('#bili-result')).toContainText(/risk|zone|low|high|intermediate/i); + }); +}); + +test.describe('Vital signs reference', () => { + test('Selecting 1-3 yr shows HR range', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'vitals'); + await page.selectOption('#vitals-age-select', '1-3yr'); + await expect(page.locator('#vitals-result')).not.toHaveClass(/hidden/); + // Expected: HR 70-110 + await expect(page.locator('#vitals-result')).toContainText(/70|HR/i); + }); +}); + +test.describe('Resus meds', () => { + test('15 kg → multiple weight-based drug doses rendered', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'resus'); + await page.fill('#resus-weight', '15'); + await page.click('#btn-calc-resus'); + await expect(page.locator('#resus-result')).not.toHaveClass(/hidden/); + // At least epinephrine + atropine should appear for any resus dose table + await expect(page.locator('#resus-result')).toContainText(/epinephrine|epi/i); + await expect(page.locator('#resus-result')).toContainText(/atropine/i); + }); +}); + +test.describe('Glasgow Coma Scale', () => { + test('Child defaults (4/5/6) → GCS 15 shown', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'gcs'); + // selects default to max scores → 4+5+6 = 15 + await expect(page.locator('#gcs-result')).toContainText(/15/); + }); + + test('Changing motor to "None" (1) → GCS drops', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'gcs'); + await page.selectOption('#gcs-child-motor', '1'); + await expect(page.locator('#gcs-result')).toContainText(/10/); // 4+5+1 + }); + + test('Switch to infant panel', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'gcs'); + await page.click('button.calc-pill[data-gcs="infant"]'); + await expect(page.locator('#gcs-infant-panel')).toBeVisible(); + await expect(page.locator('#gcs-child-panel')).toBeHidden(); + }); +}); + +test.describe('Equipment sizing', () => { + test('Selecting "1 year" renders sizes', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'equipment'); + await page.selectOption('#equip-age-select', '1yr'); + await expect(page.locator('#equip-result')).not.toHaveClass(/hidden/); + // Should mention ETT and at least one other item + await expect(page.locator('#equip-result')).toContainText(/ETT|Endotracheal/i); + }); + + test('Selecting empty option hides result', async ({ page }) => { + await openCalculators(page); + await selectTab(page, 'equipment'); + await page.selectOption('#equip-age-select', '1yr'); + await page.selectOption('#equip-age-select', ''); + await expect(page.locator('#equip-result')).toHaveClass(/hidden/); + }); +});