Merge pull request #5180 from overleaf/msm-pdf-js-viewer-tests

Unit Tests for `pdf-js-viewer.js`

GitOrigin-RevId: 4bd38cf98c598e2ea30791052b9c24568d92c6b8
This commit is contained in:
June Kelly 2021-10-06 09:33:11 +01:00 committed by Copybot
parent 44d0a8d162
commit 1e7dbeeb94
5 changed files with 159 additions and 26 deletions

View file

@ -33,15 +33,19 @@ function PdfJsViewer({ url }) {
// create the viewer when the container is mounted // create the viewer when the container is mounted
const handleContainer = useCallback(parent => { const handleContainer = useCallback(parent => {
setPdfJsWrapper(parent ? new PDFJSWrapper(parent.firstChild) : undefined) if (parent) {
const viewer = new PDFJSWrapper(parent.firstChild)
setPdfJsWrapper(viewer)
return () => viewer.destroy()
}
}, []) }, [])
// listen for initialize event // listen for initialize event
useEffect(() => { useEffect(() => {
if (pdfJsWrapper) { if (pdfJsWrapper) {
pdfJsWrapper.eventBus.on('pagesinit', () => { const handlePagesinit = () => setInitialised(true)
setInitialised(true) pdfJsWrapper.eventBus.on('pagesinit', handlePagesinit)
}) return () => pdfJsWrapper.eventBus.off('pagesinit', handlePagesinit)
} }
}, [pdfJsWrapper]) }, [pdfJsWrapper])
@ -53,6 +57,7 @@ function PdfJsViewer({ url }) {
// TODO: anything else to be reset? // TODO: anything else to be reset?
pdfJsWrapper.loadDocument(url).catch(error => setError(error)) pdfJsWrapper.loadDocument(url).catch(error => setError(error))
return () => pdfJsWrapper.abortDocumentLoading()
} }
}, [pdfJsWrapper, url]) }, [pdfJsWrapper, url])
@ -73,21 +78,22 @@ function PdfJsViewer({ url }) {
// listen for scroll events // listen for scroll events
useEffect(() => { useEffect(() => {
if (pdfJsWrapper) { if (initialised && pdfJsWrapper) {
// store the scroll position in localStorage, for the synctex button // store the scroll position in localStorage, for the synctex button
const storePosition = debounce(pdfViewer => { const storePosition = debounce(pdfViewer => {
// set position for "sync to code" button // set position for "sync to code" button
try { try {
setPosition(pdfViewer.currentPosition) setPosition(pdfViewer.currentPosition)
} catch (error) { } catch (error) {
// console.error(error) // TODO // TODO
console.error(error)
} }
}, 500) }, 500)
// store the scroll position in localStorage, for use when reloading // store the scroll position in localStorage, for use when reloading
const storeScrollTop = debounce(pdfViewer => { const storeScrollTop = debounce(pdfViewer => {
// set position for "sync to code" button // set position for "sync to code" button
setScrollTop(pdfJsWrapper.container.scrollTop) setScrollTop(pdfViewer.container.scrollTop)
}, 500) }, 500)
storePosition(pdfJsWrapper) storePosition(pdfJsWrapper)
@ -100,15 +106,17 @@ function PdfJsViewer({ url }) {
pdfJsWrapper.container.addEventListener('scroll', scrollListener) pdfJsWrapper.container.addEventListener('scroll', scrollListener)
return () => { return () => {
storePosition.cancel()
storeScrollTop.cancel()
pdfJsWrapper.container.removeEventListener('scroll', scrollListener) pdfJsWrapper.container.removeEventListener('scroll', scrollListener)
} }
} }
}, [setPosition, setScrollTop, pdfJsWrapper]) }, [setPosition, setScrollTop, pdfJsWrapper, initialised])
// listen for double-click events // listen for double-click events
useEffect(() => { useEffect(() => {
if (pdfJsWrapper) { if (pdfJsWrapper) {
pdfJsWrapper.eventBus.on('textlayerrendered', textLayer => { const handleTextlayerrendered = textLayer => {
const pageElement = textLayer.source.textLayerDiv.closest('.page') const pageElement = textLayer.source.textLayerDiv.closest('.page')
const doubleClickListener = event => { const doubleClickListener = event => {
@ -120,7 +128,11 @@ function PdfJsViewer({ url }) {
} }
pageElement.addEventListener('dblclick', doubleClickListener) pageElement.addEventListener('dblclick', doubleClickListener)
}) }
pdfJsWrapper.eventBus.on('textlayerrendered', handleTextlayerrendered)
return () =>
pdfJsWrapper.eventBus.off('textlayerrendered', handleTextlayerrendered)
} }
}, [pdfJsWrapper]) }, [pdfJsWrapper])

View file

@ -32,7 +32,7 @@ export default class PDFJSWrapper {
}) })
// create the localization // create the localization
const l10n = new PDFJSViewer.GenericL10n('en-GB') // TODO: locale mapping? // const l10n = new PDFJSViewer.GenericL10n('en-GB') // TODO: locale mapping?
// create the viewer // create the viewer
const viewer = new PDFJSViewer.PDFViewer({ const viewer = new PDFJSViewer.PDFViewer({
@ -40,7 +40,7 @@ export default class PDFJSWrapper {
eventBus, eventBus,
imageResourcesPath, imageResourcesPath,
linkService, linkService,
l10n, // l10n, // commented out since it currently breaks `aria-label` rendering in pdf pages
enableScripting: false, // default is false, but set explicitly to be sure enableScripting: false, // default is false, but set explicitly to be sure
renderInteractiveForms: false, renderInteractiveForms: false,
}) })
@ -53,22 +53,48 @@ export default class PDFJSWrapper {
} }
// load a document from a URL // load a document from a URL
async loadDocument(url) { loadDocument(url) {
const doc = await PDFJS.getDocument({ // prevents any previous loading task from populating the viewer
url, this.loadDocumentTask = undefined
cMapUrl,
cMapPacked: true,
disableFontFace,
rangeChunkSize,
disableAutoFetch: true,
disableStream: true,
textLayerMode: 2, // PDFJSViewer.TextLayerMode.ENABLE,
}).promise
this.viewer.setDocument(doc) return new Promise((resolve, reject) => {
this.linkService.setDocument(doc) this.loadDocumentTask = PDFJS.getDocument({
url,
cMapUrl,
cMapPacked: true,
disableFontFace,
rangeChunkSize,
disableAutoFetch: true,
disableStream: true,
textLayerMode: 2, // PDFJSViewer.TextLayerMode.ENABLE,
})
return doc this.loadDocumentTask.promise
.then(doc => {
if (!this.loadDocumentTask) {
return // ignoring the response since loading task has been aborted
}
const previousDoc = this.viewer.pdfDocument
this.viewer.setDocument(doc)
this.linkService.setDocument(doc)
resolve(doc)
if (previousDoc) {
previousDoc.cleanup().catch(console.error)
previousDoc.destroy()
}
})
.catch(error => {
if (this.loadDocumentTask) {
reject(error)
}
})
.finally(() => {
this.loadDocumentTask = undefined
})
})
} }
// update the current scale value if the container size changes // update the current scale value if the container size changes
@ -123,4 +149,16 @@ export default class PDFJSWrapper {
pageSize: { height, width }, pageSize: { height, width },
} }
} }
abortDocumentLoading() {
this.loadDocumentTask = undefined
}
destroy() {
if (this.loadDocumentTask) {
this.loadDocumentTask.destroy()
this.loadDocumentTask = undefined
}
this.viewer.destroy()
}
} }

