diff --git a/app/pdf-viewer.tsx b/app/pdf-viewer.tsx index 378c719..6f5ed2d 100644 --- a/app/pdf-viewer.tsx +++ b/app/pdf-viewer.tsx @@ -46,6 +46,8 @@ export default function PdfViewerScreen() { const [showTocModal, setShowTocModal] = useState(false); const [showViewModeModal, setShowViewModeModal] = useState(false); const [isSharing, setIsSharing] = useState(false); + const [pdfError, setPdfError] = useState(false); + const [pdfKey, setPdfKey] = useState(0); useEffect(() => { if (Platform.OS === "android") { @@ -127,26 +129,44 @@ export default function PdfViewerScreen() { ), }} /> - { - setTotalPages(numberOfPages); - setTableOfContents(toc ?? []); - }} - onPageChanged={(page) => setCurrentPage(page)} - onError={(error) => { - Sentry.captureException(error); - console.error("PDF Error:", error); - }} - /> + {pdfError ? ( + + + Unable to load this PDF. The file may be temporarily unavailable. + + { + setPdfError(false); + setPdfKey((k) => k + 1); + }} + className="rounded-lg bg-accent px-6 py-3" + > + Try Again + + + ) : ( + { + setTotalPages(numberOfPages); + setTableOfContents(toc ?? []); + }} + onPageChanged={(page) => setCurrentPage(page)} + onError={(error) => { + Sentry.captureException(error); + console.error("PDF Error:", error); + setPdfError(true); + }} + /> + )} setShowTocModal(false)}> diff --git a/tests/app/pdf-viewer.test.tsx b/tests/app/pdf-viewer.test.tsx new file mode 100644 index 0000000..65009da --- /dev/null +++ b/tests/app/pdf-viewer.test.tsx @@ -0,0 +1,80 @@ +import { fireEvent, render, screen } from "@testing-library/react-native"; + +jest.mock("expo-router", () => ({ + useLocalSearchParams: () => ({ uri: "https://example.com/test.pdf", title: "Test PDF" }), + Stack: { Screen: () => null }, +})); + +jest.mock("expo-navigation-bar", () => ({ + setVisibilityAsync: jest.fn(), + setBehaviorAsync: jest.fn(), +})); + +jest.mock("expo-sharing", () => ({ + isAvailableAsync: jest.fn(), + shareAsync: jest.fn(), +})); + +jest.mock("expo-file-system", () => ({ + File: { downloadFileAsync: jest.fn() }, + Paths: { cache: "/cache" }, +})); + +jest.mock("@/lib/auth-context", () => ({ + useAuth: () => ({ accessToken: "test-token" }), +})); + +jest.mock("@/lib/media-location", () => ({ + updateMediaLocation: jest.fn(), +})); + +let mockOnError: ((e: unknown) => void) | null = null; + +jest.mock("react-native-pdf", () => { + const { forwardRef } = require("react"); + const { View } = require("react-native"); + return { + __esModule: true, + default: forwardRef((props: Record, _ref: unknown) => { + mockOnError = props.onError as any; + return ; + }), + }; +}); + +import PdfViewerScreen from "@/app/pdf-viewer"; +import { act } from "react"; + +describe("PdfViewerScreen", () => { + beforeEach(() => { + mockOnError = null; + }); + + it("shows error view with Try Again button when PDF fails to load", () => { + render(); + + expect(screen.getByTestId("pdf-component")).toBeTruthy(); + expect(screen.queryByText("Try Again")).toBeNull(); + + act(() => { + mockOnError!(new Error("open failed: ENOENT (No such file or directory)")); + }); + + expect(screen.getByText("Try Again")).toBeTruthy(); + expect(screen.getByText(/Unable to load this PDF/)).toBeTruthy(); + expect(screen.queryByTestId("pdf-component")).toBeNull(); + }); + + it("re-mounts PDF component when Try Again is pressed", () => { + render(); + + act(() => { + mockOnError!(new Error("ENOENT")); + }); + + fireEvent.press(screen.getByText("Try Again")); + + expect(screen.getByTestId("pdf-component")).toBeTruthy(); + expect(screen.queryByText("Try Again")).toBeNull(); + }); +});