diff --git a/src/app/insights/__tests__/assessment-spine-order.test.ts b/src/app/insights/__tests__/assessment-spine-order.test.ts index acde8bac2..91347efb0 100644 --- a/src/app/insights/__tests__/assessment-spine-order.test.ts +++ b/src/app/insights/__tests__/assessment-spine-order.test.ts @@ -33,17 +33,29 @@ const PAGES_DIR = join(process.cwd(), "src", "app", "insights"); // `useInsightStatus`-backed pages render ``; the // medications page reads the richer `summary`-shaped route inline, so it // renders `` directly. -const BESPOKE_PAGES: ReadonlyArray<{ slug: string; assessmentTag: string }> = [ +const BESPOKE_PAGES: ReadonlyArray<{ + slug: string; + assessmentTag: string; + /** + * v1.12.8 — pulse intentionally trails its assessment with the + * cardio-fitness CTA (the operator-requested "Einschätzung above the VO₂ + * max cross-link" order). The CTA is a navigation affordance, not a data + * card, so it is the one sanctioned block that may render after the + * assessment; the guard skips the last-block check for these slugs but + * still enforces that the target card precedes the assessment. + */ + assessmentMayTrail?: boolean; +}> = [ { slug: "weight", assessmentTag: " { - for (const { slug, assessmentTag } of BESPOKE_PAGES) { + for (const { slug, assessmentTag, assessmentMayTrail } of BESPOKE_PAGES) { it(`renders the assessment card as the last SubPageShell child on /insights/${slug}`, () => { const source = readFileSync( join(PAGES_DIR, slug, "page.tsx"), @@ -68,25 +80,29 @@ describe("bespoke metric-detail spine — assessment is the last block", () => { `${slug}: assessment card ${assessmentTag} not found inside the shell`, ).toBeGreaterThan(-1); - // All six bespoke pages render the assessment as a self-closing - // element. Find the `/>` that closes it, then assert nothing but - // whitespace separates that close from ``. The icon - // passed as a prop (``) self-closes BEFORE the element's own - // `/>`, so the first `/>` after the assessment open is the icon's and - // the second is the element's — scan for the last `/>` in the body, - // which belongs to the trailing assessment element by construction. - const selfClose = body.lastIndexOf("/>"); - expect( - selfClose, - `${slug}: assessment card is not self-closing as expected`, - ).toBeGreaterThan(assessmentIndex); + // Pages where the assessment is the last block (the default) must have + // nothing but whitespace after the assessment element's `/>`. Pages that + // intentionally trail the assessment with a navigation CTA + // (`assessmentMayTrail`, today only pulse) skip the last-block check — + // the target-before-assessment guard below still applies. + if (!assessmentMayTrail) { + // The assessment renders as a self-closing element. The icon passed + // as a prop (``) self-closes BEFORE the element's own `/>`, + // so the last `/>` in the body belongs to the trailing assessment + // element by construction. + const selfClose = body.lastIndexOf("/>"); + expect( + selfClose, + `${slug}: assessment card is not self-closing as expected`, + ).toBeGreaterThan(assessmentIndex); - const trailing = body.slice(selfClose + 2).trim(); - expect( - trailing, - `${slug}: "${trailing.slice(0, 40)}" renders AFTER the assessment card — ` + - `the assessment must be the last block on the canonical spine`, - ).toBe(""); + const trailing = body.slice(selfClose + 2).trim(); + expect( + trailing, + `${slug}: "${trailing.slice(0, 40)}" renders AFTER the assessment card — ` + + `the assessment must be the last block on the canonical spine`, + ).toBe(""); + } // v1.12.4 — the target card precedes the assessment when present. const targetIndex = body.indexOf(" 0; const pulseSummary = analytics?.summaries?.PULSE ?? null; - // v1.12.7 — brushed-window stats shared between the pulse chart and the + // v1.12.8 — visible-range stats shared between the pulse chart and the // strip (the VO2 chart-row below keeps its own full-range read). - const { statsByType, onDomainStats } = useChartDomainStats(); + const { statsByType, onVisibleStats } = useChartDomainStats(); // v1.4.27 F17 — gate the sub-page on at least one pulse observation. // Brand-new accounts (no manual logs, no Apple-Health upload yet) @@ -139,41 +140,44 @@ export default function InsightsPulsPage() { chartKey="pulse" types={["PULSE"]} title={t("charts.pulse")} + titleIcon={Heart} colors={["#50fa7b"]} unit="bpm" valueBands={pulseBands} compareBaseline={compareBaseline} userTimezone={user?.timezone} - selectableDomain - onDomainStats={onDomainStats} + onVisibleStats={onVisibleStats} /> + {/* v1.12.0 — Pulse owns the mood × pulse correlation (relocated off + the overview onto its metric page). */} + + + {/* v1.12.8 — the Einschätzung (assessment) sits ABOVE the cardio-fitness + CTA so the plain-language read of the pulse data leads, and the + cross-link to the dedicated VO₂ max page trails it. */} + } /> + {/* VO₂ max is a cardio-fitness metric (Apple's Health app surfaces it under "Heart"), so the pulse page links across to its dedicated `/insights/cardio-fitness` page — the single surface that carries the full chart plus the plain-language assessment — rather than - duplicating a second, divergent VO₂ max view here. */} + duplicating a second, divergent VO₂ max view here. + + v1.12.8 — the card now leads with the canonical `` so the + `Gauge` glyph reads in the foreground colour at the same size and + position as every other tile header, with the body + CTA beneath. */} {hasVo2 ? ( - -