Merge pull request #1069 from MBombeck/fix/v1397-units

One unit system: every surface follows the reader's glucose and metric/imperial preference
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-10-01 10:34:06 +02:00
commit a3b5480d04
74 files changed
+2304 -292

No files matched your search

@@ -0,0 +1,76 @@
/**
* The settings threshold editor reads glucose in the reader's unit.
*
* It used to pass every glucose threshold through the identity path, so an
* mmol/L account was shown "Default: 70–99 mg/dL" and mg/dL input fields
* bounded at 40–400, while the insights sheet for the same threshold already
* spoke mmol/L. The stored value stays canonical mg/dL either way.
*/
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
const authUser = vi.hoisted(() => ({
current: { glucoseUnit: "mmol/L", unitPreference: "metric" } as Record<
string,
unknown
>,
}));
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({ user: authUser.current, isAuthenticated: true }),
useAccountOnceMounted: () => authUser.current,
}));
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({
isLoading: false,
data: {
effective: {
BLOOD_GLUCOSE_FASTING: {
range: { greenMin: 72, greenMax: 108 },
isOverride: true,
default: { greenMin: 70, greenMax: 99 },
bounds: { min: 40, max: 400, unit: "mg/dL" },
},
},
overrides: { BLOOD_GLUCOSE_FASTING: { min: 72, max: 108 } },
},
}),
useMutation: () => ({ mutate: vi.fn(), isPending: false }),
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
}));
import { I18nProvider } from "@/lib/i18n/context";
import { ThresholdsEditorSection } from "../thresholds-editor-section";
function render(): string {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<ThresholdsEditorSection id="thresholds" />
</I18nProvider>,
);
}
describe("<ThresholdsEditorSection> glucose unit", () => {
it("states the default, the fields and the bounds in mmol/L", () => {
authUser.current = { glucoseUnit: "mmol/L", unitPreference: "metric" };
const html = render();
// 70–99 mg/dL is 3.9–5.5 mmol/L.
expect(html).toMatch(/3\.9–5\.5 mmol\/L/);
expect(html).not.toMatch(/70(\.0)?–99(\.0)? mg\/dL/);
// The stored 72–108 mg/dL override seeds 4–6 mmol/L.
expect(html).toContain('value="4"');
expect(html).toContain('value="6"');
// Inward-rounded guardrail of 40–400 mg/dL.
expect(html).toContain('min="2.3"');
expect(html).toContain('max="22.1"');
});
it("keeps mg/dL for an mg/dL reader", () => {
authUser.current = { glucoseUnit: "mg/dL", unitPreference: "metric" };
const html = render();
expect(html).toMatch(/70(\.0)?–99(\.0)? mg\/dL/);
expect(html).toContain('value="72"');
expect(html).toContain('min="40"');
});
});
@@ -237,7 +237,7 @@ function MetricRow({
}: MetricRowProps) {
const { t } = useTranslations();
const fmt = useFormatters();
const { preference } = useUnitDisplay();
const { preference, glucoseUnit } = useUnitDisplay();
const canonicalBounds = METRIC_BOUNDS[metric];
// v1.32.27 — the row reads and writes in the user's preferred unit
@@ -246,12 +246,14 @@ function MetricRow({
// canonical, so it converts on the way into the fields and inverts on
// the way back out in `onSave`. Guardrails round inward so a value
// typed at the displayed limit still passes the server's canonical
// check. Metrics without a transform — and every metric for a metric
// user — take the adapter's identity path untouched.
// check. The glucose thresholds read in the reader's glucose unit the
// same way. Metrics without a transform — and every metric on the default
// preferences — take the adapter's identity path untouched.
const units = resolveTargetUnitAdapter(
metric,
canonicalBounds.unit,
preference,
glucoseUnit,
);
const bounds = { ...units.bounds(canonicalBounds), unit: units.unit };