import type { Page } from "@playwright/test"; import { SECTION_MOBILITY, SECTION_VITALS, } from "@/components/insights/derived/use-dashboard-derived"; import { SCORE_VERSION } from "@/lib/analytics/score/types"; /** * A POPULATED Insights overview fixture. * * The mobile-overflow guard exists to prove that a page carrying real content * does not scroll sideways. Against the shared e2e account — which owns no * measurements — `/insights` resolves to its empty state, and an empty page * trivially fits any viewport. Every section below therefore gets a body: * populated wellness scores, a full vitals grid, and the "signals of the day" * card whose heading carries the long plain-language provenance caption. * * The strings are deliberately long. Prose in a heading's trailing `action` * slot is exactly what widened the page in the first place, and a fixture with * short labels would not reproduce it. */ const NOW = "2026-01-15T08:30:00.000Z"; function provenance(inputs: string[]) { return { inputs, source: "DAY" as const, windowDays: 30, computedAt: NOW, }; } function coverage() { return { requiredInputs: 4, presentInputs: 4, historyDays: 90, missing: [] as string[], }; } function confidence() { return { score: 88, band: "high" as const }; } function deviation( type: string, value: number, low: number, high: number, direction: "above" | "below" | "in", ) { return { type, value, center: (low + high) / 2, low, high, outside: direction !== "in", direction, }; } /** * The "signals of the day" payload in its `fired` state — the branch that * renders `SectionHeading action={}`, i.e. a full * sentence pinned into the heading row. */ const COINCIDENT_FIRED = { metric: "COINCIDENT_DEVIATION", status: "ok" as const, value: { fired: true, day: "2026-01-15", illnessExplained: false, vitals: [ deviation("RESTING_HEART_RATE", 71, 52, 62, "above"), deviation("HEART_RATE_VARIABILITY", 38, 45, 78, "below"), deviation("RESPIRATORY_RATE", 17.4, 12.5, 16.2, "above"), deviation("OXYGEN_SATURATION", 97, 96, 99, "in"), deviation("BODY_TEMPERATURE", 36.9, 36.2, 37.1, "in"), ], contributing: [ deviation("RESTING_HEART_RATE", 71, 52, 62, "above"), deviation("HEART_RATE_VARIABILITY", 38, 45, 78, "below"), deviation("RESPIRATORY_RATE", 17.4, 12.5, 16.2, "above"), ], }, coverage: coverage(), confidence: confidence(), provenance: provenance([ "RESTING_HEART_RATE", "HEART_RATE_VARIABILITY", "RESPIRATORY_RATE", ]), reason: null, }; /** * A populated Health Score report for the hero band's score column. * * The panel is the widest-content block in the band: a fixed-width label * column, a fill bar, a value column and a popover trigger all on one 20 px * line, plus a coalesced failed-read line that carries an inline button. An * empty score leaves the column unmounted entirely, so without this the * phone-width guard measured a band that had no panel in it. * * Every state the panel can paint is present at once: scored rows, a failed * read with its retry, safety signposting, and counted absence. */ function scorePillar(id: string, domain: string, score: number) { return { id, domain, result: { status: "ok" as const, value: { score, observed: { value: 126, unit: "mmHg", label: "126/78 mmHg", asOf: NOW, sources: ["MANUAL"], }, reference: { kind: "clinical-threshold" as const, low: 120, high: 129, label: "120 to 129/70 to 79 mmHg", source: "ESH 2023", }, noiseFloor: 1, deltaEligible: true, deltaIdentity: id, }, coverage: coverage(), confidence: confidence(), provenance: provenance([id]), }, }; } function gatedPillar(id: string, domain: string, reason: string) { return { id, domain, result: { status: "insufficient" as const, coverage: { requiredInputs: 4, presentInputs: 0, historyDays: 0, missing: [] as string[], }, provenance: provenance([id]), reason, }, }; } const HEALTH_SCORE = { composite: { status: "ok" as const, value: { score: 74, band: "green" as const, bandSetter: "SLEEP", composition: ["BLOOD_PRESSURE", "ACTIVITY", "SLEEP", "ADIPOSITY"], configured: false, // v1.38 — four pillars across four areas of health, so this is a // full-breadth score and the panel adds no basis line. It rides // anyway, because the wire carries the block on every ok composite // and a fixture missing a field the server always sends stops // exercising the branch a person meets. scoreBasis: { domains: 4, recommended: 3, tier: "full" as const, physiological: true, }, noiseFloor: 3, scoreVersion: SCORE_VERSION, }, coverage: coverage(), confidence: confidence(), provenance: provenance(["BLOOD_PRESSURE", "ACTIVITY", "SLEEP"]), }, pillars: [ scorePillar("BLOOD_PRESSURE", "cardiometabolic", 56), scorePillar("ACTIVITY", "activity", 88), scorePillar("SLEEP", "sleep", 41), scorePillar("ADIPOSITY", "adiposity", 72), gatedPillar("LIPIDS", "cardiometabolic", "read_failed"), gatedPillar("WELLBEING", "wellbeing", "crisis_signposting"), gatedPillar("GLYCAEMIA", "cardiometabolic", "not_tracked"), ], delta: 2, deltaReason: null, scoreVersion: SCORE_VERSION, weightGoal: { status: "ok" as const, value: { currentKg: 81, target: { min: 74, max: 78 }, distanceKg: 3, deltaKg: 1, asOf: NOW, source: "MANUAL", }, coverage: coverage(), confidence: confidence(), provenance: provenance(["WEIGHT"]), }, algorithmNotice: null, restMode: null, }; /** A deterministic sparkline series around `center`. */ function series(center: number, spread: number): number[] { return Array.from({ length: 30 }, (_, i) => Number((center + Math.sin(i / 2.5) * spread).toFixed(2)), ); } /** * A populated `WellnessScoreValue` entry — the exact wire shape the three * persisted score types share (`RECOVERY_SCORE` / `STRESS_SCORE` / * `STRAIN_SCORE`). */ function wellnessScore( metric: string, score: number, band: "green" | "yellow" | "red", ) { return { metric, status: "ok" as const, value: { score, band, trendDelta: 3.4, daysInWindow: 28, asOf: NOW, series: series(score, 6), }, coverage: coverage(), confidence: confidence(), provenance: provenance(["SLEEP_DURATION", "RESTING_HEART_RATE"]), reason: null, assessment: null, }; } /** A populated `VitalsBaselineValue` tile — exact wire shape. */ function vitalsBaseline(type: string, center: number, spread: number) { return { metric: "VITALS_BASELINE", status: "ok" as const, value: { type, center, low: Number((center - spread).toFixed(2)), high: Number((center + spread).toFixed(2)), spread, sampleDays: 30, k: 2, series: series(center, spread / 2), }, coverage: coverage(), confidence: confidence(), provenance: provenance([type]), reason: null, }; } /** * The honest "not enough history" arm. Metrics whose value shapes this fixture * does not reproduce exactly are served as `insufficient` rather than as an * invented payload — that is a real state those tiles handle (they un-mount), * and a wrong-shaped `ok` payload would throw in the tile instead of testing * anything. */ function insufficient(metric: string) { return { metric, status: "insufficient" as const, value: null, coverage: { requiredInputs: 4, presentInputs: 1, historyDays: 3, missing: ["SLEEP_DURATION"], }, confidence: null, provenance: provenance([]), reason: "not_enough_history", }; } /** Center + spread per vital, in each metric's own units. */ const VITAL_BANDS: Record = { RESTING_HEART_RATE: [57, 5], RESPIRATORY_RATE: [14.1, 1.8], OXYGEN_SATURATION: [97, 1.5], BODY_TEMPERATURE: [36.6, 0.45], BLOOD_GLUCOSE: [92, 13], WEIGHT: [79.4, 3], }; function buildBatchMetrics(): Record { const metrics: Record = { RECOVERY_SCORE: wellnessScore("RECOVERY_SCORE", 66, "yellow"), STRESS_SCORE: wellnessScore("STRESS_SCORE", 41, "green"), STRAIN_SCORE: wellnessScore("STRAIN_SCORE", 72, "yellow"), READINESS: insufficient("READINESS"), SLEEP_SCORE: insufficient("SLEEP_SCORE"), FITNESS_AGE: insufficient("FITNESS_AGE"), VASCULAR_AGE_DELTA: insufficient("VASCULAR_AGE_DELTA"), HRV_BALANCE: insufficient("HRV_BALANCE"), BMI: insufficient("BMI"), SIX_MINUTE_WALK_BAND: insufficient("SIX_MINUTE_WALK_BAND"), }; for (const type of SECTION_VITALS) { if (type === "HEART_RATE_VARIABILITY") continue; const [center, spread] = VITAL_BANDS[type] ?? [50, 5]; metrics[`VITALS_BASELINE:${type}`] = vitalsBaseline(type, center, spread); } for (const { metric } of SECTION_MOBILITY) { metrics[metric] = insufficient(metric); } return metrics; } /** * Route-mock a populated Insights overview onto `page`. * * Only the reads that decide whether a section renders AT ALL are stubbed; * everything else (the i18n bundle, every layout decision, the real component * tree) stays authentic, so the geometry under test is the shipped geometry. */ export async function mockPopulatedInsights(page: Page): Promise { // Without a non-empty comprehensive payload the page short-circuits to its // "no data yet" empty state and no section mounts at all. // RegExp matchers throughout, not globs: Playwright's URL glob treats `?` as // a single-character wildcard, so `**/api/insights/derived?*` also matches // `/api/insights/derived/batch?…` and the two handlers shadow each other. await page.route(/\/api\/insights\/comprehensive(\?|$)/, (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: { totalMeasurements: 4821, moodSummary: { count: 96 }, medications: [{ id: "med-1" }, { id: "med-2" }, { id: "med-3" }], }, error: null, }), }), ); // The batched derived read behind the wellness strip + the vitals grid. await page.route(/\/api\/insights\/derived\/batch(\?|$)/, (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: { metrics: buildBatchMetrics() }, error: null, }), }), ); // The hero band's score column. The real response is fetched and the score // injected into it, so every OTHER field the page reads off `/api/analytics` // stays authentic and this fixture cannot silently blank a section it was // never meant to touch. await page.route(/\/api\/analytics(\?|$)/, async (route) => { const response = await route.fetch(); let payload: { data?: Record | null; error?: unknown }; try { payload = await response.json(); } catch { payload = { data: {}, error: null }; } await route.fulfill({ response, contentType: "application/json", body: JSON.stringify({ ...payload, data: { ...(payload.data ?? {}), healthScore: HEALTH_SCORE }, }), }); }); // The single-metric derived route. Only COINCIDENT_DEVIATION is stubbed — // it is the card whose heading carries the provenance sentence. Any other // metric falls through to the real handler. await page.route(/\/api\/insights\/derived\?/, async (route) => { const metric = new URL(route.request().url()).searchParams.get("metric"); if (metric !== "COINCIDENT_DEVIATION") return route.fallback(); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: COINCIDENT_FIRED, error: null }), }); }); }