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 }andsize: A4toggled 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.
Top comments (0)