diff --git a/src/PdfReader/reducer.ts b/src/PdfReader/reducer.ts index 743349af..5c46c50c 100644 --- a/src/PdfReader/reducer.ts +++ b/src/PdfReader/reducer.ts @@ -31,20 +31,10 @@ export function makePdfReducer( // navigating to a different page in the same resource) const shouldResetResource = state.resourceIndex !== index; - // check if there is a `?startPage` in the href of the resource we are navigating to - const href = manifest.readingOrder[index].href; - const startPage = getStartPageFromHref(href); - // only go to the start page if we don't have another valid page we are navigating to - // instead. - const isNavigatingToEnd = page === -1; - const requestedPageIsBeforeStartPage = startPage && page < startPage; - const pageNumberToNavigateTo = - !isNavigatingToEnd && requestedPageIsBeforeStartPage ? startPage : page; - const newState = { ...state, resourceIndex: index, - pageNumber: pageNumberToNavigateTo, + pageNumber: page, }; if (shouldResetResource) { return { @@ -113,11 +103,7 @@ export function makePdfReducer( */ // do nothing if we have not parsed the number of pages yet. if (!state.numPages) return state; - const atStartOfResource = isStartOfResource( - state.pageNumber, - args.manifest.readingOrder[state.resourceIndex].href - ); - + const atStartOfResource = state.pageNumber === 1; const atStartOfBook = state.resourceIndex === 0; if (atStartOfResource) { if (atStartOfBook) return state; @@ -126,8 +112,11 @@ export function makePdfReducer( ...goToLocation(state.resourceIndex - 1, -1), }; } - // go to prev page - return goToLocation(state.resourceIndex, state.pageNumber - 1); + // go to prev page, allowing navigation below startPage within the resource + return goToLocation( + state.resourceIndex, + Math.max(1, state.pageNumber - 1) + ); } case 'GO_TO_HREF': { @@ -161,15 +150,24 @@ export function makePdfReducer( // called when the resource has been parsed by react-pdf // and we know the number of pages - case 'PDF_PARSED': + case 'PDF_PARSED': { + const { numPages } = action; + const { pageNumber: currentPage, resourceIndex } = state; + + const currentHref = manifest.readingOrder[resourceIndex]?.href; + const startPage = getStartPageFromHref(currentHref) ?? 0; + + // 1. If -1, go to the end. + // 2. Otherwise, ensure we don't fall below startPage. + const pageNumber = + currentPage === -1 ? numPages : Math.max(currentPage, startPage); + return { ...state, - numPages: action.numPages, - // if the state.pageNumber is -1, we know to navigate to the - // end of the PDF that was just parsed - pageNumber: - state.pageNumber === -1 ? action.numPages : state.pageNumber, + numPages, + pageNumber, }; + } case 'PDF_LOAD_ERROR': return { @@ -255,12 +253,3 @@ function handleInvalidTransition(state: PdfState, action: PdfReaderAction) { ); return state; } - -/** - * Checks if we are at the start of the resource, taking into account the `?startPage` - * query param. - */ -function isStartOfResource(pageNumber: number, resourceHref: string) { - const startPage = getStartPageFromHref(resourceHref); - return pageNumber === (startPage ?? 1); -} diff --git a/tests/PdfReducer.test.ts b/tests/PdfReducer.test.ts new file mode 100644 index 00000000..d0d7afd3 --- /dev/null +++ b/tests/PdfReducer.test.ts @@ -0,0 +1,84 @@ +import { makePdfReducer } from '../src/PdfReader/reducer'; +import { PdfReaderArguments, PdfState } from '../src/PdfReader/types'; +import { DEFAULT_FIT_MODE, DEFAULT_SETTINGS } from '../src/constants'; + +function makeArgs(href: string): PdfReaderArguments { + return { + manifest: { + metadata: { title: 'Test' }, + readingOrder: [{ href, type: 'application/pdf' }], + }, + } as unknown as PdfReaderArguments; +} + +const baseState: PdfState = { + state: 'ACTIVE', + settings: DEFAULT_SETTINGS, + resourceIndex: 0, + resource: null, + pageNumber: 1, + numPages: null, + scale: 1, + pdfWidth: 0, + pdfHeight: 0, + pageHeight: undefined, + pageWidth: undefined, + atStart: true, + atEnd: false, + rendered: false, + fitMode: DEFAULT_FIT_MODE, + rotation: 0, +}; + +describe('makePdfReducer — PDF_PARSED', () => { + it('keeps page 1 when no start query param is present', () => { + const args = makeArgs('https://example.com/doc.pdf'); + const reducer = makePdfReducer(args); + const state = reducer(baseState, { type: 'PDF_PARSED', numPages: 100 }); + expect(state.pageNumber).toBe(1); + }); + + it('jumps to start on initial load when pageNumber is below start', () => { + const args = makeArgs('https://example.com/doc.pdf?start=15'); + const reducer = makePdfReducer(args); + const state = reducer(baseState, { type: 'PDF_PARSED', numPages: 100 }); + expect(state.pageNumber).toBe(15); + }); + + it('does not override pageNumber when user has already navigated past start', () => { + const args = makeArgs('https://example.com/doc.pdf?start=15'); + const reducer = makePdfReducer(args); + const navigatedState = { ...baseState, pageNumber: 20 }; + const state = reducer(navigatedState, { + type: 'PDF_PARSED', + numPages: 100, + }); + expect(state.pageNumber).toBe(20); + }); + + it('navigates to last page when pageNumber is -1', () => { + const args = makeArgs('https://example.com/doc.pdf'); + const reducer = makePdfReducer(args); + const endState = { ...baseState, pageNumber: -1 }; + const state = reducer(endState, { type: 'PDF_PARSED', numPages: 100 }); + expect(state.pageNumber).toBe(100); + }); +}); + +describe('makePdfReducer — GO_BACKWARD', () => { + it('allows navigating back below start page within the same resource', () => { + const args = makeArgs('https://example.com/doc.pdf?start=5'); + const reducer = makePdfReducer(args); + const onStartPage = { ...baseState, pageNumber: 5, numPages: 100 }; + const state = reducer(onStartPage, { type: 'GO_BACKWARD' }); + expect(state.pageNumber).toBe(4); + }); + + it('does not go below page 1', () => { + const args = makeArgs('https://example.com/doc.pdf'); + const reducer = makePdfReducer(args); + const onFirstPage = { ...baseState, pageNumber: 1, numPages: 100 }; + const state = reducer(onFirstPage, { type: 'GO_BACKWARD' }); + expect(state.pageNumber).toBe(1); + }); +});