From 72c9cd6578d8191a8038fdbc48f40d4decf67359 Mon Sep 17 00:00:00 2001 From: nikit Date: Mon, 6 Jul 2026 20:29:46 -0400 Subject: [PATCH] fix(settings): handle preferences fetch failure instead of hanging The /api/user/preferences fetch in useEffect only chained .then(), with setLoading(false) called inside it. A rejected fetch (network drop, or a 5xx whose body throws in .json()) left loading stuck at true forever, so the page never left the Skeleton. Add .catch to surface an error toast and .finally to always clear loading. Added a regression test (confirmed it fails without the fix, passes with it) since this bug is easy to silently reintroduce. Fixes #20 --- .../(app)/settings/__tests__/page.test.tsx | 59 +++++++++++++++++++ src/app/(app)/settings/page.tsx | 7 ++- 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/app/(app)/settings/__tests__/page.test.tsx diff --git a/src/app/(app)/settings/__tests__/page.test.tsx b/src/app/(app)/settings/__tests__/page.test.tsx new file mode 100644 index 0000000..ae0f035 --- /dev/null +++ b/src/app/(app)/settings/__tests__/page.test.tsx @@ -0,0 +1,59 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { render, screen, cleanup, waitFor } from "@testing-library/react"; +import SettingsPage from "../page"; + +vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh: vi.fn() }) })); +vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } })); +vi.mock("@/components/providers/theme-provider", () => ({ + useTheme: () => ({ + preferences: { + accentTheme: "amber", + fontStyle: "sans", + layoutDensity: "default", + currency: "USD", + weekStart: "monday", + dateFormat: "MMM d, yyyy", + timeFormat: "24h", + colorMode: "system", + }, + updatePreferences: vi.fn(), + }), +})); +vi.mock("@/components/providers/sections-provider", () => ({ + useSections: () => ({ + enabledSections: [], + customSections: [], + updateSections: vi.fn(), + updateCustomSections: vi.fn(), + }), +})); + +import { toast } from "sonner"; + +describe("SettingsPage", () => { + beforeEach(() => { + global.fetch = vi.fn((url: string) => { + if (url === "/api/user/preferences") { + return Promise.reject(new Error("network error")); + } + return Promise.resolve({ ok: true, json: async () => ({ shares: [] }) }); + }) as unknown as typeof fetch; + }); + + afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + }); + + it("clears the loading skeleton and surfaces an error when the preferences fetch fails", async () => { + render(); + + // Loaded-branch-only content must not be present while loading. + expect(screen.queryByText("Personalize your planner")).not.toBeInTheDocument(); + + await waitFor(() => + expect(screen.getByText("Personalize your planner")).toBeInTheDocument() + ); + expect(toast.error).toHaveBeenCalledWith("Failed to load settings"); + }); +}); diff --git a/src/app/(app)/settings/page.tsx b/src/app/(app)/settings/page.tsx index 8e3fc15..a134af8 100644 --- a/src/app/(app)/settings/page.tsx +++ b/src/app/(app)/settings/page.tsx @@ -123,8 +123,11 @@ export default function SettingsPage() { setTargetDaysPerWeek(data.gymConfig?.targetDaysPerWeek ?? 3); setAiProviderSetting(data.aiProvider || ""); setHasAiKey(data.hasAiKey || false); - setLoading(false); - }); + }) + .catch(() => { + toast.error("Failed to load settings"); + }) + .finally(() => setLoading(false)); }, []); useEffect(() => {