mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
fix(e2e): assert dashboard insight card container, not literal AI text
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.
This commit is contained in:
1 parent
4849ac7634
commit
03bf21e544
1 file changed
+78
-12
+78
-12
@@ -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
|
||||
|
||||
Reference in new issue
Block a user