test(document): behavioral CCITT/DCT render fixtures prove the wasm path

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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-01 20:16:35 +02:00
parent 6690e1374d
commit cf86019337
3 changed files with 52 additions and 0 deletions

View File

@@ -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<void> {
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);
});
});

Binary file not shown.

Binary file not shown.