diff --git a/frontend/src/lib/document/viewer/usePdfRenderer.svelte.test.ts b/frontend/src/lib/document/viewer/usePdfRenderer.svelte.test.ts index c9e07a51..94b2046d 100644 --- a/frontend/src/lib/document/viewer/usePdfRenderer.svelte.test.ts +++ b/frontend/src/lib/document/viewer/usePdfRenderer.svelte.test.ts @@ -1,6 +1,27 @@ import { describe, it, expect, vi } from 'vitest'; import { createPdfRenderer } from './usePdfRenderer.svelte'; import { makeFakeLibLoader } from './testHelpers'; +import { m } from '$lib/paraglide/messages.js'; + +function makeRenderingLib(renderPromise: Promise): typeof import('pdfjs-dist') { + const page = { + getViewport: vi.fn().mockReturnValue({ width: 100, height: 100 }), + render: vi.fn().mockReturnValue({ promise: renderPromise, cancel: vi.fn() }), + streamTextContent: vi.fn().mockReturnValue(new ReadableStream()) + }; + return { + GlobalWorkerOptions: { workerSrc: '' }, + getDocument: vi.fn().mockReturnValue({ + promise: Promise.resolve({ numPages: 1, getPage: vi.fn().mockResolvedValue(page) }) + }), + TextLayer: class { + render() { + return Promise.resolve(); + } + cancel() {} + } + } as unknown as typeof import('pdfjs-dist'); +} // Note: init() and loadDocument() require pdfjsLib (browser module). // These tests cover pure state logic only — bounds clamping and zoom limits. @@ -231,6 +252,31 @@ describe('createPdfRenderer', () => { expect(arg.wasmUrl?.endsWith('/')).toBe(true); }); + it('renderCurrentPage sets a localized error when the render rejects (not silently blank)', async () => { + const lib = makeRenderingLib(Promise.reject(new Error('JBig2 failed to initialize'))); + const r = createPdfRenderer(vi.fn().mockResolvedValue([lib, { default: '' }] as const)); + await r.init(); + r.setElements(document.createElement('canvas'), document.createElement('div')); + await r.loadDocument('/x'); + await r.renderCurrentPage(); + + expect(r.error).toBe(m.doc_render_failed()); + }); + + it('renderCurrentPage does NOT set an error when the render is cancelled', async () => { + const cancelled = Object.assign(new Error('cancelled'), { + name: 'RenderingCancelledException' + }); + const lib = makeRenderingLib(Promise.reject(cancelled)); + const r = createPdfRenderer(vi.fn().mockResolvedValue([lib, { default: '' }] as const)); + await r.init(); + r.setElements(document.createElement('canvas'), document.createElement('div')); + await r.loadDocument('/x'); + await r.renderCurrentPage(); + + expect(r.error).toBeNull(); + }); + it('loadDocument sets error and loading=false when getDocument().promise rejects', async () => { const failingLib = { GlobalWorkerOptions: { workerSrc: '' }, diff --git a/frontend/src/lib/document/viewer/usePdfRenderer.svelte.ts b/frontend/src/lib/document/viewer/usePdfRenderer.svelte.ts index c21ad2dd..1e169969 100644 --- a/frontend/src/lib/document/viewer/usePdfRenderer.svelte.ts +++ b/frontend/src/lib/document/viewer/usePdfRenderer.svelte.ts @@ -1,4 +1,5 @@ import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'; +import { m } from '$lib/paraglide/messages.js'; export type LibLoader = () => Promise; @@ -105,6 +106,10 @@ export function createPdfRenderer(libLoader: LibLoader = defaultLibLoader) { (e as { name: string }).name === 'RenderingCancelledException' ) return; + // A real decode/render failure (e.g. a wasm decoder that could not + // initialise) — surface a localized message instead of leaving a + // silent blank canvas. Never leak the raw pdf.js error text. + error = m.doc_render_failed(); return; } renderTask = null;