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:
Marc-André Bombeck committed 2026-07-31 11:08:54 +02:00
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");
},
);
});