// ============================================================ // 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); }); // ── drugs.json schema ─────────────────────────────────────────── // Asserts the JSON is loadable and that each of the 5 extracted // bedside sections exists with a non-empty drugs array. test('drugs.json: schema loads with all 5 extracted sections + non-empty drugs', () => { const fs = require('node:fs'); const path = require('node:path'); const raw = fs.readFileSync(path.join(__dirname, '..', 'public', 'data', 'drugs.json'), 'utf8'); const j = JSON.parse(raw); assert.equal(j.version, '1.0'); assert.ok(j.sections, 'top-level sections object present'); ['anaphylaxis', 'sedation', 'agitation', 'antiemetics', 'seizure'].forEach((key) => { const sec = j.sections[key]; assert.ok(sec, `section ${key} present`); assert.ok(Array.isArray(sec.drugs) && sec.drugs.length > 0, `${key}.drugs non-empty`); }); });