Automated PDF Layout Testing
Visual PDF diffs tell you that pixels moved. They do not tell you whether a table split, a chart lost its commentary, or a TOC number is wrong. Layout testing asserts the things that matter.
1. What to test
- Page breaks: a table row, chart, or signature block split across pages.
- Relationships: a chart and its commentary on the same page.
- TOC and page numbers: headings measured after layout.
- Coordinates: signature boxes, totals, and stamps.
- Content: missing elements, overflow, or the wrong count.
2. How a testCase works
The testCase script runs after layout with a doc object for the rendered PDF DOM. Return an object with success and any custom keys you want in testResults.
const result = await client.generate({
templateId: 'YOUR_TEMPLATE_ID',
data,
streamOnly: true,
testCase: `
const rows = doc.querySelectorAll('table tbody tr');
if (!rows.length) return { success: false, reason: 'No table rows' };
return { success: true, rows: rows.length };
`
});
console.log(result.testResults);
Use streamOnly: true when you want the SDK to return the file buffer and decode testResults.
3. Catch a table split across pages
Compare the page number of the first and last row. If they differ for a table that must stay together, fail and return a patch.
const rows = doc.querySelectorAll('table.details tbody tr');
const first = rows[0];
const last = rows[rows.length - 1];
if (first.pages[0] !== last.pages[0]) {
return {
success: false,
patch: { compact: true },
reason: 'Details table split across pages'
};
}
return { success: true };
Use a template class that responds to the patch; see the page-break guide.
4. Verify TOC page numbers
Measure each heading after pagination, then compare it with the TOC value. If they differ, return the measured values as a patch and let the engine re-render.
const headings = doc.querySelectorAll('h2[data-toc]');
const toc = headings.map((heading) => ({
id: heading.attributes.id,
page: heading.pages[0] + 1
}));
return { success: true, toc };
Full example: Table of contents with real page numbers.
5. Check signature coordinates
Query the signature block, return its page and bounding box, and use it to place the overlay.
const signature = doc.querySelector('#signature');
return {
success: Boolean(signature),
signature: signature
? { page: signature.pages[0] + 1, geometry: signature.geometry }
: null
};
See Extract element coordinates for e-signatures.
6. Use testResults in CI
Your CI can fail when testResults.success is false or when a custom assertion is wrong. Keep the returned object small: testResults is capped at 8 KB of JSON.
if (!result.testResults?.success) {
throw new Error(
'PDF layout check failed: ' + JSON.stringify(result.testResults)
);
}
For async jobs, inspect testResults in the webhook payload.
7. Let the engine heal simple breaks
If a failure has a small structural fix (shorter summary, compact class), return a patch. The engine re-renders up to two times before returning a failure.
if (summary.innerText.length > 140) {
return {
success: false,
patch: { summary: summary.innerText.slice(0, 140) },
reason: 'Summary overflow'
};
}
return { success: true };
8. Regression workflow
- Run the same
testCaseagainst every template change. - Use representative data, including long names and large totals.
- Assert only what matters; do not dump the whole DOM.
- Run in staging before production.
- For bulk jobs, use webhooks and inspect each result.
Common mistakes
- Comparing pixels instead of layout rules.
- Returning a huge object; keep it under 8 KB.
- Testing only short sample data.
- Forgetting that pages are 0-indexed; add 1 for display.