import { beforeAll, describe, expect, it } from 'vitest'; import { render } from '@testing-library/react'; import { pdfjs } from '../index.test.js'; import AnnotationLayer from './AnnotationLayer.js'; import LinkService from '../LinkService.js'; import failingPage from '../../../../__mocks__/_failing_page.js'; import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../../test-utils.js'; import DocumentContext from '../DocumentContext.js'; import PageContext from '../PageContext.js'; import type { RenderResult } from '@testing-library/react'; import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist'; import type { Annotations, DocumentContextType, PageContextType } from '../shared/types.js'; const pdfFile = loadPDF('./../../__mocks__/_pdf.pdf'); const annotatedPdfFile = loadPDF('./../../__mocks__/_pdf3.pdf'); function renderWithContext( children: React.ReactNode, documentContext: Partial, pageContext: Partial, ) { const { rerender, ...otherResult } = render( {children} , ); const customRerender = ( nextChildren: React.ReactNode, nextDocumentContext: Partial = documentContext, nextPageContext: Partial = pageContext, ) => rerender( {nextChildren} , ); return { ...otherResult, rerender: customRerender, } as RenderResult & { rerender: typeof customRerender }; } describe('AnnotationLayer', () => { const linkService = new LinkService(); // Loaded PDF file let pdf: PDFDocumentProxy; // Loaded page let page: PDFPageProxy; let page2: PDFPageProxy; // Loaded page text items let desiredAnnotations: Annotations; let desiredAnnotations2: Annotations; beforeAll(async () => { pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise; page = await pdf.getPage(1); desiredAnnotations = await page.getAnnotations(); page2 = await pdf.getPage(2); desiredAnnotations2 = await page2.getAnnotations(); }); describe('loading', () => { it('loads annotations and calls onGetAnnotationsSuccess callback properly', async () => { const { func: onGetAnnotationsSuccess, promise: onGetAnnotationsSuccessPromise } = makeAsyncCallback(); renderWithContext( , { linkService, pdf, }, { onGetAnnotationsSuccess, page, }, ); expect.assertions(1); await expect(onGetAnnotationsSuccessPromise).resolves.toMatchObject([desiredAnnotations]); }); it('calls onGetAnnotationsError when failed to load annotations', async () => { const { func: onGetAnnotationsError, promise: onGetAnnotationsErrorPromise } = makeAsyncCallback(); muteConsole(); renderWithContext( , { linkService, pdf, }, { onGetAnnotationsError, page: failingPage, }, ); expect.assertions(1); await expect(onGetAnnotationsErrorPromise).resolves.toMatchObject([expect.any(Error)]); restoreConsole(); }); it('replaces annotations properly when page is changed', async () => { const { func: onGetAnnotationsSuccess, promise: onGetAnnotationsSuccessPromise } = makeAsyncCallback(); const { rerender } = renderWithContext( , { linkService, pdf, }, { onGetAnnotationsSuccess, page, }, ); expect.assertions(2); await expect(onGetAnnotationsSuccessPromise).resolves.toMatchObject([desiredAnnotations]); const { func: onGetAnnotationsSuccess2, promise: onGetAnnotationsSuccessPromise2 } = makeAsyncCallback(); rerender( , { linkService, pdf, }, { onGetAnnotationsSuccess: onGetAnnotationsSuccess2, page: page2, }, ); await expect(onGetAnnotationsSuccessPromise2).resolves.toMatchObject([desiredAnnotations2]); }); it('throws an error when placed outside Page', () => { muteConsole(); expect(() => render()).toThrow(); restoreConsole(); }); }); describe('rendering', () => { it('renders annotations properly', async () => { const { func: onRenderAnnotationLayerSuccess, promise: onRenderAnnotationLayerSuccessPromise, } = makeAsyncCallback(); const { container } = renderWithContext( , { linkService, pdf, }, { onRenderAnnotationLayerSuccess, page, }, ); expect.assertions(1); await onRenderAnnotationLayerSuccessPromise; const wrapper = container.firstElementChild as HTMLDivElement; const annotationItems = Array.from(wrapper.children); expect(annotationItems).toHaveLength(desiredAnnotations.length); }); it.each` externalLinkTarget | target ${null} | ${''} ${'_self'} | ${'_self'} ${'_blank'} | ${'_blank'} ${'_parent'} | ${'_parent'} ${'_top'} | ${'_top'} `( 'renders all links with target $target given externalLinkTarget = $externalLinkTarget', async ({ externalLinkTarget, target }) => { const { func: onRenderAnnotationLayerSuccess, promise: onRenderAnnotationLayerSuccessPromise, } = makeAsyncCallback(); const customLinkService = new LinkService(); if (externalLinkTarget) { customLinkService.setExternalLinkTarget(externalLinkTarget); } const { container } = renderWithContext( , { linkService: customLinkService, pdf, }, { onRenderAnnotationLayerSuccess, page, }, ); expect.assertions(desiredAnnotations.length); await onRenderAnnotationLayerSuccessPromise; const wrapper = container.firstElementChild as HTMLDivElement; const annotationItems = Array.from(wrapper.children); const annotationLinkItems = annotationItems .map((item) => item.firstChild as HTMLElement) .filter((item) => item.tagName === 'A'); for (const link of annotationLinkItems) { expect(link.getAttribute('target')).toBe(target); } }, ); it.each` externalLinkRel | rel ${null} | ${'noopener noreferrer nofollow'} ${'noopener'} | ${'noopener'} `( 'renders all links with rel $rel given externalLinkRel = $externalLinkRel', async ({ externalLinkRel, rel }) => { const { func: onRenderAnnotationLayerSuccess, promise: onRenderAnnotationLayerSuccessPromise, } = makeAsyncCallback(); const customLinkService = new LinkService(); if (externalLinkRel) { customLinkService.setExternalLinkRel(externalLinkRel); } const { container } = renderWithContext( , { linkService: customLinkService, pdf, }, { onRenderAnnotationLayerSuccess, page, }, ); expect.assertions(desiredAnnotations.length); await onRenderAnnotationLayerSuccessPromise; const wrapper = container.firstElementChild as HTMLDivElement; const annotationItems = Array.from(wrapper.children); const annotationLinkItems = annotationItems .map((item) => item.firstChild as HTMLElement) .filter((item) => item.tagName === 'A'); for (const link of annotationLinkItems) { expect(link.getAttribute('rel')).toBe(rel); } }, ); it('renders annotations with the default imageResourcesPath given no imageResourcesPath', async () => { const pdf = await pdfjs.getDocument({ data: annotatedPdfFile.arrayBuffer }).promise; const annotatedPage = await pdf.getPage(1); const { func: onRenderAnnotationLayerSuccess, promise: onRenderAnnotationLayerSuccessPromise, } = makeAsyncCallback(); const imageResourcesPath = ''; const desiredImageTagRegExp = new RegExp( `]+src="${imageResourcesPath}annotation-note.svg"`, ); const { container } = renderWithContext( , { linkService, pdf, }, { onRenderAnnotationLayerSuccess, page: annotatedPage, }, ); expect.assertions(1); await onRenderAnnotationLayerSuccessPromise; const stringifiedAnnotationLayerNode = container.outerHTML; expect(stringifiedAnnotationLayerNode).toMatch(desiredImageTagRegExp); }); it('renders annotations with the specified imageResourcesPath given imageResourcesPath', async () => { const pdf = await pdfjs.getDocument({ data: annotatedPdfFile.arrayBuffer }).promise; const annotatedPage = await pdf.getPage(1); const { func: onRenderAnnotationLayerSuccess, promise: onRenderAnnotationLayerSuccessPromise, } = makeAsyncCallback(); const imageResourcesPath = '/public/images/'; const desiredImageTagRegExp = new RegExp( `]+src="${imageResourcesPath}annotation-note.svg"`, ); const { container } = renderWithContext( , { imageResourcesPath, linkService, pdf, }, { onRenderAnnotationLayerSuccess, page: annotatedPage, }, ); expect.assertions(1); await onRenderAnnotationLayerSuccessPromise; const stringifiedAnnotationLayerNode = container.outerHTML; expect(stringifiedAnnotationLayerNode).toMatch(desiredImageTagRegExp); }); }); });