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 { describe, it, expect, vi } from 'vitest';
|
||||||
import { createPdfRenderer } from './usePdfRenderer.svelte';
|
import { createPdfRenderer } from './usePdfRenderer.svelte';
|
||||||
import { makeFakeLibLoader } from './testHelpers';
|
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).
|
// Note: init() and loadDocument() require pdfjsLib (browser module).
|
||||||
// These tests cover pure state logic only — bounds clamping and zoom limits.
|
// These tests cover pure state logic only — bounds clamping and zoom limits.
|
||||||
@@ -231,6 +252,31 @@ describe('createPdfRenderer', () => {
|
|||||||
expect(arg.wasmUrl?.endsWith('/')).toBe(true);
|
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 () => {
|
it('loadDocument sets error and loading=false when getDocument().promise rejects', async () => {
|
||||||
const failingLib = {
|
const failingLib = {
|
||||||
GlobalWorkerOptions: { workerSrc: '' },
|
GlobalWorkerOptions: { workerSrc: '' },
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist';
|
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 }]>;
|
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'
|
(e as { name: string }).name === 'RenderingCancelledException'
|
||||||
)
|
)
|
||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
renderTask = null;
|
renderTask = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user