mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
One-shot prettier pass so the new format:check gate starts from a clean baseline. Formatting only — no behaviour change. Generator-owned and vendored artifacts stay untouched via .prettierignore.
494 lines
16 KiB
TypeScript
494 lines
16 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
|
|
import {
|
|
adherenceRatePercent,
|
|
buildLedgerCompliance,
|
|
collapseMeasurementsToCanonical,
|
|
normaliseDays,
|
|
normaliseDateRange,
|
|
resolveMaxMedicationAdministrations,
|
|
sanitisePracticeName,
|
|
type CanonicalCollapseRow,
|
|
type DoctorReportComplianceMedication,
|
|
type DoctorReportComplianceIntake,
|
|
} from "../doctor-report-data";
|
|
import { pickCanonicalSourceRows } from "@/lib/analytics/source-priority";
|
|
import { userDayKey } from "@/lib/tz/resolver";
|
|
import {
|
|
buildComplianceMedicationContext,
|
|
tallyComplianceFromLedger,
|
|
} from "@/lib/analytics/compliance";
|
|
import {
|
|
reconstructSleepNights,
|
|
summarizeSleepNights,
|
|
type SleepStageRow,
|
|
} from "@/lib/analytics/sleep-night";
|
|
|
|
describe("resolveMaxMedicationAdministrations", () => {
|
|
it("defaults to 5000 when unset or non-numeric", () => {
|
|
expect(resolveMaxMedicationAdministrations(undefined)).toBe(5000);
|
|
expect(resolveMaxMedicationAdministrations("")).toBe(5000);
|
|
expect(resolveMaxMedicationAdministrations("abc")).toBe(5000);
|
|
expect(resolveMaxMedicationAdministrations("1000.5")).toBe(5000);
|
|
});
|
|
|
|
it("accepts a valid integer within [1, 50000]", () => {
|
|
expect(resolveMaxMedicationAdministrations("1")).toBe(1);
|
|
expect(resolveMaxMedicationAdministrations("1000")).toBe(1000);
|
|
expect(resolveMaxMedicationAdministrations("50000")).toBe(50000);
|
|
});
|
|
|
|
it("falls back to 5000 for out-of-range values", () => {
|
|
expect(resolveMaxMedicationAdministrations("0")).toBe(5000);
|
|
expect(resolveMaxMedicationAdministrations("-1")).toBe(5000);
|
|
expect(resolveMaxMedicationAdministrations("50001")).toBe(5000);
|
|
});
|
|
});
|
|
|
|
describe("adherenceRatePercent", () => {
|
|
it("rounds to an integer percent capped at 100, matching the app card", () => {
|
|
// 87.3 → 87 (the divergence the export previously printed as 87.3).
|
|
expect(adherenceRatePercent(157, 180)).toBe(87);
|
|
// Exact half rounds up like the app's Math.round.
|
|
expect(adherenceRatePercent(1, 2)).toBe(50);
|
|
expect(adherenceRatePercent(5, 6)).toBe(83);
|
|
// Full adherence is a clean 100.
|
|
expect(adherenceRatePercent(10, 10)).toBe(100);
|
|
});
|
|
|
|
it("never exceeds 100 even when taken outruns the denominator", () => {
|
|
expect(adherenceRatePercent(12, 10)).toBe(100);
|
|
});
|
|
|
|
it("returns null when there is no expected dose", () => {
|
|
expect(adherenceRatePercent(0, 0)).toBeNull();
|
|
expect(adherenceRatePercent(3, 0)).toBeNull();
|
|
expect(adherenceRatePercent(1, -1)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("normaliseDays", () => {
|
|
it("returns the value when it's a valid integer in [1, 365]", () => {
|
|
expect(normaliseDays(1)).toBe(1);
|
|
expect(normaliseDays(90)).toBe(90);
|
|
expect(normaliseDays(365)).toBe(365);
|
|
});
|
|
|
|
it("falls back to 90 for invalid input", () => {
|
|
expect(normaliseDays(undefined)).toBe(90);
|
|
expect(normaliseDays(0)).toBe(90);
|
|
expect(normaliseDays(-1)).toBe(90);
|
|
expect(normaliseDays(366)).toBe(90);
|
|
expect(normaliseDays(7.5)).toBe(90);
|
|
expect(normaliseDays("90")).toBe(90);
|
|
expect(normaliseDays(null)).toBe(90);
|
|
});
|
|
|
|
it("respects a custom fallback", () => {
|
|
expect(normaliseDays(undefined, 30)).toBe(30);
|
|
});
|
|
});
|
|
|
|
describe("normaliseDateRange", () => {
|
|
const NOW = new Date("2026-05-09T12:00:00.000Z");
|
|
|
|
it("uses the explicit range when both dates are valid and span <= 730 days", () => {
|
|
const range = normaliseDateRange(
|
|
{
|
|
startDate: "2026-02-01T00:00:00.000Z",
|
|
endDate: "2026-05-01T00:00:00.000Z",
|
|
},
|
|
NOW,
|
|
);
|
|
expect(range.start.toISOString()).toBe("2026-02-01T00:00:00.000Z");
|
|
expect(range.end.toISOString()).toBe("2026-05-01T00:00:00.000Z");
|
|
// 89-day span rounded up to the inclusive day-count.
|
|
expect(range.days).toBe(89);
|
|
});
|
|
|
|
it("falls back to last-90-days when range is missing", () => {
|
|
const range = normaliseDateRange({}, NOW);
|
|
expect(range.days).toBe(90);
|
|
expect(range.end.toISOString()).toBe(NOW.toISOString());
|
|
const expectedStart = new Date(NOW);
|
|
expectedStart.setDate(expectedStart.getDate() - 90);
|
|
expect(range.start.toISOString()).toBe(expectedStart.toISOString());
|
|
});
|
|
|
|
it("falls back when endDate is before startDate", () => {
|
|
const range = normaliseDateRange(
|
|
{
|
|
startDate: "2026-05-01T00:00:00.000Z",
|
|
endDate: "2026-02-01T00:00:00.000Z",
|
|
},
|
|
NOW,
|
|
);
|
|
expect(range.days).toBe(90);
|
|
});
|
|
|
|
it("falls back when range exceeds 730 days", () => {
|
|
const range = normaliseDateRange(
|
|
{
|
|
startDate: "2023-01-01T00:00:00.000Z",
|
|
endDate: "2026-01-01T00:00:00.000Z",
|
|
},
|
|
NOW,
|
|
);
|
|
expect(range.days).toBe(90);
|
|
});
|
|
|
|
it("accepts the maximum 730-day window", () => {
|
|
const range = normaliseDateRange(
|
|
{
|
|
startDate: "2024-05-09T00:00:00.000Z",
|
|
endDate: "2026-05-09T00:00:00.000Z",
|
|
},
|
|
NOW,
|
|
);
|
|
expect(range.days).toBeGreaterThan(700);
|
|
expect(range.days).toBeLessThanOrEqual(730);
|
|
});
|
|
|
|
it("falls back when startDate is unparseable", () => {
|
|
const range = normaliseDateRange(
|
|
{ startDate: "not-a-date", endDate: "2026-05-01T00:00:00.000Z" },
|
|
NOW,
|
|
);
|
|
expect(range.days).toBe(90);
|
|
});
|
|
|
|
it("uses the legacy days fallback when only days is provided", () => {
|
|
const range = normaliseDateRange({ days: 30 }, NOW);
|
|
expect(range.days).toBe(30);
|
|
expect(range.end.toISOString()).toBe(NOW.toISOString());
|
|
});
|
|
|
|
it("equal start+end produces a 1-day window (not zero)", () => {
|
|
const range = normaliseDateRange(
|
|
{
|
|
startDate: "2026-05-09T00:00:00.000Z",
|
|
endDate: "2026-05-09T00:00:00.000Z",
|
|
},
|
|
NOW,
|
|
);
|
|
expect(range.days).toBe(1);
|
|
});
|
|
|
|
it("ignores non-object input", () => {
|
|
const range = normaliseDateRange("not-an-object", NOW);
|
|
expect(range.days).toBe(90);
|
|
});
|
|
});
|
|
|
|
describe("sanitisePracticeName", () => {
|
|
it("returns null for non-string input", () => {
|
|
expect(sanitisePracticeName(undefined)).toBe(null);
|
|
expect(sanitisePracticeName(null)).toBe(null);
|
|
expect(sanitisePracticeName(42)).toBe(null);
|
|
expect(sanitisePracticeName({})).toBe(null);
|
|
});
|
|
|
|
it("returns null for whitespace-only or empty strings", () => {
|
|
expect(sanitisePracticeName("")).toBe(null);
|
|
expect(sanitisePracticeName(" ")).toBe(null);
|
|
expect(sanitisePracticeName("\t\n ")).toBe(null);
|
|
});
|
|
|
|
it("trims and collapses whitespace", () => {
|
|
expect(sanitisePracticeName(" Dr. Smith ")).toBe("Dr. Smith");
|
|
expect(sanitisePracticeName("Praxis\tDr.\nMüller")).toBe(
|
|
"Praxis Dr. Müller",
|
|
);
|
|
});
|
|
|
|
it("strips ASCII control characters", () => {
|
|
expect(sanitisePracticeName("Dr.Smith")).toBe("Dr. Smith");
|
|
expect(sanitisePracticeName(" |