Calculators: - Bedside tab consolidating emergency protocols (Neonatal+Apgar+NRP, Airway/RSI, Cardiac Arrest/PALS, Respiratory, O2 & Ventilation, Status Epilepticus, Sepsis & Fever with PECARN/Aronson/Rochester/Step-by-Step, Anaphylaxis, Procedural Sedation, Agitation, Antiemetics, Antimicrobials, Burns with Lund-Browder body-parts TBSA + Parkland, Toxicology, Trauma). - Global age→weight estimator at top of Calculators tab (APLS + Best Guess). - Pressure-time waveform SVG teaching graphic for Ventilation. - Algorithm image lightbox (fullscreen, Esc/tap-to-close). - Every weight-based dose shows mg/kg inline for clinician verification. - Drug tables wrapped in overflow-x:auto for mobile. Infrastructure: - Pure dose math extracted to public/js/calc-math.js (dual-export Node+browser). - 36 unit tests in test/calc-math.test.js via node:test (zero new deps). - "npm test" added to package.json.
167 lines
7.8 KiB
JavaScript
167 lines
7.8 KiB
JavaScript
// ============================================================
|
|
// Unit tests for public/js/calc-math.js (pediatric dose math).
|
|
// Run with: npm test (alias for: node --test test/)
|
|
// ============================================================
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const M = require('../public/js/calc-math.js');
|
|
|
|
// ── estimateWeightFromAgeMonths ────────────────────────────────
|
|
test('APLS 0-12 mo: 6 mo -> 7 kg', () => {
|
|
const r = M.estimateWeightFromAgeMonths(6);
|
|
assert.equal(r.weight, 7); // 0.5*6 + 4 = 7
|
|
assert.match(r.formulaLabel, /0-12 mo/);
|
|
});
|
|
|
|
test('APLS 1-5 yr: 3 yr -> 14 kg', () => {
|
|
const r = M.estimateWeightFromAgeMonths(36);
|
|
assert.equal(r.weight, 14); // 2*3 + 8 = 14
|
|
assert.match(r.formulaLabel, /1-5 yr/);
|
|
});
|
|
|
|
test('APLS 6-12 yr: 8 yr -> 31 kg', () => {
|
|
const r = M.estimateWeightFromAgeMonths(96);
|
|
assert.equal(r.weight, 31); // 3*8 + 7 = 31
|
|
assert.match(r.formulaLabel, /6-12 yr/);
|
|
});
|
|
|
|
test('Best Guess 14 yr -> 56 kg (switches away from APLS at >=13 yr)', () => {
|
|
const r = M.estimateWeightFromAgeMonths(168);
|
|
assert.equal(r.weight, 56); // 4 * 14 = 56
|
|
assert.match(r.formulaLabel, /Best Guess/);
|
|
});
|
|
|
|
test('APLS and Best Guess both reported', () => {
|
|
const r = M.estimateWeightFromAgeMonths(48);
|
|
assert.equal(r.all.apls, 16); // 2*4 + 8
|
|
assert.equal(r.all.bestGuess, 18); // 2*(4+5)
|
|
});
|
|
|
|
test('Null/negative age returns null', () => {
|
|
assert.equal(M.estimateWeightFromAgeMonths(null), null);
|
|
assert.equal(M.estimateWeightFromAgeMonths(-5), null);
|
|
});
|
|
|
|
// ── Parkland ────────────────────────────────────────────────────
|
|
test('Parkland 20kg, 25% TBSA -> 2000 mL total, 1000 first 8h', () => {
|
|
const p = M.parkland(20, 25);
|
|
assert.equal(p.totalMl, 2000); // 4 * 20 * 25
|
|
assert.equal(p.first8Ml, 1000);
|
|
assert.equal(p.next16Ml, 1000);
|
|
assert.equal(p.rateFirst8, 125); // 1000 / 8
|
|
assert.equal(p.rateNext16, 63); // 1000 / 16 = 62.5 -> 63
|
|
});
|
|
|
|
test('Parkland invalid inputs return null', () => {
|
|
assert.equal(M.parkland(0, 25), null);
|
|
assert.equal(M.parkland(20, 0), null);
|
|
assert.equal(M.parkland(null, 25), null);
|
|
});
|
|
|
|
// ── Maintenance 4-2-1 ──────────────────────────────────────────
|
|
test('Maintenance: 8 kg -> 32 mL/hr', () => { assert.equal(M.maintenance421(8), 32); });
|
|
test('Maintenance: 15 kg -> 50 mL/hr', () => { assert.equal(M.maintenance421(15), 50); });
|
|
test('Maintenance: 25 kg -> 65 mL/hr', () => { assert.equal(M.maintenance421(25), 65); });
|
|
test('Maintenance: 70 kg -> 110 mL/hr', () => { assert.equal(M.maintenance421(70), 110); });
|
|
|
|
// ── PRAM ────────────────────────────────────────────────────────
|
|
test('PRAM all zero -> Mild (0)', () => {
|
|
const r = M.pramScore(0, 0, 0, 0, 0);
|
|
assert.equal(r.total, 0); assert.equal(r.severity, 'Mild');
|
|
});
|
|
test('PRAM total 5 -> Moderate', () => {
|
|
const r = M.pramScore(1, 2, 0, 1, 1);
|
|
assert.equal(r.total, 5); assert.equal(r.severity, 'Moderate');
|
|
});
|
|
test('PRAM total 10 -> Severe', () => {
|
|
const r = M.pramScore(2, 2, 2, 2, 2);
|
|
assert.equal(r.total, 10); assert.equal(r.severity, 'Severe');
|
|
});
|
|
|
|
// ── Westley ─────────────────────────────────────────────────────
|
|
test('Westley 2 -> Mild', () => { assert.equal(M.westleyScore(0, 0, 1, 0, 1).severity, 'Mild'); });
|
|
test('Westley 4 -> Moderate', () => { assert.equal(M.westleyScore(0, 0, 2, 1, 1).severity, 'Moderate'); });
|
|
test('Westley 8 -> Severe', () => { assert.equal(M.westleyScore(0, 4, 2, 1, 1).severity, 'Severe'); });
|
|
test('Westley 12 -> Impending respiratory failure', () => { assert.equal(M.westleyScore(5, 4, 0, 1, 2).severity, 'Impending respiratory failure'); });
|
|
|
|
// ── Epinephrine (anaphylaxis vs arrest) ─────────────────────────
|
|
test('Epi anaphylaxis IM: 20 kg -> 0.2 mg (1:1000)', () => {
|
|
const r = M.epiAnaphylaxisIM(20);
|
|
assert.equal(r.doseMg, 0.2);
|
|
assert.equal(r.volumeMl_1in1000, 0.2); // 1 mg/mL concentration
|
|
});
|
|
|
|
test('Epi anaphylaxis IM: max 0.5 mg for >=50 kg', () => {
|
|
assert.equal(M.epiAnaphylaxisIM(80).doseMg, 0.5);
|
|
});
|
|
|
|
test('Epi arrest IV: 10 kg -> 0.1 mg, 1 mL of 1:10,000', () => {
|
|
const r = M.epiArrestIV(10);
|
|
assert.equal(r.doseMg, 0.1);
|
|
assert.equal(r.volumeMl_1in10000, 1); // 0.1 mg/mL concentration
|
|
});
|
|
|
|
test('Epi arrest IV: max 1 mg for >=100 kg', () => {
|
|
assert.equal(M.epiArrestIV(120).doseMg, 1);
|
|
});
|
|
|
|
// ── NRP epi ──────────────────────────────────────────────────────
|
|
test('NRP epi: 3 kg newborn -> 0.03-0.09 mg IV (0.3-0.9 mL of 1:10,000)', () => {
|
|
const r = M.nrpEpiIV(3);
|
|
assert.equal(r.lowMg, 0.03);
|
|
assert.equal(r.highMg, 0.09);
|
|
assert.equal(r.lowMl, 0.3);
|
|
assert.equal(r.highMl, 0.9);
|
|
});
|
|
|
|
// ── RSI drugs ────────────────────────────────────────────────────
|
|
test('Ketamine RSI 20 kg -> 30-40 mg', () => {
|
|
const r = M.rsiKetamine(20);
|
|
assert.equal(r.lowMg, 30);
|
|
assert.equal(r.highMg, 40);
|
|
});
|
|
|
|
test('Rocuronium 25 kg -> 30 mg (1.2 mg/kg)', () => {
|
|
assert.equal(M.rsiRocuronium(25).mg, 30);
|
|
});
|
|
|
|
test('Succinylcholine dose/kg: <10 kg = 2 mg/kg; >=10 kg = 1.5 mg/kg', () => {
|
|
assert.equal(M.rsiSuccinylcholine(8).perKg, 2);
|
|
assert.equal(M.rsiSuccinylcholine(20).perKg, 1.5);
|
|
assert.equal(M.rsiSuccinylcholine(20).mg, 30); // 20*1.5
|
|
});
|
|
|
|
// ── Min systolic BP ─────────────────────────────────────────────
|
|
test('Min SBP: 5 yr -> 80', () => { assert.equal(M.minSBP(5), 80); });
|
|
test('Min SBP: infant (<1 yr) -> 70', () => { assert.equal(M.minSBP(0.5), 70); });
|
|
test('Min SBP: 12 yr -> 90', () => { assert.equal(M.minSBP(12), 90); });
|
|
|
|
// ── ETT sizing ──────────────────────────────────────────────────
|
|
test('ETT 4 yr: uncuffed 5.0, cuffed 4.5, depth 15', () => {
|
|
const r = M.ettSize(4);
|
|
assert.equal(r.uncuffedMm, 5);
|
|
assert.equal(r.cuffedMm, 4.5);
|
|
assert.equal(r.depthCm, 15);
|
|
});
|
|
|
|
// ── Lund-Browder TBSA ───────────────────────────────────────────
|
|
test('Lund-Browder: full head infant = 18%', () => {
|
|
assert.equal(M.lundBrowderTBSA('infant', { head: 100 }), 18);
|
|
});
|
|
test('Lund-Browder: full head adult = 7%', () => {
|
|
assert.equal(M.lundBrowderTBSA('adult', { head: 100 }), 7);
|
|
});
|
|
test('Lund-Browder: full body adds to ~100% (infant) — tolerance for published rounding', () => {
|
|
const all = {};
|
|
Object.keys(M.LB).forEach((k) => { all[k] = 100; });
|
|
const t = M.lundBrowderTBSA('infant', all);
|
|
// Published charts typically sum to 99-100% depending on head % (18 vs 19 at 0 yr vs 1 yr).
|
|
assert.ok(t >= 98 && t <= 100.5, 'expected 98-100.5, got ' + t);
|
|
});
|
|
test('Lund-Browder: 50% of both thighs young child = 8%', () => {
|
|
// young (1-5) thigh = 8% each, so 50% of each = 4% + 4% = 8%
|
|
assert.equal(M.lundBrowderTBSA('young', { r_thigh: 50, l_thigh: 50 }), 8);
|
|
});
|
|
test('Lund-Browder: invalid bracket returns null', () => {
|
|
assert.equal(M.lundBrowderTBSA('bogus', { head: 50 }), null);
|
|
});
|