Dev.to WebDev 🛠 Dev 👁 0 📖 3 min read

Testing that an HTML page prints to exactly one PDF page, headless, with Chrome and a regex

I ship small HTML tools that print to a one-page PDF: receipts, notices, checklists. The bug that keeps coming back is not in the JavaScript. It is a header 2 px too tall that pushes the signature line onto page two, and

I ship small HTML tools that print to a one-page PDF: receipts, notices, checklists. The bug that keeps coming back is not in the JavaScript. It is a header 2 px too tall that pushes the signature line onto page two, and nobody notices until a customer prints it. Here is the headless test I now run on every file before it goes out. It needs Chrome and Node, nothing else.

1. Render the PDF without a window

Chrome can print a local file to PDF from the command line:

chrome --headless=new --disable-gpu --no-pdf-header-footer \
  --print-to-pdf=out.pdf --virtual-time-budget=3000 \
  "file:///C:/work/rent-receipt.html?test=1"

--virtual-time-budget lets timers and localStorage restores run before the print. The ?test=1 query string is read by the page: when present, a tiny script fills every field with realistic values (long names, a 12-digit amount, a two-line address) so the layout is tested under load, not with empty inputs.

if (location.search.includes('test=1')) {
  const demo = { landlord: 'Marguerite de la Fontaine-Lacroix', tenant: 'Jonathan Christopher Alexander',
    address: '1234 Longest Street Name You Can Think Of, Apartment 12B', rent: '12345678.90' };
  for (const [k, v] of Object.entries(demo)) { const el = document.getElementById(k); if (el) { el.value = v; el.dispatchEvent(new Event('input')); } }
}

2. Count the pages

A PDF page object contains /Type /Page (without the trailing s of /Pages). A regex is enough; no library:

import { readFileSync } from 'node:fs';
const pdf = readFileSync('out.pdf', 'latin1');
const pages = (pdf.match(/\/Type\s*\/Page[^s]/g) || []).length;
if (pages !== 1) { console.error(`expected 1 page, got ${pages}`); process.exit(1); }

Compressed object streams (PDF 1.5+) can hide page objects from this regex. Chrome's output has not hidden them in my files, but if the count ever comes back as 0, fall back to pdf-lib or pdfinfo.

3. Check what actually printed

Page count says nothing about content. The second check renders the page with --dump-dom under the print media and reads a <pre id="RESULT"> block that the page fills with the values it displays:

chrome --headless=new --dump-dom --virtual-time-budget=3000 \
  "file:///C:/work/rent-receipt.html?test=1" > dom.html

Then a few includes() assertions on the text: the amount in words is present, the month is the current month (my January bug, see the previous post), the receipt number incremented. Take the last <pre id="RESULT"> in the document, not the first: the first one is the test script's own source.

4. Hide the harness from the print

A <pre> block injected for testing adds height and triggers the very overflow you are testing for. Put it under .noprint:

@media print { .noprint, #RESULT { display: none !important; } }

5. Run it on every file

for f in produits/*.html; do node test-print.mjs "$f" || exit 1; done

Ten tools, about twelve seconds. The script has caught a two-page overflow three times since I wrote it, each time from a change that looked harmless (a longer label, a second address line).

Two things I have not solved

  • Letter vs A4: I test both with @page { size: Letter } and size: A4 toggled by a query string, but Chrome's headless margins differ slightly from the print dialog. The 18 mm margin leaves enough slack so far.
  • Mobile Safari ignores part of @page; I have no headless equivalent for it.

The tools this test protects: the free rent receipt generator at https://paperworkkit.itch.io/rent-receipt-free (runs in the browser), and the paid Landlord Paperwork Kit, six offline documents with the same engine: https://paperworkkit.itch.io/landlord-paperwork-kit

If you count PDF pages differently, or have a reliable way to test Safari printing without a device farm, I would like to know.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.