Ships the AAP 2022 phototherapy/exchange nomograms, the Bhutani 1999
risk zones, and the Fenton 2013 preterm weight-for-GA chart as real
React calculators. These were the high-risk "table-driven" ports the
migration checkpoint flagged as needing captured vectors before
landing — the whole vector-capture workflow now exists and can be
reused for BP / BMI / growth-beyond-Fenton next.
Workflow, so future calculator ports have a template:
scripts/capture-calc-vectors.js
Standalone Node script. Data tables + math inlined VERBATIM from
public/js/calculators.js (no rewriting, no reformatting). Picks 83
carefully chosen test cases — edge-of-domain, table-key-exact,
interpolated-between-keys, and clinical-decision-boundary values —
and emits e2e/fixtures/calc-vectors.json with
{ inputs, output } pairs produced by the authoritative math.
e2e/fixtures/calc-vectors.json
12 Bhutani cases, 58 AAP 2022 cases, 13 Fenton cases. Re-run
capture-calc-vectors.js whenever the vanilla file changes.
shared/clinical/bilirubin.ts
• 17 HourTable constants ported byte-for-byte from calculators.js
lines 1489-1512: photo 35w/36w/37w/38w/39w/40+ (low risk) +
35w/36w/37w/38+ (medium risk), same 8 for exchange.
• Bhutani zones (p95/p75/p40) from lines 1644-1651.
• interpolateThreshold helper (lines 1514-1525).
• classifyBhutani + classifyAapBili functions mirror the vanilla
click-handler logic.
shared/clinical/fenton.ts
• 15-week × 2-sex LMS table from lines 1168-1183 preserved entry-
for-entry.
• interpolateLMS + calcZ + zToPercentile (Abramowitz & Stegun
normal CDF, lines 2299-2326) ported byte-for-byte.
• fentonWeightForAge returns { L, M, S, z, percentile };
classifySizeForAge labels SGA/<10 / AGA / LGA/>90.
shared/clinical/bilirubin.test.ts + fenton.test.ts
Vitest suites that import e2e/fixtures/calc-vectors.json and assert
classifyBhutani / classifyAapBili / fentonWeightForAge match
every captured vector to 10 decimal places for threshold values
and 6 decimals for the Fenton M (grams, so 6 is >= 1e-3 g).
All 102 tests pass locally (19 prior + 70 bili + 13 Fenton).
client/src/pages/Calculators.tsx
• BiliPanel with AAP 2022 / Bhutani mode switch, GA + risk-factor
dropdowns, color-coded status / zone badges, both threshold
pairs surfaced in the result grid.
• GrowthPanel runs Fenton with sex + GA + weight inputs, emits
Z-score, percentile, L/M/S reference, and SGA/AGA/LGA label.
• PILLS flags bili + growth as ported: true; ActivePanel routes to
the new components. BP / BMI / vitals / resus / equipment
remain legacy-linked until their own vectors land.
e2e/tests/calculators-react.spec.js
Three new parity tests covering above-phototherapy/above-exchange
transitions (38w 72h TSB 20 → phototherapy; TSB 26 → exchange),
Bhutani high-risk classification at 36h TSB 13, and Fenton 32w
male 1795g landing exactly at 50th percentile AGA.
Backend tsc + client tsc + vite build + vitest (102/102) all green.
Bundle 619 kB / 178 kB gz (+10 kB for the bili tables).
109 lines
5 KiB
JavaScript
109 lines
5 KiB
JavaScript
// ============================================================
|
|
// CALCULATORS (React port) — sub-nav shell smoke test.
|
|
//
|
|
// Pure shell test until test vectors + formula ports land in
|
|
// dedicated follow-up commits.
|
|
// ============================================================
|
|
|
|
const { test, expect, E2E_BASE } = require('../fixtures');
|
|
|
|
const EXPECTED_PILLS = [
|
|
'bp', 'bmi', 'growth', 'bili', 'vitals',
|
|
'bsa', 'dose', 'resus', 'gcs', 'equipment',
|
|
];
|
|
|
|
async function openReactCalc(page) {
|
|
await page.goto(E2E_BASE + '/app/calculators');
|
|
await page.waitForSelector('[data-testid="calc-subnav"]', { timeout: 15000 });
|
|
}
|
|
|
|
test.describe('React Calculators — sub-nav shell', () => {
|
|
|
|
test('all 10 pills render in expected order', async ({ authedPage: _, page }) => {
|
|
await openReactCalc(page);
|
|
for (const id of EXPECTED_PILLS) {
|
|
await expect(page.locator('[data-testid="calc-pill-' + id + '"]')).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('clicking a pill switches the active panel', async ({ authedPage: _, page }) => {
|
|
await openReactCalc(page);
|
|
await expect(page.locator('[data-testid="calc-panel-bp"]')).toBeVisible();
|
|
await page.click('[data-testid="calc-pill-bili"]');
|
|
await expect(page.locator('[data-testid="calc-panel-bili"]')).toBeVisible();
|
|
});
|
|
|
|
test('each panel carries a legacy-viewer link', async ({ authedPage: _, page }) => {
|
|
await openReactCalc(page);
|
|
await expect(page.getByText('Open in legacy viewer').first()).toBeVisible();
|
|
});
|
|
|
|
test('BSA calculator runs in React', async ({ authedPage: _, page }) => {
|
|
await openReactCalc(page);
|
|
await page.click('[data-testid="calc-pill-bsa"]');
|
|
await page.fill('#react-bsa-weight', '20');
|
|
await page.fill('#react-bsa-height', '110');
|
|
await page.click('[data-testid="calc-bsa-calculate"]');
|
|
await expect(page.locator('[data-testid="calc-bsa-result"]')).toContainText('0.782');
|
|
});
|
|
|
|
test('weight-based dose calculator caps max dose', async ({ authedPage: _, page }) => {
|
|
await openReactCalc(page);
|
|
await page.click('[data-testid="calc-pill-dose"]');
|
|
await page.fill('#react-dose-weight', '15');
|
|
await page.fill('#react-dose-per-kg', '100');
|
|
await page.fill('#react-dose-max', '500');
|
|
await page.click('[data-testid="calc-dose-calculate"]');
|
|
await expect(page.locator('[data-testid="calc-dose-result"]')).toContainText('500.0 mg');
|
|
await expect(page.locator('[data-testid="calc-dose-result"]')).toContainText('Capped');
|
|
});
|
|
|
|
test('GCS calculator updates score from selected components', async ({ authedPage: _, page }) => {
|
|
await openReactCalc(page);
|
|
await page.click('[data-testid="calc-pill-gcs"]');
|
|
await expect(page.locator('[data-testid="calc-gcs-result"]')).toContainText('GCS: 15/15');
|
|
await page.selectOption('#react-gcs-motor', '1');
|
|
await expect(page.locator('[data-testid="calc-gcs-result"]')).toContainText('GCS: 10/15');
|
|
});
|
|
|
|
test('AAP 2022 bilirubin classifies above exchange correctly', async ({ authedPage: _, page }) => {
|
|
await openReactCalc(page);
|
|
await page.click('[data-testid="calc-pill-bili"]');
|
|
// 38w low-risk at 72h: phototherapy threshold = 18.8, exchange = 25.9.
|
|
// TSB 20 should be "Above Phototherapy"; 26 should be "Above Exchange".
|
|
await page.selectOption('#bili-ga', '38');
|
|
await page.selectOption('#bili-risk', 'low');
|
|
await page.fill('#bili-hours', '72');
|
|
await page.fill('#bili-tsb', '20');
|
|
await page.click('[data-testid="calc-bili-calculate"]');
|
|
await expect(page.locator('[data-testid="calc-bili-aap-result"]')).toContainText('Above Phototherapy');
|
|
await expect(page.locator('[data-testid="calc-bili-aap-result"]')).toContainText('18.8');
|
|
|
|
await page.fill('#bili-tsb', '26');
|
|
await page.click('[data-testid="calc-bili-calculate"]');
|
|
await expect(page.locator('[data-testid="calc-bili-aap-result"]')).toContainText('Above Exchange');
|
|
});
|
|
|
|
test('Bhutani nomogram classifies high-risk zone', async ({ authedPage: _, page }) => {
|
|
await openReactCalc(page);
|
|
await page.click('[data-testid="calc-pill-bili"]');
|
|
await page.click('[data-testid="bili-mode-bhutani"]');
|
|
// At 36h, p95 = 12.8 — TSB 13 should land in High-Risk.
|
|
await page.fill('#bili-hours', '36');
|
|
await page.fill('#bili-tsb', '13');
|
|
await page.click('[data-testid="calc-bili-calculate"]');
|
|
await expect(page.locator('[data-testid="calc-bili-bhutani-result"]')).toContainText('High-Risk Zone');
|
|
});
|
|
|
|
test('Fenton growth classifies 32w male 1795g as AGA (50th percentile)', async ({ authedPage: _, page }) => {
|
|
await openReactCalc(page);
|
|
await page.click('[data-testid="calc-pill-growth"]');
|
|
// 32w male median is 1795g — should land right at 50th percentile.
|
|
await page.selectOption('#fenton-sex', 'male');
|
|
await page.fill('#fenton-ga', '32');
|
|
await page.fill('#fenton-weight', '1795');
|
|
await page.click('[data-testid="calc-fenton-calculate"]');
|
|
await expect(page.locator('[data-testid="calc-fenton-result"]')).toContainText('AGA');
|
|
await expect(page.locator('[data-testid="calc-fenton-result"]')).toContainText('50.0%');
|
|
});
|
|
});
|