Replaces the legacy-viewer fallback for three of the fifteen Bedside
sub-modules with real React implementations. Weight-based dosing now
runs locally for the most time-critical emergencies — the other 12
modules (neonatal, airway, respiratory, ventilation, sepsis, sedation,
agitation, antiemetics, antimicrobials, burns, toxicology, trauma)
still fall through to LegacyPanel.
shared/clinical/calculators.ts
New formatDose(weightKg, perKg, max?, unit = 'mg') helper that
mirrors the vanilla S.dStr exactly — same rounding, same cap rule,
same label format. Returns { value, unit, capped, perKg, max,
label } so consumers can render rich UI without re-parsing a HTML
string. Pure function; unit tests in the existing vitest suite
still pass.
client/src/pages/BedsidePanels.tsx (new)
Three panels keyed to the Bedside pill id:
• AnaphylaxisPanel — STEP 1 epi IM callout + full 9-row dosing
table (fluids, diphenhydramine, ranitidine, dex, methylpred,
refractory epi gtt, glucagon). Drug per-kg + max values ported
verbatim from ANAPH_FALLBACK in the vanilla module.
• CardiacPanel — PALS dosing with 6 sub-views (general,
asystole/PEA, bradycardia, SVT, VF/pulseless VT, stable VT).
Every drug row carries the same mg/kg + max values as the
vanilla cardiac.js (epi, amio, lido, atropine, adenosine ×2,
bicarb, CaCl/CaGluc, Mg, D10, defib energies). Concentration
disclaimer + AHA citation preserved.
• SeizurePanel — full status-epilepticus timeline (0 min →
40 min refractory) with the vanilla SEIZURE_FALLBACK drug
table (loraz / midaz / diaz for benzo; levetiracetam /
fosphenytoin / valproate / phenobarb for 2nd-line; midaz
pentobarb propofol ketamine infusions for refractory). Key
points + citation preserved.
REAL_BEDSIDE_PANELS set + renderBedsideRealPanel(pillId) export so
Bedside.tsx can dispatch by id without import sprawl.
client/src/pages/Bedside.tsx
When the active pill is in REAL_BEDSIDE_PANELS, render the real
panel; otherwise fall back to LegacyPanel. No other changes — the
sub-nav, age→weight estimator, and 12 legacy-linked modules stay
exactly as they were.
e2e/tests/bedside-react.spec.js
Three new parity tests (in addition to the existing shell checks):
• Anaphylaxis: 20 kg → 0.2 mg epi IM; 70 kg → 0.5 mg (capped).
• Cardiac: 25 kg → 0.25 mg epi IV in general view and asystole.
• Seizure: 10 kg → D10W 20-50 mL, Lorazepam 1 mg.
Client tsc -b + vite build clean. Bundle 609 kB / 173 kB gz (+28 kB
for three full panels; vite's 500 kB chunk warning noted for later
code-splitting, not blocking).
85 lines
4.2 KiB
JavaScript
85 lines
4.2 KiB
JavaScript
// ============================================================
|
||
// BEDSIDE (React port) — sub-nav shell smoke test.
|
||
//
|
||
// First-commit scope: the 15 sub-pills render in the expected order
|
||
// and selecting a pill reveals a panel with a legacy-viewer link.
|
||
// Actual clinical dosing panels (neonatal through trauma) port in
|
||
// dedicated follow-up commits alongside the calculators.
|
||
// ============================================================
|
||
|
||
const { test, expect, E2E_BASE } = require('../fixtures');
|
||
|
||
const EXPECTED_PILLS = [
|
||
'neonatal', 'airway', 'cardiac', 'respiratory', 'ventilation',
|
||
'seizure', 'sepsis', 'anaphylaxis', 'sedation', 'agitation',
|
||
'antiemetics', 'antimicrobials', 'burns', 'toxicology', 'trauma',
|
||
];
|
||
|
||
async function openReactBedside(page) {
|
||
await page.goto(E2E_BASE + '/app/bedside');
|
||
await page.waitForSelector('[data-testid="bedside-subnav"]', { timeout: 15000 });
|
||
}
|
||
|
||
test.describe('React Bedside — sub-nav shell', () => {
|
||
|
||
test('all 15 sub-pills render in the expected order', async ({ authedPage: _, page }) => {
|
||
await openReactBedside(page);
|
||
for (const id of EXPECTED_PILLS) {
|
||
await expect(page.locator('[data-testid="bedside-pill-' + id + '"]')).toBeVisible();
|
||
}
|
||
});
|
||
|
||
test('clicking a pill switches the active panel', async ({ authedPage: _, page }) => {
|
||
await openReactBedside(page);
|
||
await expect(page.locator('[data-testid="bedside-panel-neonatal"]')).toBeVisible();
|
||
await page.click('[data-testid="bedside-pill-airway"]');
|
||
await expect(page.locator('[data-testid="bedside-panel-airway"]')).toBeVisible();
|
||
});
|
||
|
||
test('panel carries a legacy-viewer link', async ({ authedPage: _, page }) => {
|
||
await openReactBedside(page);
|
||
await expect(page.getByText('Open in legacy viewer').first()).toBeVisible();
|
||
});
|
||
|
||
test('age-to-weight estimator runs in React', async ({ authedPage: _, page }) => {
|
||
await openReactBedside(page);
|
||
await page.fill('[data-testid="bedside-age-input"]', '3y');
|
||
await expect(page.locator('[data-testid="bedside-estimate-result"]')).toContainText('14 kg');
|
||
await expect(page.locator('[data-testid="bedside-weight-input"]')).toHaveValue('14');
|
||
await page.selectOption('[data-testid="bedside-formula-select"]', 'bestguess');
|
||
await expect(page.locator('[data-testid="bedside-estimate-result"]')).toContainText('16 kg');
|
||
await expect(page.locator('[data-testid="bedside-weight-input"]')).toHaveValue('16');
|
||
});
|
||
|
||
test('anaphylaxis panel computes IM epinephrine dose by weight', async ({ authedPage: _, page }) => {
|
||
await openReactBedside(page);
|
||
await page.click('[data-testid="bedside-pill-anaphylaxis"]');
|
||
await page.fill('[data-testid="anaph-weight"]', '20');
|
||
// 20 kg × 0.01 mg/kg = 0.2 mg epi IM (below the 0.5 mg cap).
|
||
await expect(page.locator('[data-testid="bedside-panel-anaphylaxis"]')).toContainText('0.2 mg');
|
||
// Uncapped — ensure 0.5 kicks in past the cap threshold (wt ≥ 50 kg).
|
||
await page.fill('[data-testid="anaph-weight"]', '70');
|
||
await expect(page.locator('[data-testid="bedside-panel-anaphylaxis"]')).toContainText('0.5 mg');
|
||
});
|
||
|
||
test('cardiac arrest PALS general table renders weight-scaled epinephrine', async ({ authedPage: _, page }) => {
|
||
await openReactBedside(page);
|
||
await page.click('[data-testid="bedside-pill-cardiac"]');
|
||
await page.fill('[data-testid="cardiac-weight"]', '25');
|
||
// Default view is general. Epi IV = 0.01 mg/kg × 25 = 0.25 mg.
|
||
await expect(page.locator('[data-testid="bedside-panel-cardiac"]')).toContainText('0.25 mg');
|
||
// Switching to Asystole keeps the same dose logic.
|
||
await page.click('[data-testid="cardiac-view-asystole"]');
|
||
await expect(page.locator('[data-testid="bedside-panel-cardiac"]')).toContainText('0.25 mg');
|
||
});
|
||
|
||
test('seizure pathway computes D10W bolus range by weight', async ({ authedPage: _, page }) => {
|
||
await openReactBedside(page);
|
||
await page.click('[data-testid="bedside-pill-seizure"]');
|
||
await page.fill('[data-testid="seizure-weight"]', '10');
|
||
// 10 kg × 2-5 mL/kg = 20-50 mL D10W
|
||
await expect(page.locator('[data-testid="bedside-panel-seizure"]')).toContainText('20-50 mL');
|
||
// Lorazepam 0.1 mg/kg × 10 = 1 mg
|
||
await expect(page.locator('[data-testid="bedside-panel-seizure"]')).toContainText('1 mg');
|
||
});
|
||
});
|