Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 40 additions & 20 deletions app/pdf-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -127,26 +129,44 @@ export default function PdfViewerScreen() {
),
}}
/>
<Pdf
key={viewMode}
ref={pdfRef}
source={{ uri }}
style={styles.pdf}
trustAllCerts={false}
fitPolicy={0}
enablePaging={viewMode === "single"}
horizontal={viewMode === "single"}
page={initialPage ? Number(initialPage) : 1}
onLoadComplete={(numberOfPages, _path, _size, toc) => {
setTotalPages(numberOfPages);
setTableOfContents(toc ?? []);
}}
onPageChanged={(page) => setCurrentPage(page)}
onError={(error) => {
Sentry.captureException(error);
console.error("PDF Error:", error);
}}
/>
{pdfError ? (
<View className="flex-1 items-center justify-center gap-4 px-8">
<Text className="text-center text-lg text-foreground">
Unable to load this PDF. The file may be temporarily unavailable.
</Text>
<TouchableOpacity
onPress={() => {
setPdfError(false);
setPdfKey((k) => k + 1);
}}
className="rounded-lg bg-accent px-6 py-3"
>
<Text className="text-base font-semibold text-white">Try Again</Text>
</TouchableOpacity>
</View>
) : (
<Pdf
key={`${viewMode}-${pdfKey}`}
ref={pdfRef}
source={{ uri }}
style={styles.pdf}
trustAllCerts={false}
fitPolicy={0}
enablePaging={viewMode === "single"}
horizontal={viewMode === "single"}
page={initialPage ? Number(initialPage) : 1}
onLoadComplete={(numberOfPages, _path, _size, toc) => {
setTotalPages(numberOfPages);
setTableOfContents(toc ?? []);
}}
onPageChanged={(page) => setCurrentPage(page)}
onError={(error) => {
Sentry.captureException(error);
console.error("PDF Error:", error);
setPdfError(true);
}}
/>
)}

<Sheet open={showTocModal} onOpenChange={setShowTocModal}>
<SheetHeader onClose={() => setShowTocModal(false)}>
Expand Down
80 changes: 80 additions & 0 deletions tests/app/pdf-viewer.test.tsx
Original file line number Diff line number Diff line change
@@ -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<string, unknown>, _ref: unknown) => {
mockOnError = props.onError as any;
return <View testID="pdf-component" />;
}),
};
});

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(<PdfViewerScreen />);

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(<PdfViewerScreen />);

act(() => {
mockOnError!(new Error("ENOENT"));
});

fireEvent.press(screen.getByText("Try Again"));

expect(screen.getByTestId("pdf-component")).toBeTruthy();
expect(screen.queryByText("Try Again")).toBeNull();
});
});
Loading