fix(document): surface PDF render failures instead of a silent blank canvas
renderCurrentPage swallowed every render rejection with a bare return, so a decode failure left a blank white viewer with no feedback. Now a non-cancellation rejection sets a localized doc_render_failed message, which routes into the existing error UI (message + download link). Cancellation (page-nav / zoom) still returns silently — no error. Refs #708 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<void>): 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: '' },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
export type LibLoader = () => Promise<readonly [typeof import('pdfjs-dist'), { default: string }]>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user