View file

@ -0,0 +1,83 @@
import { expect } from 'chai'
import { screen } from '@testing-library/react'
import path from 'path'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import { pathToFileURL } from 'url'
import PdfJsViewer from '../../../../../frontend/js/features/pdf-preview/components/pdf-js-viewer'
const example = pathToFileURL(
path.join(__dirname, '../fixtures/test-example.pdf').toString()
)
const exampleCorrupt = pathToFileURL(
path.join(__dirname, '../fixtures/test-example-corrupt.pdf')
).toString()
const invalidURL = 'http://nonexisting.com/doc'
describe('<PdfJSViewer/>', function () {
it('loads all PDF pages', async function () {
renderWithEditorContext(<PdfJsViewer url={example} />)
await screen.findByLabelText('Page 1')
await screen.findByLabelText('Page 2')
await screen.findByLabelText('Page 3')
expect(screen.queryByLabelText('Page 4')).to.not.exist
})
it('renders pages in a "loading" state', async function () {
renderWithEditorContext(<PdfJsViewer url={example} />)
await screen.findByLabelText('Loading…')
})
it('can be unmounted while loading a document', async function () {
const { unmount } = renderWithEditorContext(<PdfJsViewer url={example} />)
unmount()
})
it('can be unmounted after loading a document', async function () {
const { unmount } = renderWithEditorContext(<PdfJsViewer url={example} />)
await screen.findByLabelText('Page 1')
unmount()
})
describe('with an invalid URL', function () {
it('renders an error alert', async function () {
renderWithEditorContext(<PdfJsViewer url={invalidURL} />)
await screen.findByRole('alert')
expect(screen.queryByLabelText('Page 1')).to.not.exist
})
it('can load another document after the error', async function () {
const { rerender } = renderWithEditorContext(
<PdfJsViewer url={invalidURL} />
)
await screen.findByRole('alert')
rerender(<PdfJsViewer url={example} />)
await screen.findByLabelText('Page 1')
expect(screen.queryByRole('alert')).to.not.exist
})
})
describe('with an corrupted document', function () {
it('renders an error alert', async function () {
renderWithEditorContext(<PdfJsViewer url={exampleCorrupt} />)
await screen.findByRole('alert')
expect(screen.queryByLabelText('Page 1')).to.not.exist
})
it('can load another document after the error', async function () {
const { rerender } = renderWithEditorContext(
<PdfJsViewer url={exampleCorrupt} />
)
await screen.findByRole('alert')
rerender(<PdfJsViewer url={example} />)
await screen.findByLabelText('Page 1')
expect(screen.queryByRole('alert')).to.not.exist
})
})
})