Automated PDF Layout Testing

Updated October 30, 2026 ยท 8 min read

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 };

See Self-healing PDF layouts.

8. Regression workflow

  1. Run the same testCase against every template change.
  2. Use representative data, including long names and large totals.
  3. Assert only what matters; do not dump the whole DOM.
  4. Run in staging before production.
  5. 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.

Test one template

Add a testCase to your next generation and inspect testResults.

Generate PDF API