mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(charts): name every series by its localised metric label
The chart's label map covered eleven measurement types and fell back to the raw enum for the rest, so a resting-pulse chart with its trend overlays on showed "RESTING_HEART_RATE" in the trend line, the legend, the tooltip and the spoken summary. Types outside the chart's short labels now take their measurement label, which exists for every type in every locale.
This commit is contained in:
1 parent
524975d5f3
commit
f5b63d33ab
2 files changed
+162
-1
No files matched your search
@@ -0,0 +1,137 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import de from "../../../../messages/de.json";
|
||||
import en from "../../../../messages/en.json";
|
||||
import es from "../../../../messages/es.json";
|
||||
import fr from "../../../../messages/fr.json";
|
||||
import it_ from "../../../../messages/it.json";
|
||||
import ko from "../../../../messages/ko.json";
|
||||
import pl from "../../../../messages/pl.json";
|
||||
|
||||
/**
|
||||
* The chart names a series by its localised metric label, never by the enum.
|
||||
*
|
||||
* The label map in the chart covered eleven types and fell back to the raw
|
||||
* type for every other one, so a resting-pulse chart with its trend overlays
|
||||
* on read "RESTING_HEART_RATE" in the trend line, the legend, the tooltip and
|
||||
* the spoken summary. Every overlay series takes its name from the same
|
||||
* helper, so the summary and the trend line stand in for all of them here.
|
||||
*/
|
||||
|
||||
const MESSAGES = { de, en, es, fr, it: it_, ko, pl } as const;
|
||||
|
||||
function buildData(): unknown[] {
|
||||
const out: Array<{
|
||||
date: string;
|
||||
timestamp: number;
|
||||
RESTING_HEART_RATE: number;
|
||||
}> = [];
|
||||
const base = Date.now() - 12 * 3_600_000;
|
||||
for (let i = 0; i < 28; i++) {
|
||||
out.push({
|
||||
date: `p${i}`,
|
||||
timestamp: base - (27 - i) * 86_400_000,
|
||||
RESTING_HEART_RATE: 58 + (i % 5),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({ isAuthenticated: true, user: null, isLoading: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-chart-overlay-prefs", () => ({
|
||||
useChartOverlayPrefs: () => ({
|
||||
prefs: {
|
||||
showTrendIndicator: true,
|
||||
showTrendArrow: true,
|
||||
showTargetRange: true,
|
||||
comparisonBaseline: "none",
|
||||
},
|
||||
setPrefs: () => undefined,
|
||||
isSaving: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("<HealthChart> — series names", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it.each(Object.keys(MESSAGES) as Array<keyof typeof MESSAGES>)(
|
||||
"names the resting-pulse series in %s, overlays on",
|
||||
async (locale) => {
|
||||
const data = buildData();
|
||||
vi.doMock("@tanstack/react-query", () => ({
|
||||
keepPreviousData: (previous: unknown) => previous,
|
||||
useQuery: () => ({ data, isLoading: false }),
|
||||
useQueryClient: () => ({
|
||||
cancelQueries: () => Promise.resolve(),
|
||||
getQueryData: () => undefined,
|
||||
setQueryData: () => undefined,
|
||||
invalidateQueries: () => Promise.resolve(),
|
||||
}),
|
||||
useMutation: () => ({ mutate: () => undefined, isPending: false }),
|
||||
}));
|
||||
|
||||
const { I18nProvider } = await import("@/lib/i18n/context");
|
||||
const { HealthChart } = await import("../health-chart");
|
||||
const messages = MESSAGES[locale];
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale} initialMessages={messages}>
|
||||
<HealthChart
|
||||
types={["RESTING_HEART_RATE"]}
|
||||
title="Resting pulse"
|
||||
unit="bpm"
|
||||
chartKey="pulse"
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
const label = (
|
||||
messages as unknown as { measurements: Record<string, string> }
|
||||
).measurements.typeRestingHeartRate;
|
||||
expect(label).toBeTruthy();
|
||||
|
||||
const aria = html.match(/role="img"[^>]*aria-label="([^"]*)"/)?.[1];
|
||||
expect(aria).toBeTruthy();
|
||||
expect(aria).not.toContain("RESTING_HEART_RATE");
|
||||
|
||||
const visible = html.replace(/<[^>]+>/g, " ");
|
||||
expect(visible).not.toContain("RESTING_HEART_RATE");
|
||||
expect(visible).toContain(label);
|
||||
|
||||
vi.doUnmock("@tanstack/react-query");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("measurementLabelKey", () => {
|
||||
it("matches the canonical label map for every type outside the chart's own labels", async () => {
|
||||
const { MEASUREMENT_TYPE_LABEL_KEYS } =
|
||||
await import("@/lib/measurements/type-label-keys");
|
||||
const { measurementLabelKey } = await import("../health-chart");
|
||||
// The four types whose key breaks the convention carry a chart label.
|
||||
const chartLabelled = new Set([
|
||||
"WEIGHT",
|
||||
"BLOOD_PRESSURE_SYS",
|
||||
"BLOOD_PRESSURE_DIA",
|
||||
"PULSE",
|
||||
"BODY_FAT",
|
||||
"SLEEP_DURATION",
|
||||
"ACTIVITY_STEPS",
|
||||
"BLOOD_GLUCOSE",
|
||||
"TOTAL_BODY_WATER",
|
||||
"BONE_MASS",
|
||||
"OXYGEN_SATURATION",
|
||||
]);
|
||||
const mismatched = Object.entries(MEASUREMENT_TYPE_LABEL_KEYS)
|
||||
.filter(([type]) => !chartLabelled.has(type))
|
||||
.filter(([type, key]) => measurementLabelKey(type) !== key);
|
||||
expect(mismatched).toEqual([]);
|
||||
expect(Object.keys(MEASUREMENT_TYPE_LABEL_KEYS).length).toBeGreaterThan(50);
|
||||
});
|
||||
});
|
||||
@@ -381,6 +381,27 @@ const BASE_TYPE_LABEL_KEYS: Record<string, string> = {
|
||||
OXYGEN_SATURATION: "charts.spo2",
|
||||
};
|
||||
|
||||
/**
|
||||
* The measurement label key for a type outside the chart's short labels:
|
||||
* `RESTING_HEART_RATE` → `measurements.typeRestingHeartRate`. Derived rather
|
||||
* than imported, because the full label map would ride into every route that
|
||||
* draws a chart; `health-chart-type-label.test.tsx` pins that the derivation
|
||||
* matches `MEASUREMENT_TYPE_LABEL_KEYS` for every type it is used for.
|
||||
*/
|
||||
export function measurementLabelKey(type: string): string {
|
||||
return `measurements.type${type
|
||||
.toLowerCase()
|
||||
.split("_")
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join("")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name a series carries in the trend line, the legend, the tooltip and
|
||||
* the spoken summary. The chart's own short labels come first; every other
|
||||
* type takes its measurement label, so no overlay ever shows the raw enum
|
||||
* (a resting-pulse trend read "RESTING_HEART_RATE").
|
||||
*/
|
||||
function getTypeLabel(
|
||||
type: string,
|
||||
valueMode: "raw" | "bmi",
|
||||
@@ -390,7 +411,10 @@ function getTypeLabel(
|
||||
return "BMI";
|
||||
}
|
||||
const key = BASE_TYPE_LABEL_KEYS[type];
|
||||
return key ? t(key) : type;
|
||||
if (key) return t(key);
|
||||
const measurementKey = measurementLabelKey(type);
|
||||
const label = t(measurementKey);
|
||||
return label === measurementKey ? type : label;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user