diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts index 5f2e445e6..53465f41c 100644 --- a/e2e/dashboard.spec.ts +++ b/e2e/dashboard.spec.ts @@ -110,20 +110,78 @@ test.describe("authenticated dashboard render", () => { }); }); - // The general-status insight card on /insights drives the "AI insight - // card" assertion. We mock its endpoint with a connected, populated - // payload so the card resolves to a real text body instead of the - // "Connect ChatGPT to enable" empty-state. - await page.route("**/api/insights/general-status*", (route) => + // The /insights mother page gates the InsightAdvisorCard on + // `/api/insights/comprehensive` returning a non-null payload + // (`totalMeasurements` is the gate). Mock it so the page renders + // the card surface instead of the EmptyState CTA. + await page.route("**/api/insights/comprehensive", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: { - hasProvider: true, - text: "Your blood-pressure trend is stable. Continue current routine.", - cached: false, - updatedAt: new Date().toISOString(), + summaries: {}, + bmi: 24.2, + bmiClassification: null, + bpClassification: null, + bpPctInTarget: 78, + bpTargets: { sysLow: 120, sysHigh: 140, diaLow: 80, diaHigh: 90 }, + weightBpCorrelation: null, + scatterData: [], + bpMedicationCorrelation: null, + bpMedicationScatterData: [], + medications: [], + alerts: [], + hasOpenAiKey: true, + dataSpanDays: 90, + totalMeasurements: 25, + moodSummary: null, + moodBpCorrelation: null, + moodBpScatterData: [], + moodWeightCorrelation: null, + moodWeightScatterData: [], + moodPulseCorrelation: null, + moodPulseScatterData: [], + }, + error: null, + }), + }), + ); + + // InsightAdvisorCard reads `/api/insights/generate` (a POST that + // doubles as cache-aware read). Mock a minimal-but-valid + // InsightResult so the card renders the populated body branch with + // the `data-slot="insight-summary"` container the assertion below + // pins on. The exact prose is intentionally NOT asserted — AI + // Insights are HealthLog's differentiator and the rendered text + // depends on provider + cache state. The contract under test is + // "an insight card with a populated body is visible", not "the + // string X is on screen". + await page.route("**/api/insights/generate", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + data: { + insights: { + insightType: "general", + summary: "Blood-pressure trend is stable; continue routine.", + classification: "gut", + classificationLabel: "Good", + findings: [], + correlations: [], + primaryRecommendation: "Keep up the current routine.", + recommendations: [], + dataQuality: { + coverage: "good", + gaps: [], + confidence: "hoch", + }, + disclaimer: "Not medical advice.", + }, + cached: true, + cachedAt: new Date().toISOString(), + legacyPayload: false, }, error: null, }), @@ -161,11 +219,19 @@ test.describe("authenticated dashboard render", () => { page.locator(".recharts-wrapper, .recharts-surface").first(), ).toBeVisible({ timeout: 10_000 }); - // AI insight card: visit the dedicated /insights page (which is the - // only surface that mounts the general-status card by default). + // AI insight card: visit the dedicated /insights page (the + // mother surface that mounts the InsightAdvisorCard). We assert + // on the populated-card's stable structural hook + // (`data-slot="insight-summary"`) rather than a literal piece of + // AI prose. AI Insights are evidence-grounded and dynamic by + // design — the rendered text changes per provider + cache state + // and never repeats verbatim, so pinning a literal substring is + // brittle and contradicts the differentiator. The contract under + // test is "the insight card mounts its summary body when the + // payload arrives", which is exactly what the slot signals. await page.goto("/insights", { waitUntil: "domcontentloaded" }); await expect( - page.getByText("Your blood-pressure trend is stable", { exact: false }), + page.locator('[data-slot="insight-summary"]'), ).toBeVisible({ timeout: 10_000 }); // Filter out a small set of known-noise messages — we care about