import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; import type { SharedDoses, SharedLab } from "../src/api/types"; import { DoseBars, FindingsMix, LabsPanel, LabTrend } from "../src/clinician/charts"; import { doseHeadline, doseStack, dosePercent, findingsMix, labChange, labSeries, labsAnswer, niceTicks, parseLabValue, parseRange, plotLab, rangeLabel, smoothPath, sumDoses, trendWords, } from "../src/logic/charts"; const lab = (id: string, name: string, value: string, date: string, more: Partial = {}): SharedLab => ({ id, name, value, unit: "mL/min/1.73m2", range: "Above 60", flag: null, date, addedBy: "MARMAR Health", addedAt: `${date}T09:00:00Z`, changedAt: `${date}T09:00:00Z`, ...more, }); describe("lab ranges", () => { it("reads both ends, with any dash or 'to'", () => { expect(parseRange("3.5-5.0")).toEqual({ low: 3.5, high: 5 }); expect(parseRange("3.5 – 5.0")).toEqual({ low: 3.5, high: 5 }); expect(parseRange("3.5 — 5.0 mmol/L")).toEqual({ low: 3.5, high: 5 }); expect(parseRange("3.5 to 5.0")).toEqual({ low: 3.5, high: 5 }); expect(parseRange("1,000-4,000")).toEqual({ low: 1000, high: 4000 }); expect(parseRange("-2 - 2")).toEqual({ low: -2, high: 2 }); expect(parseRange("4.0 - 11.0 (adult)")).toEqual({ low: 4, high: 11 }); }); it("reads one open end", () => { expect(parseRange("< 5.7")).toEqual({ low: null, high: 5.7 }); expect(parseRange("<=5.7%")).toEqual({ low: null, high: 5.7 }); expect(parseRange("≤ 5.7")).toEqual({ low: null, high: 5.7 }); expect(parseRange("Below 7.0")).toEqual({ low: null, high: 7 }); expect(parseRange("up to 40")).toEqual({ low: null, high: 40 }); expect(parseRange("5.7 or less")).toEqual({ low: null, high: 5.7 }); expect(parseRange(">60")).toEqual({ low: 60, high: null }); expect(parseRange("≥ 60")).toEqual({ low: 60, high: null }); expect(parseRange("60+")).toEqual({ low: 60, high: null }); expect(parseRange("Above 60")).toEqual({ low: 60, high: null }); expect(parseRange("60 and above")).toEqual({ low: 60, high: null }); expect(parseRange("Above 60 mL/min/1.73m2")).toEqual({ low: 60, high: null }); }); it("reads the backend's one-ended form", () => { expect(parseRange("3.5 to")).toEqual({ low: 3.5, high: null }); expect(parseRange("to 5")).toEqual({ low: null, high: 5 }); expect(rangeLabel("3.5 to")).toBe("3.5 or above"); expect(rangeLabel("to 5")).toBe("Up to 5"); expect(rangeLabel("Above 60")).toBe("Above 60"); }); it("draws no band for junk", () => { for (const junk of ["", " ", "Normal", "see report", "N/A", "5.7", "<", "60-", "1-2-3", "5.0-3.5", "3,5-5,0", "90-120/60-80", null, undefined]) expect(parseRange(junk as string | null)).toBeNull(); }); }); describe("lab values", () => { it("reads plain numbers only", () => { expect(parseLabValue("38")).toBe(38); expect(parseLabValue(" 7.9 ")).toBe(7.9); expect(parseLabValue("1,200")).toBe(1200); expect(parseLabValue("−3")).toBe(-3); expect(parseLabValue("7.9 %")).toBe(7.9); for (const v of ["<0.5", ">90", "Positive", "148/92", "12,5", ""]) expect(parseLabValue(v)).toBeNull(); }); }); describe("lab series", () => { const labs = [ lab("e3", "eGFR", "38", "2026-10-12", { flag: "low" }), lab("h1", "HbA1c", "7.9", "2026-09-15", { unit: "%", range: "Below 7.0", flag: "high" }), lab("e1", "eGFR", "58", "2026-04-03"), lab("e2", "egfr ", "47", "2026-08-14", { flag: "low" }), lab("e0", "eGFR", "61", "2026-02-01", { unit: "ml/min" }), ]; it("groups by name, newest test first, and charts only numbers in the latest unit", () => { const [egfr, hba1c] = labSeries(labs); expect(egfr.name).toBe("eGFR"); expect(egfr.results.map((l) => l.id)).toEqual(["e3", "e2", "e1", "e0"]); expect(egfr.points.map((p) => p.lab.id)).toEqual(["e1", "e2", "e3"]); expect(egfr.trend).toBe(true); expect(egfr.range).toEqual({ low: 60, high: null }); expect(hba1c.trend).toBe(false); }); it("is not a trend when the latest is not a number or every result is on one day", () => { expect(labSeries([lab("a", "CRP", "<5", "2026-10-12"), lab("b", "CRP", "12", "2026-09-01")])[0].trend).toBe(false); const sameDay = labSeries([lab("a", "K", "4.1", "2026-10-12"), lab("b", "K", "4.4", "2026-10-12", { addedAt: "2026-10-12T08:00:00Z" })])[0]; expect(sameDay.points).toHaveLength(1); expect(sameDay.trend).toBe(false); }); it("says the trend and the flags in words", () => { const [egfr] = labSeries(labs); const words = trendWords(egfr); expect(words).toContain("eGFR, 3 results from 3 Apr 2026 to 12 Oct 2026: it fell from 58 to 38 mL/min/1.73m2."); expect(words).toContain("Lowest 38 on 12 Oct 2026, highest 58 on 3 Apr 2026."); expect(words).toContain("Range on the record: Above 60."); expect(words).toContain("2 of 3 flagged high or low on the record, the latest low."); expect(words).not.toMatch(/recommend|should|safe|diagnos/i); }); it("gives the change since the result before, with the record's decimals", () => { const [egfr] = labSeries(labs); expect(labChange(egfr)).toEqual({ direction: "down", words: "Down 9 since 14 Aug" }); const a1c = labSeries([lab("a", "HbA1c", "8.1", "2026-10-12", { unit: "%" }), lab("b", "HbA1c", "7.4", "2025-06-01", { unit: "%" })])[0]; expect(labChange(a1c)).toEqual({ direction: "up", words: "Up 0.7 since 1 Jun 2025" }); }); it("answers with what the record flags", () => { expect(labsAnswer(labSeries(labs))).toBe("2 of 2 tests flagged on the latest result: eGFR low, HbA1c high."); expect(labsAnswer(labSeries([lab("k", "Potassium", "4.6", "2026-10-12")]))).toBe("1 test, none flagged high or low on the latest result."); expect(labsAnswer([])).toBe("No lab results shared."); }); }); describe("chart geometry", () => { const [egfr] = labSeries([lab("e1", "eGFR", "58", "2026-04-03"), lab("e2", "eGFR", "47", "2026-08-14"), lab("e3", "eGFR", "38", "2026-10-12")]); it("puts the first and last results at the edges and the range's open end at the top", () => { const plot = plotLab(egfr); expect(plot.points[0].x).toBe(3); expect(plot.points[2].x).toBe(97); expect(plot.points[0].y).toBeLessThan(plot.points[2].y); for (const p of plot.points) { expect(p.y).toBeGreaterThan(0); expect(p.y).toBeLessThan(100); } expect(plot.band?.top).toBe(0); expect(plot.band!.bottom).toBeGreaterThan(0); expect(plot.band!.bottom).toBeLessThan(plot.points[0].y); expect(plot.area.endsWith("Z")).toBe(true); expect(plot.ticks.map((t) => t.label)).toEqual(["40", "50", "60"]); }); it("draws no band without a range and copes with one value", () => { expect(plotLab({ ...egfr, range: null }).band).toBeNull(); const one = plotLab({ points: egfr.points.slice(0, 1), range: null }); expect(one.points[0]).toMatchObject({ x: 50, y: 50 }); expect(one.line).toBe(""); }); it("keeps nice ticks and a line that passes through every point", () => { expect(niceTicks(0, 10, 4)).toEqual([0, 2.5, 5, 7.5, 10]); expect(niceTicks(3.2, 3.9, 3)).toEqual([3.25, 3.5, 3.75]); const d = smoothPath([{ x: 0, y: 10 }, { x: 50, y: 50 }, { x: 100, y: 20 }]); expect(d.startsWith("M0 10")).toBe(true); expect(d).toContain("50 50"); expect(d.endsWith("100 20")).toBe(true); }); }); describe("doses", () => { it("stacks to what was due, with the rest as not yet due or not logged", () => { const s = doseStack({ onTime: 10, late: 1, skipped: 1, missed: 0, expected: 14 }); expect(s.segments.map((x) => [x.key, x.count])).toEqual([["onTime", 10], ["late", 1], ["skipped", 1], ["rest", 2]]); expect(s.segments.reduce((a, x) => a + x.count, 0)).toBe(14); expect(s.segments.reduce((a, x) => a + x.share, 0)).toBeCloseTo(1); expect(s).toMatchObject({ taken: 11, logged: 12, expected: 14, over: false }); }); it("handles more logged than due without a bar past 100%", () => { const s = doseStack({ onTime: 15, late: 2, skipped: 0, missed: 1, expected: 14 }); expect(s.over).toBe(true); expect(s.segments.some((x) => x.key === "rest")).toBe(false); expect(s.segments.reduce((a, x) => a + x.share, 0)).toBeCloseTo(1); expect(doseStack({ onTime: 0, late: 0, skipped: 0, missed: 0, expected: 0 }).segments).toEqual([]); expect(doseStack({ onTime: -2, late: Number.NaN, skipped: 0, missed: 0, expected: 7 }).segments).toEqual([{ key: "rest", count: 7, share: 1 }]); }); it("rounds the figure down so 100% means every dose", () => { expect(dosePercent(12, 14)).toBe(85); expect(dosePercent(299, 300)).toBe(99); expect(dosePercent(14, 14)).toBe(100); expect(dosePercent(16, 14)).toBe(100); expect(dosePercent(0, 0)).toBeNull(); }); it("keeps this week's sentence and adds up the last 30 days from the medicines", () => { const d = { week: { taken: 12, expected: 14, onTime: 12 }, perMedicine: [ { medicineId: "a", name: "Metformin", schedule: null, changedAt: "", week: { onTime: 12, late: 0, skipped: 2, missed: 0, expected: 14 }, last30: { onTime: 51, late: 3, skipped: 6, missed: 0, expected: 60 } }, { medicineId: "b", name: "Amlodipine", schedule: null, changedAt: "", week: { onTime: 7, late: 0, skipped: 0, missed: 0, expected: 7 }, last30: { onTime: 28, late: 1, skipped: 1, missed: 0, expected: 30 } }, ], }; expect(doseHeadline(d, "week")).toMatchObject({ percent: 85, sentence: "12 of 14 doses taken this week, all on time." }); expect(doseHeadline(d, "last30")).toMatchObject({ percent: 92, taken: 83, expected: 90, sentence: "83 of 90 doses taken in the last 30 days, 79 on time." }); expect(doseHeadline({ ...d, week: { taken: 0, expected: 0, onTime: 0 } }, "week")).toMatchObject({ percent: null, sentence: "No doses were due this week." }); expect(doseHeadline({ ...d, week: { taken: 0, expected: 7, onTime: 0 } }, "week").sentence).toBe("0 of 7 doses taken this week."); expect(sumDoses([])).toEqual({ onTime: 0, late: 0, skipped: 0, missed: 0, expected: 0 }); }); }); describe("findings mix", () => { it("lists every tier, most pressing first, with unknown tiers as Good to know", () => { const mix = findingsMix([{ tier: "this_week" }, { tier: "get_help_now" }, { tier: "this_week" }, { tier: "something_new" }]); expect(mix.total).toBe(4); expect(mix.tiers.map((t) => [t.label, t.count])).toEqual([ ["Urgent review", 1], ["Review today", 0], ["Review this week", 2], ["Good to know", 1], ]); expect(findingsMix([]).tiers.every((t) => t.share === 0)).toBe(true); }); }); const html = (el: ReturnType) => renderToStaticMarkup(el).replace(/'/g, "'").replace(/"/g, '"').replace(/&/g, "&"); describe("the charts on the page", () => { const labs = [ lab("e3", "eGFR", "38", "2026-10-12", { flag: "low" }), lab("e2", "eGFR", "47", "2026-08-14", { flag: "low" }), lab("e1", "eGFR", "58", "2026-04-03"), lab("k1", "Potassium", "4.6", "2026-10-12", { unit: "mmol/L", range: "3.5-5.0" }), ]; it("says the trend in words and gives every point a label and one tab stop", () => { const out = html(createElement(LabTrend, { series: labSeries(labs)[0] })); expect(out).toContain('role="img"'); expect(out).toContain("eGFR, 3 results from 3 Apr 2026 to 12 Oct 2026: it fell from 58 to 38 mL/min/1.73m2."); expect(out).toContain('aria-label="12 Oct 2026 · 38 mL/min/1.73m2 · Low on the record"'); expect(out.match(/tabindex="0"/g)).toHaveLength(1); expect(out).toContain("Range Above 60"); expect(out).toContain("Down 9 since 14 Aug"); }); it("shows a single value as a row, not a chart, and keeps the table", () => { const out = html(createElement(LabsPanel, { labs, lastLookedAt: null })); expect(out.match(/role="img"/g)).toHaveLength(1); expect(out).toContain("Potassium"); expect(out).toContain("Range 3.5-5.0"); expect(out).toContain("Lab results shared from MARMAR Health"); expect(out).toContain("1 of 2 tests flagged on the latest result: eGFR low."); }); it("puts the dose figure first and says every colour in words", () => { const doses: SharedDoses = { asOf: "2026-10-30T10:00:00Z", lastLookedAt: null, week: { taken: 12, expected: 14, onTime: 12 }, perMedicine: [{ medicineId: "m", name: "Metformin", schedule: "twice_daily", changedAt: "", week: { onTime: 12, late: 0, skipped: 2, missed: 0, expected: 14 }, last30: { onTime: 51, late: 3, skipped: 6, missed: 0, expected: 60 } }], patterns: [], }; const out = html(createElement(DoseBars, { doses })); expect(out).toContain("85"); expect(out).toContain("12 of 14 doses taken this week, all on time."); expect(out).toContain("This week: 12 on time · 0 late · 2 skipped · 0 missed, of 14"); for (const word of ["On time", "Late", "Skipped", "Missed", "Not yet due or not logged"]) expect(out).toContain(word); expect(out).toContain("Twice a day"); }); it("lists each tier with its count", () => { const out = html(createElement(FindingsMix, { findings: [{ tier: "today" }, { tier: "good_to_know" }, { tier: "good_to_know" }] })); expect(out).toContain("Review today"); expect(out).toMatch(/Good to know<\/span>]*>2