Enhancement: pngx pdf viewer (#12043)

This commit is contained in:
shamoon
2026-02-08 21:24:43 -08:00
committed by GitHub
parent 6a87c3f4dd
commit 9e9e55758f
29 changed files with 1123 additions and 165 deletions

View File

@@ -0,0 +1,299 @@
import { SimpleChange } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs'
import { PDFSinglePageViewer, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs'
import { PngxPdfViewerComponent } from './pdf-viewer.component'
import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types'
describe('PngxPdfViewerComponent', () => {
let fixture: ComponentFixture<PngxPdfViewerComponent>
let component: PngxPdfViewerComponent
const initComponent = async (src = 'test.pdf') => {
component.src = src
fixture.detectChanges()
await fixture.whenStable()
}
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PngxPdfViewerComponent],
}).compileComponents()
fixture = TestBed.createComponent(PngxPdfViewerComponent)
component = fixture.componentInstance
})
it('loads a document and emits events', async () => {
const loadSpy = jest.fn()
const renderedSpy = jest.fn()
component.afterLoadComplete.subscribe(loadSpy)
component.rendered.subscribe(renderedSpy)
await initComponent()
expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe(
'/assets/js/pdf.worker.min.mjs'
)
const isVisible = (component as any).findController.onIsPageVisible as
| (() => boolean)
| undefined
expect(isVisible?.()).toBe(true)
expect(loadSpy).toHaveBeenCalledWith(
expect.objectContaining({ numPages: 1 })
)
expect(renderedSpy).toHaveBeenCalled()
expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
})
it('initializes single-page viewer and disables text layer', async () => {
component.renderMode = PdfRenderMode.Single
component.selectable = false
await initComponent()
const viewer = (component as any).pdfViewer as PDFSinglePageViewer & {
options: Record<string, unknown>
}
expect(viewer).toBeInstanceOf(PDFSinglePageViewer)
expect(viewer.options.textLayerMode).toBe(0)
})
it('applies zoom, rotation, and page changes', async () => {
await initComponent()
const pageSpy = jest.fn()
component.pageChange.subscribe(pageSpy)
component.zoomScale = PdfZoomScale.PageFit
component.zoom = PdfZoomLevel.Two
component.rotation = 90
component.page = 2
component.ngOnChanges({
zoomScale: new SimpleChange(
PdfZoomScale.PageWidth,
PdfZoomScale.PageFit,
false
),
zoom: new SimpleChange(PdfZoomLevel.One, PdfZoomLevel.Two, false),
rotation: new SimpleChange(undefined, 90, false),
page: new SimpleChange(undefined, 2, false),
})
const viewer = (component as any).pdfViewer as PDFViewer
expect(viewer.pagesRotation).toBe(90)
expect(viewer.currentPageNumber).toBe(2)
expect(pageSpy).toHaveBeenCalledWith(2)
viewer.currentScale = 1
;(component as any).applyScale()
expect(viewer.currentScaleValue).toBe(PdfZoomScale.PageFit)
expect(viewer.currentScale).toBe(2)
const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
component.page = 2
;(component as any).lastViewerPage = 2
;(component as any).applyViewerState()
expect((component as any).lastViewerPage).toBeUndefined()
expect(applyScaleSpy).toHaveBeenCalled()
})
it('dispatches find when search query changes after render', async () => {
await initComponent()
const eventBus = (component as any).eventBus as { dispatch: jest.Mock }
const dispatchSpy = jest.spyOn(eventBus, 'dispatch')
;(component as any).hasRenderedPage = true
component.searchQuery = 'needle'
component.ngOnChanges({
searchQuery: new SimpleChange('', 'needle', false),
})
expect(dispatchSpy).toHaveBeenCalledWith('find', {
query: 'needle',
caseSensitive: false,
highlightAll: true,
phraseSearch: true,
})
component.ngOnChanges({
searchQuery: new SimpleChange('needle', 'needle', false),
})
expect(dispatchSpy).toHaveBeenCalledTimes(1)
})
it('emits error when document load fails', async () => {
const errorSpy = jest.fn()
component.loadError.subscribe(errorSpy)
jest.spyOn(pdfjs, 'getDocument').mockImplementationOnce(() => {
return {
promise: Promise.reject(new Error('boom')),
destroy: jest.fn(),
} as any
})
await initComponent('bad.pdf')
expect(errorSpy).toHaveBeenCalled()
})
it('cleans up resources on destroy', async () => {
await initComponent()
const viewer = (component as any).pdfViewer as { cleanup: jest.Mock }
const loadingTask = (component as any).loadingTask as unknown as {
destroy: jest.Mock
}
const resizeObserver = (component as any).resizeObserver as unknown as {
disconnect: jest.Mock
}
const eventBus = (component as any).eventBus as { off: jest.Mock }
jest.spyOn(viewer, 'cleanup')
jest.spyOn(loadingTask, 'destroy')
jest.spyOn(resizeObserver, 'disconnect')
jest.spyOn(eventBus, 'off')
component.ngOnDestroy()
expect(eventBus.off).toHaveBeenCalledWith(
'pagerendered',
expect.any(Function)
)
expect(eventBus.off).toHaveBeenCalledWith('pagesinit', expect.any(Function))
expect(eventBus.off).toHaveBeenCalledWith(
'pagechanging',
expect.any(Function)
)
expect(resizeObserver.disconnect).toHaveBeenCalled()
expect(loadingTask.destroy).toHaveBeenCalled()
expect(viewer.cleanup).toHaveBeenCalled()
expect((component as any).pdfViewer).toBeUndefined()
})
it('skips work when viewer is missing or has no pages', () => {
const eventBus = (component as any).eventBus as { dispatch: jest.Mock }
const dispatchSpy = jest.spyOn(eventBus, 'dispatch')
;(component as any).dispatchFindIfReady()
expect(dispatchSpy).not.toHaveBeenCalled()
;(component as any).applyViewerState()
;(component as any).applyScale()
const viewer = new PDFViewer({ eventBus: undefined })
viewer.pagesCount = 0
;(component as any).pdfViewer = viewer
viewer.currentScale = 5
;(component as any).applyScale()
expect(viewer.currentScale).toBe(5)
})
it('returns early on src change in ngOnChanges', () => {
const loadSpy = jest.spyOn(component as any, 'loadDocument')
const initSpy = jest.spyOn(component as any, 'initViewer')
const scaleSpy = jest.spyOn(component as any, 'applyViewerState')
const resizeSpy = jest.spyOn(component as any, 'setupResizeObserver')
component.ngOnChanges({
src: new SimpleChange(undefined, 'test.pdf', true),
zoomScale: new SimpleChange(
PdfZoomScale.PageWidth,
PdfZoomScale.PageFit,
false
),
})
expect(loadSpy).toHaveBeenCalled()
expect(resizeSpy).not.toHaveBeenCalled()
expect(initSpy).not.toHaveBeenCalled()
expect(scaleSpy).not.toHaveBeenCalled()
})
it('applies viewer state after view init when already loaded', () => {
const applySpy = jest.spyOn(component as any, 'applyViewerState')
;(component as any).hasLoaded = true
;(component as any).pdf = { numPages: 1 }
fixture.detectChanges()
expect(applySpy).toHaveBeenCalled()
})
it('skips viewer state after view init when no pdf is available', () => {
const applySpy = jest.spyOn(component as any, 'applyViewerState')
;(component as any).hasLoaded = true
fixture.detectChanges()
expect(applySpy).not.toHaveBeenCalled()
})
it('does not reload when already loaded', async () => {
await initComponent()
const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument')
const callCount = getDocumentSpy.mock.calls.length
await (component as any).loadDocument()
expect(getDocumentSpy).toHaveBeenCalledTimes(callCount)
})
it('runs applyScale on resize observer notifications', async () => {
await initComponent()
const applySpy = jest.spyOn(component as any, 'applyScale')
const resizeObserver = (component as any).resizeObserver as {
trigger: () => void
}
resizeObserver.trigger()
expect(applySpy).toHaveBeenCalled()
})
it('skips page work when no pages are available', async () => {
await initComponent()
const viewer = (component as any).pdfViewer as PDFViewer
viewer.pagesCount = 0
const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
component.page = undefined
;(component as any).lastViewerPage = 1
;(component as any).applyViewerState()
expect(applyScaleSpy).not.toHaveBeenCalled()
expect((component as any).lastViewerPage).toBe(1)
})
it('falls back to a default zoom when input is invalid', async () => {
await initComponent()
const viewer = (component as any).pdfViewer as PDFViewer
viewer.currentScale = 3
component.zoom = 'not-a-number' as PdfZoomLevel
;(component as any).applyScale()
expect(viewer.currentScale).toBe(3)
})
it('re-initializes viewer on selectable or render mode changes', async () => {
await initComponent()
const initSpy = jest.spyOn(component as any, 'initViewer')
component.selectable = false
component.renderMode = PdfRenderMode.Single
component.ngOnChanges({
selectable: new SimpleChange(true, false, false),
renderMode: new SimpleChange(
PdfRenderMode.All,
PdfRenderMode.Single,
false
),
})
expect(initSpy).toHaveBeenCalled()
})
})