Files
HealthLog/src/lib/__tests__/glucose.test.ts
T
Marc-André Bombeck 25944ed57a feat(glucose): read glucose in one unit across series, CSV, and detail page
Glucose is stored canonical mg/dL but the series DTO, CSV export, and detail
page emitted raw mg/dL while FHIR already converted to the user preference.

Convert at serialization through the single convertGlucose helper:
- series route sets unit from the user's glucoseUnit and converts each point
- CSV export converts BLOOD_GLUCOSE rows and emits the unit in the unit column
- the detail page + stat strip honour the preference via the chart value scale
- declare the trailing-90-day window in the glucose median label across locales

mmol/L-preference users now see the blood-glucose page, stat strip, iOS series,
and CSV in mmol/L; mg/dL users are unchanged.
2026-06-14 17:16:48 +02:00

111 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from "vitest";
import {
mgdlToMmol,
mmolToMgdl,
convertGlucose,
toCanonicalMgdl,
resolveGlucoseUnit,
thresholdMetricForContext,
} from "../glucose";
describe("glucose conversion", () => {
it("converts mg/dL to mmol/L with 1 decimal", () => {
expect(mgdlToMmol(100)).toBe(5.5);
expect(mgdlToMmol(126)).toBeCloseTo(7.0, 1); // diabetes threshold
expect(mgdlToMmol(70)).toBeCloseTo(3.9, 1); // hypoglycemia threshold
});
it("convertGlucose dispatches to the right unit", () => {
expect(convertGlucose(100, "mg/dL")).toBe(100);
expect(convertGlucose(100, "mmol/L")).toBe(5.5);
});
it("resolveGlucoseUnit defaults to mg/dL", () => {
expect(resolveGlucoseUnit(null)).toBe("mg/dL");
expect(resolveGlucoseUnit(undefined)).toBe("mg/dL");
expect(resolveGlucoseUnit("mmol/L")).toBe("mmol/L");
expect(resolveGlucoseUnit("mg/dL")).toBe("mg/dL");
expect(resolveGlucoseUnit("random-garbage")).toBe("mg/dL");
});
it("thresholdMetricForContext maps every context", () => {
expect(thresholdMetricForContext("FASTING")).toBe("BLOOD_GLUCOSE_FASTING");
expect(thresholdMetricForContext("POSTPRANDIAL")).toBe(
"BLOOD_GLUCOSE_POSTPRANDIAL",
);
expect(thresholdMetricForContext("RANDOM")).toBe("BLOOD_GLUCOSE_RANDOM");
expect(thresholdMetricForContext("BEDTIME")).toBe("BLOOD_GLUCOSE_BEDTIME");
});
// Display-unit logic guard for the targets/dashboard surfaces:
// canonical mg/dL stays integer; mmol/L stays 1-decimal; ranges convert too.
it("display unit conversion preserves integer/decimal precision", () => {
const fastingMin = 70; // mg/dL
const fastingMax = 99;
const ada126 = 126; // diabetes threshold
expect(convertGlucose(fastingMin, "mg/dL")).toBe(70);
expect(convertGlucose(fastingMax, "mg/dL")).toBe(99);
expect(convertGlucose(ada126, "mg/dL")).toBe(126);
expect(convertGlucose(fastingMin, "mmol/L")).toBe(3.9);
expect(convertGlucose(fastingMax, "mmol/L")).toBe(5.5);
expect(convertGlucose(ada126, "mmol/L")).toBe(7);
});
// The target-editor write path: a value the user typed in their
// display unit must persist as canonical mg/dL. Before the fix the
// editor saved the mmol/L number verbatim (a 5.5 mmol/L target stored
// as 5.5 mg/dL, or rejected outright by the 40–400 mg/dL bounds).
it("converts a display-unit value back to canonical mg/dL", () => {
expect(mmolToMgdl(5.5)).toBe(99);
expect(mmolToMgdl(7.0)).toBe(126); // ADA diabetes threshold
expect(mmolToMgdl(3.9)).toBe(70); // hypoglycemia threshold
});
// v1.16.16 — "one number, one engine" coherence. The wire series DTO, the
// CSV export, and the FHIR export each convert a stored mg/dL reading to the
// user's display unit through the SAME `convertGlucose` helper, so the three
// surfaces emit an identical value + unit. This guards against any surface
// drifting back to a render-time or hard-coded conversion.
it("emits a coherent value+unit across series / CSV / FHIR for mmol/L", () => {
const storedMgdl = 126; // ADA diabetes threshold
const unit = resolveGlucoseUnit("mmol/L");
// Series route: point value = convertGlucose(row.value, unit).
const seriesValue = convertGlucose(storedMgdl, unit);
// CSV export: BLOOD_GLUCOSE row value = convertGlucose(m.value, unit).
const csvValue = convertGlucose(storedMgdl, unit);
// FHIR export: valueQuantity.value = convertGlucose(stat.latest, unit).
const fhirValue = convertGlucose(storedMgdl, unit);
expect(unit).toBe("mmol/L");
expect(seriesValue).toBe(7);
expect(csvValue).toBe(seriesValue);
expect(fhirValue).toBe(seriesValue);
});
it("emits a coherent value+unit across series / CSV / FHIR for mg/dL", () => {
const storedMgdl = 100;
const unit = resolveGlucoseUnit("mg/dL");
const seriesValue = convertGlucose(storedMgdl, unit);
const csvValue = convertGlucose(storedMgdl, unit);
const fhirValue = convertGlucose(storedMgdl, unit);
expect(unit).toBe("mg/dL");
expect(seriesValue).toBe(100);
expect(csvValue).toBe(seriesValue);
expect(fhirValue).toBe(seriesValue);
});
it("toCanonicalMgdl is the inverse of convertGlucose at the editor boundary", () => {
// mg/dL display unit is already canonical — only rounds.
expect(toCanonicalMgdl(126, "mg/dL")).toBe(126);
expect(toCanonicalMgdl(99.4, "mg/dL")).toBe(99);
// mmol/L display unit converts back; the round-trip is stable to the
// 1-decimal mmol/L the UI shows (≤1 mg/dL drift is unavoidable).
expect(toCanonicalMgdl(7.0, "mmol/L")).toBe(126);
expect(toCanonicalMgdl(mgdlToMmol(126), "mmol/L")).toBeCloseTo(126, -0.5);
expect(toCanonicalMgdl(mgdlToMmol(70), "mmol/L")).toBe(70);
});
});