mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Explain a blood-pressure score in its own popover
Three lines now sit above the reference band: which axis set the number and how far it was from its ceiling, what the scale means at the ceiling and at 100, and that the pair shown is a recency-weighted average rather than a reading anyone took. In all six languages.
This commit is contained in:
1 parent
0ce40b0950
commit
cf7bb13ee8
2 files changed
+260
No files matched your search
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* v1.34.5 — the route from the blood-pressure score to its reason.
|
||||
*
|
||||
* A pillar reading 56 gave no way to find out why: the popover held the
|
||||
* reference band, the personal target and the source, and nothing said
|
||||
* which of the two axes had set the number, how far over its ceiling that
|
||||
* axis sat, or that a reading exactly at the ceiling reads 85 rather than
|
||||
* 100. These pin the three lines that answer it, in the two locales the
|
||||
* maintainer reads, against the real bundles.
|
||||
*
|
||||
* The lines live behind a Radix popover, which only mounts once opened,
|
||||
* so they are asserted through the pure formatter the card feeds it —
|
||||
* the same strings the card shows, not a second copy of the copy.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider, useTranslations } from "@/lib/i18n/context";
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import type { BpScoreBasis } from "@/lib/analytics/bp-grade";
|
||||
import type { ScorePillarResult } from "@/lib/analytics/score/types";
|
||||
import {
|
||||
pillarDetailLines,
|
||||
type Translate,
|
||||
} from "../health-score-pillar-detail";
|
||||
|
||||
function translator(locale: Locale = "en"): Translate {
|
||||
let captured: Translate | null = null;
|
||||
function Probe() {
|
||||
captured = useTranslations().t;
|
||||
return null;
|
||||
}
|
||||
renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<Probe />
|
||||
</I18nProvider>,
|
||||
);
|
||||
if (!captured) throw new Error("translator probe did not run");
|
||||
return captured;
|
||||
}
|
||||
|
||||
function bloodPressure(scoreBasis?: BpScoreBasis): ScorePillarResult {
|
||||
return {
|
||||
id: "BLOOD_PRESSURE",
|
||||
domain: "cardiometabolic",
|
||||
result: {
|
||||
status: "ok",
|
||||
value: {
|
||||
score: 56,
|
||||
...(scoreBasis ? { scoreBasis } : {}),
|
||||
observed: {
|
||||
value: 137,
|
||||
unit: "mmHg",
|
||||
label: "137/87 mmHg",
|
||||
asOf: "2026-07-30T08:00:00.000Z",
|
||||
sources: ["MANUAL"],
|
||||
},
|
||||
reference: {
|
||||
kind: "clinical-threshold",
|
||||
low: 120,
|
||||
high: 129,
|
||||
label: "120 to 129/70 to 79 mmHg",
|
||||
source: "ESH 2023",
|
||||
},
|
||||
noiseFloor: 1,
|
||||
deltaEligible: true,
|
||||
deltaIdentity: "graded_bp_pair_series",
|
||||
},
|
||||
coverage: {
|
||||
requiredInputs: 12,
|
||||
presentInputs: 12,
|
||||
historyDays: 60,
|
||||
missing: [],
|
||||
},
|
||||
confidence: { score: 100, band: "high" },
|
||||
provenance: {
|
||||
inputs: ["BLOOD_PRESSURE"],
|
||||
source: "live",
|
||||
windowDays: 90,
|
||||
computedAt: "2026-07-30T12:00:00.000Z",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ABOVE_CEILING: BpScoreBasis = {
|
||||
axis: "systolic",
|
||||
relation: "above_ceiling",
|
||||
offsetMmHg: 8,
|
||||
boundaryMmHg: 129,
|
||||
};
|
||||
|
||||
describe("blood-pressure score basis in the popover", () => {
|
||||
it("leads with the binding axis, its distance and its ceiling", () => {
|
||||
const lines = pillarDetailLines(bloodPressure(ABOVE_CEILING), {
|
||||
t: translator(),
|
||||
glucoseUnit: "mg/dL",
|
||||
});
|
||||
expect(lines[0]).toBe(
|
||||
"Systolic sets this score: about 8 mmHg over its ceiling of 129 mmHg.",
|
||||
);
|
||||
});
|
||||
|
||||
it("names the diastolic axis when that is the one that bound", () => {
|
||||
const lines = pillarDetailLines(
|
||||
bloodPressure({
|
||||
axis: "diastolic",
|
||||
relation: "above_ceiling",
|
||||
offsetMmHg: 9,
|
||||
boundaryMmHg: 79,
|
||||
}),
|
||||
{ t: translator(), glucoseUnit: "mg/dL" },
|
||||
);
|
||||
expect(lines[0]).toBe(
|
||||
"Diastolic sets this score: about 9 mmHg over its ceiling of 79 mmHg.",
|
||||
);
|
||||
});
|
||||
|
||||
it("measures a hypotensive reading from the low-pressure floor", () => {
|
||||
const lines = pillarDetailLines(
|
||||
bloodPressure({
|
||||
axis: "systolic",
|
||||
relation: "below_floor",
|
||||
offsetMmHg: 4,
|
||||
boundaryMmHg: 90,
|
||||
}),
|
||||
{ t: translator(), glucoseUnit: "mg/dL" },
|
||||
);
|
||||
expect(lines[0]).toBe(
|
||||
"Systolic sets this score: about 4 mmHg under the low-pressure floor of 90 mmHg.",
|
||||
);
|
||||
});
|
||||
|
||||
it("says both axes are in band without naming a distance", () => {
|
||||
const lines = pillarDetailLines(
|
||||
bloodPressure({
|
||||
axis: "systolic",
|
||||
relation: "in_band",
|
||||
offsetMmHg: 5,
|
||||
boundaryMmHg: 129,
|
||||
}),
|
||||
{ t: translator(), glucoseUnit: "mg/dL" },
|
||||
);
|
||||
expect(lines[0]).toBe(
|
||||
"Both values are at or below their reference ceilings.",
|
||||
);
|
||||
});
|
||||
|
||||
it("explains the scale and the averaged reading, then the shipped lines", () => {
|
||||
const lines = pillarDetailLines(bloodPressure(ABOVE_CEILING), {
|
||||
t: translator(),
|
||||
glucoseUnit: "mg/dL",
|
||||
});
|
||||
expect(lines[1]).toBe(
|
||||
"At the ceiling the score is 85; 100 means roughly 12 mmHg below it.",
|
||||
);
|
||||
// The window comes off the pillar's own provenance, so the sentence
|
||||
// cannot drift from the window the score was actually read over.
|
||||
expect(lines[2]).toBe(
|
||||
"Based on a recency-weighted average of the last 90 days, so it may not match any single reading.",
|
||||
);
|
||||
expect(lines).toHaveLength(5);
|
||||
expect(lines[3]).toContain("ESH 2023");
|
||||
expect(lines[4]).toContain("Source: MANUAL");
|
||||
});
|
||||
|
||||
it("reads in German too", () => {
|
||||
const lines = pillarDetailLines(bloodPressure(ABOVE_CEILING), {
|
||||
t: translator("de"),
|
||||
glucoseUnit: "mg/dL",
|
||||
});
|
||||
expect(lines[0]).toBe(
|
||||
"Ausschlaggebend: Systolisch, etwa 8 mmHg über der Obergrenze von 129 mmHg.",
|
||||
);
|
||||
expect(lines[1]).toBe(
|
||||
"An der Obergrenze liegt der Wert bei 85; 100 bedeutet rund 12 mmHg darunter.",
|
||||
);
|
||||
expect(lines[2]).toBe(
|
||||
"Beruht auf einem Durchschnitt der letzten 90 Tage, der neuere Messungen stärker gewichtet, und kann deshalb von jeder einzelnen Messung abweichen.",
|
||||
);
|
||||
});
|
||||
|
||||
it("says nothing at all when the server published no basis", () => {
|
||||
// The field is optional and blood-pressure-only: an older payload, or
|
||||
// a pillar that has nothing of the kind to say, must fall back to the
|
||||
// shipped lines rather than to an empty or invented sentence.
|
||||
const lines = pillarDetailLines(bloodPressure(), {
|
||||
t: translator(),
|
||||
glucoseUnit: "mg/dL",
|
||||
});
|
||||
expect(lines).toHaveLength(2);
|
||||
expect(lines[0]).toContain("ESH 2023");
|
||||
expect(lines.join("\n")).not.toContain("mmHg over its ceiling");
|
||||
});
|
||||
|
||||
it("leaves other pillars untouched", () => {
|
||||
const glycaemia: ScorePillarResult = {
|
||||
...bloodPressure(ABOVE_CEILING),
|
||||
id: "GLYCAEMIA",
|
||||
};
|
||||
const lines = pillarDetailLines(glycaemia, {
|
||||
t: translator(),
|
||||
glucoseUnit: "mg/dL",
|
||||
});
|
||||
expect(lines.join("\n")).not.toContain("sets this score");
|
||||
expect(lines.join("\n")).not.toContain("At the ceiling");
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,8 @@
|
||||
* (low-to-mid 50s), not in the catastrophic band.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
gradeBpScore,
|
||||
gradeBpScoreFromSeries,
|
||||
@@ -474,3 +476,53 @@ describe("gradeBpScoreFromSeries — the basis travels with the score", () => {
|
||||
expect(personal.score).toBeGreaterThan(clinical.score);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The popover's scale line quotes two numbers out of this curve ("at the
|
||||
* ceiling the score is 85; 100 means roughly 12 mmHg below it") because
|
||||
* the sentence has to read naturally in six languages. That makes the
|
||||
* copy and the anchors two places that must agree, so this pins them to
|
||||
* each other: re-anchoring the curve turns this red and the six strings
|
||||
* have to follow.
|
||||
*/
|
||||
describe("the scale the popover quotes", () => {
|
||||
const AT_CEILING_SCORE = 85;
|
||||
const OPTIMAL_OFFSET_MMHG = 12;
|
||||
|
||||
it("still scores 85 at the ceiling and 100 at twelve below it", () => {
|
||||
expect(
|
||||
bpScore({ sys: UNDER65.sysHigh, dia: UNDER65.diaHigh, target: UNDER65 }),
|
||||
).toBe(AT_CEILING_SCORE);
|
||||
expect(
|
||||
bpScore({
|
||||
sys: UNDER65.sysHigh - OPTIMAL_OFFSET_MMHG,
|
||||
dia: UNDER65.diaHigh - OPTIMAL_OFFSET_MMHG,
|
||||
target: UNDER65,
|
||||
}),
|
||||
).toBe(100);
|
||||
// Twelve is the SMALLEST distance that reaches 100 — one less does not.
|
||||
expect(
|
||||
bpScore({
|
||||
sys: UNDER65.sysHigh - (OPTIMAL_OFFSET_MMHG - 1),
|
||||
dia: UNDER65.diaHigh - (OPTIMAL_OFFSET_MMHG - 1),
|
||||
target: UNDER65,
|
||||
}),
|
||||
).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it.each(["en", "de", "fr", "es", "it", "pl"])(
|
||||
"quotes those same two numbers in %s",
|
||||
(locale) => {
|
||||
const bundle = JSON.parse(
|
||||
readFileSync(
|
||||
join(__dirname, "../../../../messages", `${locale}.json`),
|
||||
"utf8",
|
||||
),
|
||||
) as { insights: { healthScore: Record<string, string> } };
|
||||
const line = bundle.insights.healthScore.bpScale;
|
||||
expect(line).toContain(String(AT_CEILING_SCORE));
|
||||
expect(line).toContain(String(OPTIMAL_OFFSET_MMHG));
|
||||
expect(line).toContain("100");
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user