mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
fix(cycle): render mood and glucose rows in the phase crosstab without crashing
The phase-crosstab server emits display "mood" and "glucose", but the client unit/label maps only covered the vital channels, so a surviving mood or glucose phase correlation passed an undefined key to t() and crashed the insights tab. Add the missing maps and harden both unit lookups to fall back to an empty unit on any unmapped display.
This commit is contained in:
1 parent
b5da97cf6e
commit
73d0b0a3a4
8 files changed
+124
-3
No files matched your search
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Basaltemperatur",
|
||||
"metricWristTemperature": "Handgelenkstemperatur nachts",
|
||||
"metricSkinTemperature": "Hauttemperatur",
|
||||
"metricBloodGlucose": "Blutzucker",
|
||||
"metricMood": "Stimmung",
|
||||
"unitHours": "Std.",
|
||||
"unitSteps": "Schritte",
|
||||
"unitBpm": "S/min",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "Pkt.",
|
||||
"confidenceLow": "Geringe Sicherheit",
|
||||
"confidenceMedium": "Mittlere Sicherheit",
|
||||
"confidenceHigh": "Hohe Sicherheit"
|
||||
|
||||
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Basal body temperature",
|
||||
"metricWristTemperature": "Overnight wrist temperature",
|
||||
"metricSkinTemperature": "Skin temperature",
|
||||
"metricBloodGlucose": "Blood glucose",
|
||||
"metricMood": "Mood",
|
||||
"unitHours": "h",
|
||||
"unitSteps": "steps",
|
||||
"unitBpm": "bpm",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "pts",
|
||||
"confidenceLow": "Low confidence",
|
||||
"confidenceMedium": "Medium confidence",
|
||||
"confidenceHigh": "High confidence"
|
||||
|
||||
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Temperatura basal",
|
||||
"metricWristTemperature": "Temperatura de la muñeca nocturna",
|
||||
"metricSkinTemperature": "Temperatura de la piel",
|
||||
"metricBloodGlucose": "Glucosa en sangre",
|
||||
"metricMood": "Estado de ánimo",
|
||||
"unitHours": "h",
|
||||
"unitSteps": "pasos",
|
||||
"unitBpm": "lpm",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "pts",
|
||||
"confidenceLow": "Confianza baja",
|
||||
"confidenceMedium": "Confianza media",
|
||||
"confidenceHigh": "Confianza alta"
|
||||
|
||||
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Température basale",
|
||||
"metricWristTemperature": "Température du poignet la nuit",
|
||||
"metricSkinTemperature": "Température de la peau",
|
||||
"metricBloodGlucose": "Glycémie",
|
||||
"metricMood": "Humeur",
|
||||
"unitHours": "h",
|
||||
"unitSteps": "pas",
|
||||
"unitBpm": "bpm",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "pts",
|
||||
"confidenceLow": "Confiance faible",
|
||||
"confidenceMedium": "Confiance moyenne",
|
||||
"confidenceHigh": "Confiance élevée"
|
||||
|
||||
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Temperatura basale",
|
||||
"metricWristTemperature": "Temperatura del polso notturna",
|
||||
"metricSkinTemperature": "Temperatura della pelle",
|
||||
"metricBloodGlucose": "Glicemia",
|
||||
"metricMood": "Umore",
|
||||
"unitHours": "h",
|
||||
"unitSteps": "passi",
|
||||
"unitBpm": "bpm",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "pts",
|
||||
"confidenceLow": "Affidabilità bassa",
|
||||
"confidenceMedium": "Affidabilità media",
|
||||
"confidenceHigh": "Affidabilità alta"
|
||||
|
||||
@@ -4958,12 +4958,16 @@
|
||||
"metricBasalBodyTemp": "Temperatura podstawowa",
|
||||
"metricWristTemperature": "Nocna temperatura nadgarstka",
|
||||
"metricSkinTemperature": "Temperatura skóry",
|
||||
"metricBloodGlucose": "Glukoza we krwi",
|
||||
"metricMood": "Nastrój",
|
||||
"unitHours": "godz.",
|
||||
"unitSteps": "kroki",
|
||||
"unitBpm": "ud./min",
|
||||
"unitMs": "ms",
|
||||
"unitKg": "kg",
|
||||
"unitCelsius": "°C",
|
||||
"unitGlucose": "mg/dL",
|
||||
"unitMood": "pkt",
|
||||
"confidenceLow": "Niska pewność",
|
||||
"confidenceMedium": "Średnia pewność",
|
||||
"confidenceHigh": "Wysoka pewność"
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import {
|
||||
CyclePhaseCrosstab,
|
||||
CyclePhaseHeadline,
|
||||
type CyclePhaseCrosstabRow,
|
||||
type CyclePhaseCrosstabDisplay,
|
||||
} from "../cycle-phase-crosstab";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
function render(node: React.ReactNode) {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">{node}</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
function row(
|
||||
metricKey: string,
|
||||
display: CyclePhaseCrosstabDisplay,
|
||||
): CyclePhaseCrosstabRow {
|
||||
return {
|
||||
metricKey,
|
||||
display,
|
||||
lutealDays: 12,
|
||||
follicularDays: 11,
|
||||
lutealAvg: 5,
|
||||
follicularAvg: 4,
|
||||
delta: 1,
|
||||
pValue: 0.01,
|
||||
qValue: 0.02,
|
||||
confidence: "high",
|
||||
};
|
||||
}
|
||||
|
||||
describe("<CyclePhaseCrosstab>", () => {
|
||||
// Regression: the server emits `display: "mood"` and `display: "glucose"`,
|
||||
// which the client UNIT_KEY map did not cover. The map miss handed `undefined`
|
||||
// to `t()`, whose resolver runs `key.split(".")` → "Cannot read properties of
|
||||
// undefined (reading 'split')", crashing the whole insights tab.
|
||||
it("renders mood and glucose rows without throwing", () => {
|
||||
expect(() =>
|
||||
render(
|
||||
<CyclePhaseCrosstab
|
||||
rows={[row("mood", "mood"), row("bloodGlucose", "glucose")]}
|
||||
/>,
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("resolves the mood + glucose metric labels and units", () => {
|
||||
const html = render(
|
||||
<CyclePhaseCrosstab
|
||||
rows={[row("mood", "mood"), row("bloodGlucose", "glucose")]}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("Mood");
|
||||
expect(html).toContain("Blood glucose");
|
||||
expect(html).toContain("pts");
|
||||
expect(html).toContain("mg/dL");
|
||||
// Never leaks the raw metric key or the raw i18n key on a covered row.
|
||||
expect(html).not.toContain("cycle.insights.crosstab");
|
||||
});
|
||||
|
||||
it("renders the headline for a mood row without throwing", () => {
|
||||
expect(() =>
|
||||
render(<CyclePhaseHeadline headline={row("mood", "mood")} />),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("degrades an unknown display to an empty unit instead of crashing", () => {
|
||||
const unknown = row(
|
||||
"somethingNew",
|
||||
"unmappedDisplay" as unknown as CyclePhaseCrosstabDisplay,
|
||||
);
|
||||
expect(() => render(<CyclePhaseCrosstab rows={[unknown]} />)).not.toThrow();
|
||||
expect(() =>
|
||||
render(<CyclePhaseHeadline headline={unknown} />),
|
||||
).not.toThrow();
|
||||
// The delta still renders; the unit segment is simply blank (trailing space
|
||||
// trimmed away in markup), never the literal "undefined".
|
||||
const html = render(<CyclePhaseCrosstab rows={[unknown]} />);
|
||||
expect(html).not.toContain("undefined");
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,9 @@ export type CyclePhaseCrosstabDisplay =
|
||||
| "bpm"
|
||||
| "ms"
|
||||
| "kg"
|
||||
| "celsius";
|
||||
| "celsius"
|
||||
| "glucose"
|
||||
| "mood";
|
||||
|
||||
export type CyclePhaseCrosstabConfidence = "low" | "medium" | "high";
|
||||
|
||||
@@ -50,6 +52,8 @@ const METRIC_LABEL_KEY: Record<string, string> = {
|
||||
basalBodyTemp: "cycle.insights.crosstab.metricBasalBodyTemp",
|
||||
wristTemperature: "cycle.insights.crosstab.metricWristTemperature",
|
||||
skinTemperature: "cycle.insights.crosstab.metricSkinTemperature",
|
||||
bloodGlucose: "cycle.insights.crosstab.metricBloodGlucose",
|
||||
mood: "cycle.insights.crosstab.metricMood",
|
||||
};
|
||||
|
||||
const UNIT_KEY: Record<CyclePhaseCrosstabDisplay, string> = {
|
||||
@@ -59,6 +63,8 @@ const UNIT_KEY: Record<CyclePhaseCrosstabDisplay, string> = {
|
||||
ms: "cycle.insights.crosstab.unitMs",
|
||||
kg: "cycle.insights.crosstab.unitKg",
|
||||
celsius: "cycle.insights.crosstab.unitCelsius",
|
||||
glucose: "cycle.insights.crosstab.unitGlucose",
|
||||
mood: "cycle.insights.crosstab.unitMood",
|
||||
};
|
||||
|
||||
const CONFIDENCE_KEY: Record<CyclePhaseCrosstabConfidence, string> = {
|
||||
@@ -107,7 +113,10 @@ export function CyclePhaseHeadline({
|
||||
const metricLabel = t(
|
||||
METRIC_LABEL_KEY[headline.metricKey] ?? headline.metricKey,
|
||||
);
|
||||
const unit = t(UNIT_KEY[headline.display]);
|
||||
// Defensive: an unmapped display must never pass undefined to `t` (whose
|
||||
// resolver does `key.split(".")`). An empty unit is the honest degrade.
|
||||
const unitKey = UNIT_KEY[headline.display];
|
||||
const unit = unitKey ? t(unitKey) : "";
|
||||
const up = headline.delta >= 0;
|
||||
const dir = t(
|
||||
up
|
||||
@@ -151,7 +160,10 @@ export function CyclePhaseCrosstab({
|
||||
const metricLabel = t(
|
||||
METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey,
|
||||
);
|
||||
const unit = t(UNIT_KEY[row.display]);
|
||||
// Defensive: an unmapped display must never pass undefined to `t`
|
||||
// (whose resolver does `key.split(".")`). Empty unit on a miss.
|
||||
const unitKey = UNIT_KEY[row.display];
|
||||
const unit = unitKey ? t(unitKey) : "";
|
||||
// `delta` = lutealAvg − follicularAvg: positive = the vital runs
|
||||
// higher in the luteal phase. The number stays NEUTRAL — higher is
|
||||
// good for steps but bad for resting HR, and this board is
|
||||
|
||||
Reference in new issue
Block a user