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.
This commit is contained in:
Marc-André Bombeck committed 2026-08-09 04:15:12 +02:00
1 parent 1229d54d30
commit 6485aae787
2 files changed
+64 -1

No files matched your search

@@ -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(
<I18nProvider initialLocale="en">
<MoodInsightsSections region={region} />
</I18nProvider>,
);
}
describe("<MoodInsightsSections> — 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("");
});
});
@@ -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<MoodInsightsResponse>("/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" ? (
<QueryErrorRow onRetry={() => refetch()} />
) : null;
}
if (!data || data.summary.totalEntries === 0) {
return null;
}