diff --git a/src/__tests__/features/opds/useOpdsScreenOrchestration.test.tsx b/src/__tests__/features/opds/useOpdsScreenOrchestration.test.tsx index 2783f87..1897983 100644 --- a/src/__tests__/features/opds/useOpdsScreenOrchestration.test.tsx +++ b/src/__tests__/features/opds/useOpdsScreenOrchestration.test.tsx @@ -6,6 +6,7 @@ import { } from "@/features/opds/useOpdsScreenOrchestration"; import { savedCatalogsService } from "@/services/savedCatalogs"; import type { Publication } from "@/types/opds"; +import { notifyOpdsError } from "@/utils/notifyOpdsError"; vi.mock("@/services/savedCatalogs", () => ({ savedCatalogsService: { @@ -13,6 +14,12 @@ vi.mock("@/services/savedCatalogs", () => ({ }, })); +vi.mock("@/utils/notifyOpdsError", () => ({ + notifyOpdsError: vi.fn(), +})); + +const mockNotify = vi.mocked(notifyOpdsError); + const makePublication = (id: string): Publication => ({ id, title: "Dune", @@ -112,12 +119,35 @@ describe("useOpdsScreenOrchestration", () => { ); }); - it("is best-effort: swallows a save failure without bumping the refresh key", async () => { + it("is best-effort: swallows a save failure without bumping the refresh key and surfaces it via notifyOpdsError", async () => { vi.mocked(savedCatalogsService.save).mockRejectedValue(new Error("backend down")); const { result } = render(); await expect(result.current.handleSaveCatalog()).resolves.toBeUndefined(); expect(result.current.savedCatalogsKey).toBe(0); + expect(mockNotify).toHaveBeenCalledTimes(1); + expect(mockNotify).toHaveBeenCalledWith( + expect.objectContaining({ message: "backend down" }), + { context: "Save catalog", fallback: "Failed to save the catalog" }, + ); + }); + + it("does not toast when the save succeeds", async () => { + vi.mocked(savedCatalogsService.save).mockResolvedValue({ + id: "cat-ok", + name: "https://example.com/opds", + url: "https://example.com/opds", + username: "alice", + added_at: "2026-08-28T00:00:00Z", + }); + const { result } = render(); + + await act(async () => { + await result.current.handleSaveCatalog(); + }); + + expect(result.current.savedCatalogsKey).toBe(1); + expect(mockNotify).not.toHaveBeenCalled(); }); }); diff --git a/src/features/opds/useOpdsScreenOrchestration.ts b/src/features/opds/useOpdsScreenOrchestration.ts index 98e5e85..ba71e82 100644 --- a/src/features/opds/useOpdsScreenOrchestration.ts +++ b/src/features/opds/useOpdsScreenOrchestration.ts @@ -1,6 +1,7 @@ import { useCallback, useState } from "react"; import { savedCatalogsService } from "@/services/savedCatalogs"; import type { Publication } from "@/types/opds"; +import { notifyOpdsError } from "@/utils/notifyOpdsError"; import type { UseCatalogConnectionResult } from "./useCatalogConnection"; import type { UseDownloadRegistryResult } from "./useDownloadRegistry"; @@ -36,8 +37,12 @@ export function useOpdsScreenOrchestration({ connection.username, ); setSavedCatalogsKey((k) => k + 1); - } catch { - // Non-fatal: saving the catalog is best-effort. + } catch (error) { + // Non-fatal: saving the catalog is best-effort, but surface it. + notifyOpdsError(error, { + context: "Save catalog", + fallback: "Failed to save the catalog", + }); } }, [connection]);