Test PDF output
Use case: your app generates PDFs — invoices, reports, tickets, contracts. “Clicking Download invoice produces a correct PDF” is a real user journey, and a regression here reaches customers on paper.
Why this is hard
Section titled “Why this is hard”The browser either downloads the file (nothing rendered to assert on) or shows it in a native PDF viewer Playwright can’t see into. Either way, the content lives in a binary file, not the DOM.
Recipe
Section titled “Recipe”Capture the download
Section titled “Capture the download”import { test, expect } from '@playwright/test';
test('invoice PDF contains the right data', async ({ page }) => { await page.goto('/orders/1042');
const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Download invoice' }).click(); const download = await downloadPromise;
const path = await download.path(); // …assert on `path` below});If the PDF opens inline instead of downloading, fetch it directly with the page’s cookies: const pdf = await page.request.get(url).then(r => r.body()).
Assert on the text
Section titled “Assert on the text”npm install -D pdf-parseimport pdf from 'pdf-parse';import { readFileSync } from 'node:fs';
const { text, numpages } = await pdf(readFileSync(path));
expect(numpages).toBe(1);expect(text).toContain('Invoice #1042');expect(text).toContain('Total: $1,337.00');expect(text).not.toContain('undefined'); // the classic template bugpdf-parse flattens layout — columns and tables come out in reading order, so assert on fragments, not exact whole-document strings.
Assert on the layout (visual)
Section titled “Assert on the layout (visual)”Text checks miss broken layouts: overlapping columns, a logo pushed onto page 2. Rasterize and reuse Playwright’s visual comparison:
# poppler-utils; in CI: apt-get install poppler-utils / brew install popplerpdftoppm -png -r 100 invoice.pdf out/invoiceimport { execFileSync } from 'node:child_process';
execFileSync('pdftoppm', ['-png', '-r', '100', path, 'out/invoice']);expect(readFileSync('out/invoice-1.png')).toMatchSnapshot('invoice-page1.png', { maxDiffPixelRatio: 0.02,});Mask or stabilize dynamic regions first (dates, invoice numbers) — either fixed test data, or crop known-stable regions with sharp before comparing.
Deeper checks when it matters
Section titled “Deeper checks when it matters”- Machine-readable invoices (ZUGFeRD/Factur-X) embed XML — extract and validate against schema.
- PDF/A compliance for archival documents:
verapdfin CI. - Accessibility (tagged PDF): at minimum assert text is extractable (a scanned-image PDF returns empty
text— a real regression class after “we switched the PDF library”).
Caveats
Section titled “Caveats”- PDF generation is often async server-side — poll the download endpoint rather than racing the button click.
- Font substitution makes rasterized output differ between machines; generate baselines in the same Docker image CI uses.
page.pdf()in Playwright creates PDFs of web pages (Chromium-only); it’s unrelated to asserting on PDFs your app generates.
Related
Section titled “Related”- Test transactional email — PDFs frequently arrive as email attachments.