From 6485aae7877a94bc6d5f4c585f8e8cf007df7f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 9 Aug 2026 04:15:12 +0200 Subject: [PATCH] fix(insights): surface mood-insights read failures instead of silent degradation The mood-insights regions returned nothing on a failed read by documented intent, degrading to the line chart. Enforce the honest-error standard here too: on error the main region paints a QueryErrorRow with retry, once, mirroring how the loading skeleton is scoped to that one region so the three regions do not stack identical rows. Test watched the silent empty first. --- ...ood-insights-sections-error-state.test.tsx | 53 +++++++++++++++++++ .../insights/mood/mood-insights-sections.tsx | 12 ++++- 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/components/insights/mood/__tests__/mood-insights-sections-error-state.test.tsx diff --git a/src/components/insights/mood/__tests__/mood-insights-sections-error-state.test.tsx b/src/components/insights/mood/__tests__/mood-insights-sections-error-state.test.tsx new file mode 100644 index 000000000..7df15c84d --- /dev/null +++ b/src/components/insights/mood/__tests__/mood-insights-sections-error-state.test.tsx @@ -0,0 +1,53 @@ +/** + * The mood-insights regions used to render nothing on a failed read, by + * documented intent ("degrades gracefully to the line chart"). The maintainer + * decision is to enforce §6 here like everywhere else: a failed read is not an + * empty page. The shared query is forced into its error state; the main "rest" + * region has to surface a `query-error-row` with retry (mirroring the loading + * convention, which also only paints in the "rest" region). + */ +import { describe, it, expect, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ + user: { id: "u1" }, + isLoading: false, + isAuthenticated: true, + error: null, + refetch: () => {}, + }), +})); + +vi.mock("@tanstack/react-query", () => ({ + useQuery: () => ({ + data: undefined, + isLoading: false, + isError: true, + refetch: vi.fn(), + }), +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { MoodInsightsSections } from "../mood-insights-sections"; + +function render(region: "heatmap" | "assessment" | "rest") { + return renderToStaticMarkup( + + + , + ); +} + +describe(" — a read failure is honest, not empty", () => { + it("surfaces a query-error row with retry in the rest region", () => { + const html = render("rest"); + expect(html).toContain('data-slot="query-error-row"'); + expect(html).toContain('data-slot="query-error-row-retry"'); + }); + + it("stays quiet in the heatmap/assessment regions to avoid stacking rows", () => { + expect(render("heatmap")).toBe(""); + expect(render("assessment")).toBe(""); + }); +}); diff --git a/src/components/insights/mood/mood-insights-sections.tsx b/src/components/insights/mood/mood-insights-sections.tsx index be65c1124..a5de9a854 100644 --- a/src/components/insights/mood/mood-insights-sections.tsx +++ b/src/components/insights/mood/mood-insights-sections.tsx @@ -7,6 +7,7 @@ import { CalendarDays, Sparkles } from "lucide-react"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { TileHeader } from "@/components/insights/tile-header"; +import { QueryErrorRow } from "@/components/ui/query-error-row"; import { Skeleton } from "@/components/ui/skeleton"; import { useAuth } from "@/hooks/use-auth"; import { queryKeys } from "@/lib/query-keys"; @@ -100,7 +101,7 @@ export function MoodInsightsSections({ const { isAuthenticated } = useAuth(); const { t } = useTranslations(); - const { data, isLoading } = useQuery({ + const { data, isLoading, isError, refetch } = useQuery({ queryKey: queryKeys.moodInsights(), queryFn: async () => { return apiGet("/api/mood/insights"); @@ -118,6 +119,15 @@ export function MoodInsightsSections({ ) : null; } + if (isError) { + // §6 — a failed read is not an empty page. Surface it once, in the main + // "rest" region (mirroring the loading convention above), rather than + // stacking an identical row in all three regions down the page. + return region === "rest" ? ( + refetch()} /> + ) : null; + } + if (!data || data.summary.totalEntries === 0) { return null; }