From 03bf21e54496c85411f531b770e64ec4c1ea37c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 19:39:47 +0200 Subject: [PATCH] fix(e2e): assert dashboard insight card container, not literal AI text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /insights mother page no longer fetches the `/api/insights/general-status` endpoint — InsightAdvisorCard reads `/api/insights/generate` (a POST that doubles as cache-aware read), gated by `/api/insights/comprehensive` returning a non-null payload. The old test mocked a dead endpoint, hit the EmptyState CTA path, and asserted on a literal AI prose substring that the renderer never produces verbatim — twice brittle. Swap the mocks to the real endpoints with a minimal-but-valid `InsightResult` payload, then assert on the populated card's stable structural hook `[data-slot="insight-summary"]`. The hook is set by `InsightAdvisorCard` on the populated-body branch only, so the assertion still proves "the card mounts its summary when the payload arrives" — the actual contract under test — without coupling to AI prose that varies per provider and cache state by design. --- e2e/dashboard.spec.ts | 90 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 12 deletions(-) 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