From cf86019337a9e60d1e1d2609e2dab3ea888b9332 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 1 Jun 2026 20:16:35 +0200 Subject: [PATCH] test(document): behavioral CCITT/DCT render fixtures prove the wasm path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render committed synthetic fixtures through PdfViewer with the REAL pdf.js loader and assert the canvas is non-blank (sampled dark-pixel count). The CCITT (G4 fax) fixture exercises the shared jbig2.wasm decode path — the same module pdf.js uses for JBIG2 — so it transitively covers the JBIG2 acceptance criterion (the archive sample found zero true JBIG2 docs and jbig2enc is unavailable to synthesize one). The JPEG/DCTDecode fixture guards against regressing the natively-decoded path. Verified the CCITT case goes red when wasmUrl is removed. Fixtures are hermetic, committed assets (~2-5 KB each), generated with ImageMagick — never fetched from staging at test time. CI browser mode. Refs #708 Co-Authored-By: Claude Opus 4.8 --- .../viewer/PdfViewerFixtures.svelte.test.ts | 52 ++++++++++++++++++ .../lib/document/viewer/fixtures/ccitt-g4.pdf | Bin 0 -> 2064 bytes .../lib/document/viewer/fixtures/jpeg-dct.pdf | Bin 0 -> 5065 bytes 3 files changed, 52 insertions(+) create mode 100644 frontend/src/lib/document/viewer/PdfViewerFixtures.svelte.test.ts create mode 100644 frontend/src/lib/document/viewer/fixtures/ccitt-g4.pdf create mode 100644 frontend/src/lib/document/viewer/fixtures/jpeg-dct.pdf diff --git a/frontend/src/lib/document/viewer/PdfViewerFixtures.svelte.test.ts b/frontend/src/lib/document/viewer/PdfViewerFixtures.svelte.test.ts new file mode 100644 index 00000000..2ec78f32 --- /dev/null +++ b/frontend/src/lib/document/viewer/PdfViewerFixtures.svelte.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import PdfViewer from './PdfViewer.svelte'; +import ccittUrl from './fixtures/ccitt-g4.pdf?url'; +import jpegUrl from './fixtures/jpeg-dct.pdf?url'; + +// Behavioral, real-render coverage of the wasm decode path. Unlike the rest of +// the viewer tests, these use the REAL pdf.js loader (no libLoader prop) so the +// page is actually decoded and painted, and the wasm is fetched from +// /pdfjs-wasm/ exactly as in production. CI runs this in a real Chromium. +// See issue #708. + +afterEach(cleanup); + +// A blank page is a uniform white canvas. A rendered page has dark glyph pixels. +function countNonBackgroundPixels(canvas: HTMLCanvasElement): number { + const ctx = canvas.getContext('2d'); + if (!ctx || canvas.width === 0 || canvas.height === 0) return 0; + const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height); + let count = 0; + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const a = data[i + 3]; + if (a > 0 && (r < 250 || g < 250 || b < 250)) count++; + } + return count; +} + +async function expectNonBlankRender(url: string): Promise { + render(PdfViewer, { url, documentId: 'fixture' }); + await vi.waitFor( + () => { + const canvas = document.querySelector('canvas'); + expect(canvas).not.toBeNull(); + expect((canvas as HTMLCanvasElement).width).toBeGreaterThan(0); + expect(countNonBackgroundPixels(canvas as HTMLCanvasElement)).toBeGreaterThan(50); + }, + { timeout: 20000, interval: 250 } + ); +} + +describe('PdfViewer — real codec fixtures (wasm decode path)', () => { + it('renders a CCITT (G4 fax) scan as a non-blank page — same jbig2.wasm path JBIG2 uses', async () => { + await expectNonBlankRender(ccittUrl); + }); + + it('renders a DCTDecode (JPEG) PDF as a non-blank page — no regression', async () => { + await expectNonBlankRender(jpegUrl); + }); +}); diff --git a/frontend/src/lib/document/viewer/fixtures/ccitt-g4.pdf b/frontend/src/lib/document/viewer/fixtures/ccitt-g4.pdf new file mode 100644 index 0000000000000000000000000000000000000000..585893e5b34a0d9a8931acbb283d84bf930f7ef6 GIT binary patch literal 2064 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 literal 0 HcmV?d00001