import { beforeAll, describe, expect, it, vi } from 'vitest'; import { createRef } from 'react'; import { fireEvent, getByTestId, render } from '@testing-library/react'; import { pdfjs } from './index.test.js'; import Document from './Document.js'; import DocumentContext from './DocumentContext.js'; import Page from './Page.js'; import { makeAsyncCallback, loadPDF, muteConsole, restoreConsole } from '../../../test-utils.js'; import type { PDFDocumentProxy } from 'pdfjs-dist'; import type { ScrollPageIntoViewArgs } from './shared/types.js'; import type LinkService from './LinkService.js'; const pdfFile = loadPDF('./../../__mocks__/_pdf.pdf'); const pdfFile2 = loadPDF('./../../__mocks__/_pdf2.pdf'); const OK = Symbol('OK'); function ChildInternal({ renderMode, rotate, }: { renderMode?: string | null; rotate?: number | null; }) { return
; } function Child(props: React.ComponentProps) { return ( {(context) => } ); } async function waitForAsync() { await new Promise((resolve) => { setTimeout(resolve, 0); }); } describe('Document', () => { // Object with basic loaded PDF information that shall match after successful loading const desiredLoadedPdf: Partial = {}; const desiredLoadedPdf2: Partial = {}; beforeAll(async () => { const pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise; desiredLoadedPdf._pdfInfo = pdf._pdfInfo; const pdf2 = await pdfjs.getDocument({ data: pdfFile2.arrayBuffer }).promise; desiredLoadedPdf2._pdfInfo = pdf2._pdfInfo; }); describe('loading', () => { it('loads a file and calls onSourceSuccess and onLoadSuccess callbacks via data URI properly', async () => { const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK); const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); render( , ); expect.assertions(2); await expect(onSourceSuccessPromise).resolves.toBe(OK); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]); }); it('loads a file and calls onSourceSuccess and onLoadSuccess callbacks via data URI properly (param object)', async () => { const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK); const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); render( , ); expect.assertions(2); await expect(onSourceSuccessPromise).resolves.toBe(OK); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]); }); // FIXME: In Jest, it used to be worked around as described in https://github.com/facebook/jest/issues/7780 it.skip('loads a file and calls onSourceSuccess and onLoadSuccess callbacks via ArrayBuffer properly', async () => { const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK); const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); render( , ); expect.assertions(2); await expect(onSourceSuccessPromise).resolves.toBe(OK); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]); }); it('loads a file and calls onSourceSuccess and onLoadSuccess callbacks via Blob properly', async () => { const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK); const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); render( , ); expect.assertions(2); await expect(onSourceSuccessPromise).resolves.toBe(OK); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]); }); it('loads a file and calls onSourceSuccess and onLoadSuccess callbacks via File properly', async () => { const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK); const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); render( , ); expect.assertions(2); await expect(onSourceSuccessPromise).resolves.toBe(OK); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]); }); it('fails to load a file and calls onSourceError given invalid file source', async () => { const { func: onSourceError, promise: onSourceErrorPromise } = makeAsyncCallback(); muteConsole(); // @ts-expect-error-next-line render( null} onSourceError={onSourceError} />); expect.assertions(1); const [error] = await onSourceErrorPromise; expect(error).toMatchObject(expect.any(Error)); restoreConsole(); }); it('replaces a file properly', async () => { const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK); const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { rerender } = render( , ); expect.assertions(4); await expect(onSourceSuccessPromise).resolves.toBe(OK); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]); const { func: onSourceSuccess2, promise: onSourceSuccessPromise2 } = makeAsyncCallback(OK); const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback(); rerender( , ); await expect(onSourceSuccessPromise2).resolves.toBe(OK); await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedPdf2]); }); }); describe('rendering', () => { it('applies className to its wrapper when given a string', () => { const className = 'testClassName'; const { container } = render(); const wrapper = container.querySelector('.react-pdf__Document'); expect(wrapper).toHaveClass(className); }); it('passes container element to inputRef properly', () => { const inputRef = createRef(); render(); expect(inputRef.current).toBeInstanceOf(HTMLDivElement); }); it('renders "No PDF file specified." when given nothing', () => { const { container } = render(); const noData = container.querySelector('.react-pdf__message'); expect(noData).toBeInTheDocument(); expect(noData).toHaveTextContent('No PDF file specified.'); }); it('renders custom no data message when given nothing and noData prop is given', () => { const { container } = render(); const noData = container.querySelector('.react-pdf__message'); expect(noData).toBeInTheDocument(); expect(noData).toHaveTextContent('Nothing here'); }); it('renders custom no data message when given nothing and noData prop is given as a function', () => { const { container } = render( 'Nothing here'} />); const noData = container.querySelector('.react-pdf__message'); expect(noData).toBeInTheDocument(); expect(noData).toHaveTextContent('Nothing here'); }); it('renders "Loading PDF…" when loading a file', async () => { const { container, findByText } = render(); const loading = container.querySelector('.react-pdf__message'); expect(loading).toBeInTheDocument(); expect(await findByText('Loading PDF…')).toBeInTheDocument(); }); it('renders custom loading message when loading a file and loading prop is given', async () => { const { container, findByText } = render(); const loading = container.querySelector('.react-pdf__message'); expect(loading).toBeInTheDocument(); expect(await findByText('Loading')).toBeInTheDocument(); }); it('renders custom loading message when loading a file and loading prop is given as a function', async () => { const { container, findByText } = render( 'Loading'} />, ); const loading = container.querySelector('.react-pdf__message'); expect(loading).toBeInTheDocument(); expect(await findByText('Loading')).toBeInTheDocument(); }); it('renders "Failed to load PDF file." when failed to load a document', async () => { const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback(); const failingPdf = 'data:application/pdf;base64,abcdef'; muteConsole(); const { container, findByText } = render( , ); expect.assertions(2); await onLoadErrorPromise; await waitForAsync(); const error = container.querySelector('.react-pdf__message'); expect(error).toBeInTheDocument(); expect(await findByText('Failed to load PDF file.')).toBeInTheDocument(); restoreConsole(); }); it('renders custom error message when failed to load a document and error prop is given', async () => { const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback(); const failingPdf = 'data:application/pdf;base64,abcdef'; muteConsole(); const { container, findByText } = render( , ); expect.assertions(2); await onLoadErrorPromise; await waitForAsync(); const error = container.querySelector('.react-pdf__message'); expect(error).toBeInTheDocument(); expect(await findByText('Error')).toBeInTheDocument(); restoreConsole(); }); it('renders custom error message when failed to load a document and error prop is given as a function', async () => { const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback(); const failingPdf = 'data:application/pdf;base64,abcdef'; muteConsole(); const { container, findByText } = render( , ); expect.assertions(2); await onLoadErrorPromise; await waitForAsync(); const error = container.querySelector('.react-pdf__message'); expect(error).toBeInTheDocument(); expect(await findByText('Error')).toBeInTheDocument(); restoreConsole(); }); it('passes renderMode prop to its children', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = render( , ); expect.assertions(1); await onLoadSuccessPromise; const child = getByTestId(container, 'child'); expect(child.dataset.rendermode).toBe('custom'); }); it('passes rotate prop to its children', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = render( , ); expect.assertions(1); await onLoadSuccessPromise; const child = getByTestId(container, 'child'); expect(child.dataset.rotate).toBe('90'); }); it('does not overwrite renderMode prop in its children when given renderMode prop to both Document and its children', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = render( , ); expect.assertions(1); await onLoadSuccessPromise; const child = getByTestId(container, 'child'); expect(child.dataset.rendermode).toBe('custom'); }); it('does not overwrite rotate prop in its children when given rotate prop to both Document and its children', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = render( , ); expect.assertions(1); await onLoadSuccessPromise; const child = getByTestId(container, 'child'); expect(child.dataset.rotate).toBe('180'); }); }); describe('viewer', () => { it('calls onItemClick if defined', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const onItemClick = vi.fn(); const instance = createRef<{ linkService: React.RefObject; pages: React.RefObject; viewer: React.RefObject<{ scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void }>; }>(); render( , ); if (!instance.current) { throw new Error('Document ref is not set'); } if (!instance.current.viewer.current) { throw new Error('Viewer ref is not set'); } expect.assertions(2); await onLoadSuccessPromise; const dest: number[] = []; const pageIndex = 5; const pageNumber = 6; // Simulate clicking on an outline item instance.current.viewer.current.scrollPageIntoView({ dest, pageIndex, pageNumber }); expect(onItemClick).toHaveBeenCalledTimes(1); expect(onItemClick).toHaveBeenCalledWith({ dest, pageIndex, pageNumber }); }); it('attempts to find a page and scroll it into view if onItemClick is not given', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const instance = createRef<{ linkService: React.RefObject; // biome-ignore lint/suspicious/noExplicitAny: Intentional use to simplify the test pages: React.RefObject; viewer: React.RefObject<{ scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void }>; }>(); render(); if (!instance.current) { throw new Error('Document ref is not set'); } if (!instance.current.pages.current) { throw new Error('Pages ref is not set'); } if (!instance.current.viewer.current) { throw new Error('Viewer ref is not set'); } expect.assertions(1); await onLoadSuccessPromise; const scrollIntoView = vi.fn(); const dest: number[] = []; const pageIndex = 5; const pageNumber = 6; // Register fake page in Document viewer instance.current.pages.current[pageIndex] = { scrollIntoView }; // Simulate clicking on an outline item instance.current.viewer.current.scrollPageIntoView({ dest, pageIndex, pageNumber }); expect(scrollIntoView).toHaveBeenCalledTimes(1); }); }); describe('linkService', () => { it.each` externalLinkTarget | target ${null} | ${''} ${'_self'} | ${'_self'} ${'_blank'} | ${'_blank'} ${'_parent'} | ${'_parent'} ${'_top'} | ${'_top'} `( 'returns externalLinkTarget = $target given externalLinkTarget prop = $externalLinkTarget', async ({ externalLinkTarget, target }) => { const { func: onRenderAnnotationLayerSuccess, promise: onRenderAnnotationLayerSuccessPromise, } = makeAsyncCallback(); const { container } = render( , ); expect.assertions(1); await onRenderAnnotationLayerSuccessPromise; const link = container.querySelector('a') as HTMLAnchorElement; expect(link.target).toBe(target); }, ); it.each` externalLinkRel | rel ${null} | ${'noopener noreferrer nofollow'} ${'noopener'} | ${'noopener'} ${'noreferrer'} | ${'noreferrer'} ${'nofollow'} | ${'nofollow'} `( 'returns externalLinkRel = $rel given externalLinkRel prop = $externalLinkRel', async ({ externalLinkRel, rel }) => { const { func: onRenderAnnotationLayerSuccess, promise: onRenderAnnotationLayerSuccessPromise, } = makeAsyncCallback(); const { container } = render( , ); expect.assertions(1); await onRenderAnnotationLayerSuccessPromise; const link = container.querySelector('a') as HTMLAnchorElement; expect(link.rel).toBe(rel); }, ); }); it('calls onClick callback when clicked a page (sample of mouse events family)', () => { const onClick = vi.fn(); const { container } = render(); const document = container.querySelector('.react-pdf__Document') as HTMLDivElement; fireEvent.click(document); expect(onClick).toHaveBeenCalled(); }); it('calls onTouchStart callback when touched a page (sample of touch events family)', () => { const onTouchStart = vi.fn(); const { container } = render(); const document = container.querySelector('.react-pdf__Document') as HTMLDivElement; fireEvent.touchStart(document); expect(onTouchStart).toHaveBeenCalled(); }); it('does not warn if file prop was memoized', () => { const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // Intentionally empty }); const file = { data: pdfFile.arrayBuffer }; const { rerender } = render(); rerender(); expect(spy).not.toHaveBeenCalled(); vi.mocked(globalThis.console.error).mockRestore(); }); it('warns if file prop was not memoized', () => { const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // Intentionally empty }); const { rerender } = render(); rerender(); expect(spy).toHaveBeenCalledTimes(1); vi.mocked(globalThis.console.error).mockRestore(); }); it('does not warn if file prop was not memoized, but was changed', () => { const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // Intentionally empty }); const { rerender } = render(); rerender(); expect(spy).not.toHaveBeenCalled(); vi.mocked(globalThis.console.error).mockRestore(); }); it('does not warn if options prop was memoized', () => { const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // Intentionally empty }); const options = {}; const { rerender } = render(); rerender(); expect(spy).not.toHaveBeenCalled(); vi.mocked(globalThis.console.error).mockRestore(); }); it('warns if options prop was not memoized', () => { const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // Intentionally empty }); const { rerender } = render(); rerender(); expect(spy).toHaveBeenCalledTimes(1); vi.mocked(globalThis.console.error).mockRestore(); }); it('does not warn if options prop was not memoized, but was changed', () => { const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // Intentionally empty }); const { rerender } = render(); rerender(); expect(spy).not.toHaveBeenCalled(); vi.mocked(globalThis.console.error).mockRestore(); }); it('does not throw an error on unmount', async () => { const { func: onLoadProgress, promise: onLoadProgressPromise } = makeAsyncCallback(); const { unmount } = render(); await onLoadProgressPromise; expect(unmount).not.toThrowError(); }); });