pdf-document.client.spec.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. // @vitest-environment jsdom
  2. /** Page rendering cancellation is driven by controlled pdfjs promises, not scheduler delays. */
  3. import { describe, expect, it, vi } from 'vitest'
  4. import type { PDFPageProxy } from 'pdfjs-dist'
  5. import { renderPdfPage } from '../src/client/pdf/document.ts'
  6. describe('PDF canvas rendering', () => {
  7. it('joins canvas cancellation before cleanup when text rendering fails', async () => {
  8. const rendered = Promise.withResolvers<undefined>()
  9. const textStarted = Promise.withResolvers<undefined>()
  10. const textFailed = Promise.withResolvers<undefined>()
  11. const cleanup = vi.fn()
  12. const cancelText = vi.fn()
  13. const cancelCanvas = vi.fn()
  14. const page = {
  15. getViewport: () => ({ width: 100, height: 80 }),
  16. render: () => ({ promise: rendered.promise, cancel: cancelCanvas }), cleanup,
  17. } as unknown as PDFPageProxy
  18. const pending = renderPdfPage({ numPages: 1, getPage: async () => page }, 1,
  19. document.createElement('canvas'), new AbortController().signal, 1, () => {
  20. textStarted.resolve(undefined)
  21. return { promise: textFailed.promise, cancel: cancelText }
  22. })
  23. const failure = expect(pending).rejects.toThrow('text failed')
  24. await textStarted.promise
  25. textFailed.reject(new Error('text failed'))
  26. await vi.waitFor(() => { expect(cancelText).toHaveBeenCalledOnce() })
  27. expect(cancelCanvas).toHaveBeenCalledOnce()
  28. expect(cleanup).not.toHaveBeenCalled()
  29. rendered.resolve(undefined)
  30. await failure
  31. expect(cleanup).toHaveBeenCalledOnce()
  32. })
  33. it('cancels when the owner aborts during the library render call, before the abort listener attaches', async () => {
  34. const controller = new AbortController()
  35. const cancel = vi.fn()
  36. const cleanup = vi.fn()
  37. const page = {
  38. getViewport: () => ({ width: 100, height: 80 }),
  39. render: () => {
  40. controller.abort()
  41. return { cancel, promise: Promise.resolve() }
  42. },
  43. cleanup,
  44. } as unknown as PDFPageProxy
  45. await expect(renderPdfPage({ numPages: 1, getPage: async () => page }, 1, document.createElement('canvas'), controller.signal, 1))
  46. .rejects.toMatchObject({ name: 'AbortError' })
  47. expect(cancel).toHaveBeenCalledOnce()
  48. expect(cleanup).toHaveBeenCalledOnce()
  49. })
  50. it('does not touch a canvas when getPage settles after cancellation', async () => {
  51. const lookup = Promise.withResolvers<PDFPageProxy>()
  52. const controller = new AbortController()
  53. const canvas = document.createElement('canvas')
  54. const cleanup = vi.fn()
  55. const render = vi.fn()
  56. const pending = renderPdfPage({ numPages: 1, getPage: () => lookup.promise }, 1, canvas, controller.signal, 1)
  57. const rejected = expect(pending).rejects.toMatchObject({ name: 'AbortError' })
  58. controller.abort()
  59. lookup.resolve({ cleanup, render } as unknown as PDFPageProxy)
  60. await rejected
  61. expect(render).not.toHaveBeenCalled()
  62. expect(cleanup).toHaveBeenCalledOnce()
  63. expect(canvas.width).toBe(300)
  64. expect(canvas.style.getPropertyValue('--pdf-page-width')).toBe('')
  65. })
  66. it('cancels an active task and cleans up only after its promise settles', async () => {
  67. const running = Promise.withResolvers<undefined>()
  68. const entered = Promise.withResolvers<undefined>()
  69. const controller = new AbortController()
  70. const cleanup = vi.fn()
  71. const cancel = vi.fn()
  72. const page = {
  73. getViewport: vi.fn(() => ({ width: 100, height: 80 })),
  74. render: vi.fn(() => { entered.resolve(undefined); return { promise: running.promise, cancel } }),
  75. cleanup,
  76. } as unknown as PDFPageProxy
  77. const pending = renderPdfPage({ numPages: 1, getPage: async () => page }, 1, document.createElement('canvas'), controller.signal, 2)
  78. const rejected = expect(pending).rejects.toThrow('cancelled')
  79. await entered.promise
  80. controller.abort()
  81. expect(cancel).toHaveBeenCalledOnce()
  82. expect(cleanup).not.toHaveBeenCalled()
  83. running.reject(new Error('cancelled'))
  84. await rejected
  85. expect(cleanup).toHaveBeenCalledOnce()
  86. })
  87. it('bounds raster allocation while retaining the selected display dimensions', async () => {
  88. const cleanup = vi.fn()
  89. const getViewport = vi.fn(() => ({ width: 8192, height: 8192 }))
  90. const page = {
  91. getViewport,
  92. render: vi.fn(() => ({ promise: Promise.resolve(), cancel: vi.fn() })),
  93. cleanup,
  94. } as unknown as PDFPageProxy
  95. const canvas = document.createElement('canvas')
  96. await expect(renderPdfPage({ numPages: 1, getPage: async () => page }, 1, canvas, new AbortController().signal, 2))
  97. .resolves.toEqual({ width: 8192, height: 8192 })
  98. expect(getViewport).toHaveBeenCalledWith({ scale: 96 / 72 })
  99. expect(canvas.width * canvas.height).toBe(16_777_216)
  100. expect(canvas.style.getPropertyValue('--pdf-page-width')).toBe('8192px')
  101. expect(cleanup).toHaveBeenCalledOnce()
  102. })
  103. })