Skip to main content

Testing your PDFs

Assert against your own templates with a real render using @lupinum/nuxt-pdf/test, plus a realistic Vitest example.

The utilities Nuxt PDF is tested with ship as @lupinum/nuxt-pdf/test. They run a real render through the same pipeline that your server uses. This pipeline includes asset resolution, font registration, and single-pass or multi-pass layout. You assert against actual output, not a mocked tree. There is one parser, and it is the one the package's own suite runs on.

Setup

The parser and rasterizer load pdfjs-dist and @napi-rs/canvas lazily. Install them as dev dependencies of the project under test:

pnpm
pnpm add -D pdfjs-dist @napi-rs/canvas

The helpers are runner-agnostic: they throw a PdfAssertionError with an actionable message rather than depending on Vitest or Jest.

A realistic example

test/invoice.test.ts
import { describe, it } from 'vitest'
import { expectPdf, renderPdfSfc } from '@lupinum/nuxt-pdf/test'

describe('invoice.vue', () => {
  it('renders the customer, a terms link, and an outline', async () => {
    const { parsed } = await renderPdfSfc(
      './pdfs/invoice.vue',
      { invoice: { customer: 'Acme Corp', number: 'INV-001', total: 'EUR 1,250.00' } },
      { fonts: [{ family: 'Invoice Sans', src: 'InvoiceSans-Regular.ttf' }] },
    )

    expectPdf(parsed)
      .toHavePageCount(2)
      .toContainText('Invoice for Acme Corp', { page: 1 })
      .toHaveLink({ destination: 'terms', page: 1 })
      .toHaveLink({ url: 'https://example.com/' })
      .toHaveOutline([{ title: 'Terms' }])
  })
})

renderPdfSfc(file, props, options) compiles nested SFC imports, discovers local images, bundles declared fonts, mounts through the real registry pipeline, and returns { bytes, parsed, result }. Use renderPdfTemplate(Component, props) when the test already has an ordinary Vue component. Both helpers accept the same partial remote and limits options as nuxt.config; the SFC helper also accepts the same local fonts declarations.

Assertions

expectPdf(parsed) returns a chainable expectation:

AssertionChecks
toHavePageCount(n)The document has n pages
toContainText(text, { page? })The extracted text contains text (optionally on a page)
toHaveLink({ destination? , url? , page? })A named-destination or external-URL link annotation exists
toHaveOutline(shape)The bookmark outline matches the given title hierarchy

parsePdf exposes the same data directly. The data includes page text, page count, flattened link annotations, and the outline. Use it when you want to assert without the fluent API.

Testing a server route

parsePdf also accepts a PdfRenderResult straight from the registry, so route tests read naturally:

ts
import { parsePdf } from '@lupinum/nuxt-pdf/test'
import { pdf } from '#pdf'

const parsed = await parsePdf(await pdf.invoice.render(props))

Pixel-level regressions

For visual regressions, comparePdfSnapshot follows a reviewed-baseline policy: it writes per-page PNG baselines into a directory when UPDATE_PDF_BASELINES=1 (or { update: true }) is set, and otherwise compares each page against them within a pixel threshold:

ts
import { comparePdfSnapshot, renderPdfSfc } from '@lupinum/nuxt-pdf/test'

const { bytes } = await renderPdfSfc('./pdfs/invoice.vue', props)
await comparePdfSnapshot(bytes, './test/baselines/invoice')

A failed comparison writes expected, actual, and diff PNGs for every changed page plus metrics.json under reports/pdf-snapshots. Upload that directory in CI so the failure can be inspected without reproducing it locally. Semantic geometry checks are available through parsePdf(...).pages[n].textRuns; compare coordinates with a tolerance instead of exact equality.

Raster baselines are environment-sensitive. Review them when they change and commit them deliberately, exactly as you would a visual snapshot. Nuxt PDF does not claim byte-identical output across machines.

The full set of exports and options is in the test utilities reference.