diff --git a/src/components/measurements/__tests__/measurement-form-resting-heart-rate.test.ts b/src/components/measurements/__tests__/measurement-form-resting-heart-rate.test.ts new file mode 100644 index 000000000..5a2074a91 --- /dev/null +++ b/src/components/measurements/__tests__/measurement-form-resting-heart-rate.test.ts @@ -0,0 +1,45 @@ +/** + * The manual-entry form offers the resting heart rate (#1079). + * + * Resting heart rate has its own chart and series, separate from spot pulse. + * A value read off a watch or taken on waking had no row in the form, so it + * could only be logged as a pulse, which feeds a different chart. + */ +import { describe, it, expect } from "vitest"; + +import { VALUE_RANGES, getUnitForType } from "@/lib/validations/measurement"; +import { + MEASUREMENT_FORM_TYPE_VALUES, + MEASUREMENT_TYPES, +} from "@/components/measurements/measurement-form"; +import en from "../../../../messages/en.json"; + +describe("manual entry of the resting heart rate", () => { + it("offers it as its own type, next to pulse", () => { + expect(MEASUREMENT_FORM_TYPE_VALUES).toContain("RESTING_HEART_RATE"); + const values = MEASUREMENT_TYPES.map((t) => t.value); + expect(values.indexOf("RESTING_HEART_RATE")).toBe( + values.indexOf("PULSE") + 1, + ); + }); + + it("uses the server's unit, a labelled row and a plausible placeholder", () => { + const row = MEASUREMENT_TYPES.find((t) => t.value === "RESTING_HEART_RATE"); + expect(row).toBeDefined(); + expect(row && "unit" in row ? row.unit : undefined).toBe( + getUnitForType("RESTING_HEART_RATE"), + ); + expect( + (en as { measurements: Record }).measurements + .typeRestingHeartRate, + ).toBeTruthy(); + const range = VALUE_RANGES.RESTING_HEART_RATE; + const placeholder = Number( + row && "placeholder" in row ? row.placeholder : NaN, + ); + if (range) { + expect(placeholder).toBeGreaterThanOrEqual(range.min); + expect(placeholder).toBeLessThanOrEqual(range.max); + } + }); +}); diff --git a/src/components/measurements/measurement-form.tsx b/src/components/measurements/measurement-form.tsx index 092bc2e7a..662a6c310 100644 --- a/src/components/measurements/measurement-form.tsx +++ b/src/components/measurements/measurement-form.tsx @@ -67,6 +67,14 @@ export const MEASUREMENT_TYPES = [ unit: "bpm", placeholder: "72", }, + // The resting heart rate has its own chart and series; a value read off a + // watch or taken on waking belongs there rather than among spot pulses. + { + value: "RESTING_HEART_RATE", + labelKey: "measurements.typeRestingHeartRate", + unit: "bpm", + placeholder: "58", + }, { value: "BODY_FAT", labelKey: "measurements.typeBodyFat", diff --git a/src/lib/modules/surface.ts b/src/lib/modules/surface.ts index e0d3ba08d..efc803457 100644 --- a/src/lib/modules/surface.ts +++ b/src/lib/modules/surface.ts @@ -100,6 +100,9 @@ const STATIC_SURFACE_MODULE = { "insights-page:six-minute-walk": "recovery", "insights-page:stair-ascent-speed": "recovery", "insights-page:stair-descent-speed": "recovery", + // The resting heart rate is the recovery module's `resting_hr` domain; its + // page, trend slot, correlation channel and summary key follow it. + "insights-page:resting-pulse": "recovery", // ── Insights overview blocks ── "overview:cycle-summary": "cycle", @@ -115,6 +118,7 @@ const STATIC_SURFACE_MODULE = { // ── Trends row ── "trend:mood": "mood", "trend:sleep": "sleep", + "trend:resting_hr": "recovery", // ── Add menu ── "capture:mood": "mood", @@ -141,6 +145,9 @@ const STATIC_SURFACE_MODULE = { "summary:SLEEP_DURATION": "sleep", "summary:BLOOD_GLUCOSE": "glucose", "summary:CARDIO_RECOVERY": "recovery", + // The recovery module owns the resting_hr domain (`measurement-scope.ts`), + // so the server gates the type on it; the form and the dashboard follow. + "summary:RESTING_HEART_RATE": "recovery", "summary:SIX_MINUTE_WALK_DISTANCE": "recovery", "summary:STAIR_ASCENT_SPEED": "recovery", "summary:STAIR_DESCENT_SPEED": "recovery", @@ -189,6 +196,7 @@ const STATIC_SURFACE_MODULE = { "correlation:MOOD": "mood", "correlation:SLEEP_DURATION": "sleep", "correlation:BLOOD_GLUCOSE": "glucose", + "correlation:RESTING_HEART_RATE": "recovery", "correlation:MEDICATION_COMPLIANCE": "medications", // Symptom burden is read from the illness journal. "correlation:SYMPTOM_SEVERITY": "illness",