pediatric-ai-scribe-v3/e2e/tests/calculators-react.spec.js
Daniel 9aa3961459 feat(client): port Bilirubin + Fenton calculators with captured vectors
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).
2026-04-24 01:49:45 +02:00

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%');
});
});