Skip to main content

Testing templates

@formfeed/testing lets your own Vitest or Jest suite check templates: that they compile and render with given data, that they read only variables the data has, what text they contain, and, through the API, how many pages they print. It needs Node.js 22 or later, and Vitest 3.2 or later when you use Vitest. Install the helpers:

npm install -D @formfeed/testing
import { loadTemplate } from '@formfeed/testing';
import invoice from '../templates/invoice-de/data/default.json';

const tpl = await loadTemplate('templates/invoice-de');

test('compiles and uses only known variables', async () => {
await expect(tpl).toRenderWithoutErrors(invoice);
await expect(tpl).toUseOnlyKnownVariables(invoice);
});

test('html snapshot', async () => {
const html = await tpl.render(invoice);
expect(html).toMatchSnapshot();
});

test('two pages for forty lines', async () => {
await expect(tpl).toHavePageCount(2, invoice); // true render via the API; skipped without a key
});

The matchers need the setup file of your test runner.

loadTemplate​

loadTemplate(folder) reads a template folder and returns a handle. The path is resolved from the current working directory (where the test runner starts), not from the test file. A formfeed.json above the folder is used when present; otherwise the folder's parent is taken as the templates folder, and partials/ beside that parent serves the includes.

MemberWhat it is
tpl.render(data?)The assembled document as HTML, as the API would print it. Header and footer are not part of it.
tpl.analyze(data?)The editor's diagnostics: syntax errors, unknown filters, variables missing from the data
tpl.data(name?)A data set from data/<name>.json; without a name default, else the first file. An unknown name throws.
tpl.slug, tpl.dirThe folder's name and absolute path
tpl.files, tpl.projectThe template's files and the project as the devkit reads them

data is optional everywhere: without it, the template's default data set is used.

The helpers render HTML templates. loadTemplate also reads a folder with a Word or PowerPoint document, and tpl.analyze(data) lists its findings by part and paragraph, but tpl.render throws for it, so the matchers fail. Test those templates with formfeed test.

Matchers​

MatcherLayerPasses when
toRenderWithoutErrors(data?)offlineThe template has no error diagnostics and renders without an engine error
toUseOnlyKnownVariables(data?)offlineNo variable path the template reads is missing from the data (the warning unknown-variable, which validate lets pass)
toContainText(text, data?)offlineThe rendered document's text, with tags, <style> and <script> removed, contains text
toHavePageCount(n, data?)APIThe true render through the API has exactly n pages

toHavePageCount renders the document locally, sends it to the API as HTML with the template's settings, and reads the page count of the PDF. It is a real render: free with a test key, counted against your plan with a live key. It reads its key from FORMFEED_API_KEY only, not from the CLI's stored login. Without that variable it passes and prints a warning (toHavePageCount skipped …); set FORMFEED_REQUIRE_API=1 in CI to make it fail instead.

Not available yet: pixel snapshots of the true render (toMatchRenderSnapshot).

Setup​

Vitest:

vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: { setupFiles: ['@formfeed/testing/vitest'] },
});

Jest: add @formfeed/testing/jest to setupFilesAfterEnv. Both setup files call expect.extend with the matchers and add their types to expect, so TypeScript knows toRenderWithoutErrors and the others. For another runner, call expect.extend(matchers) yourself with matchers from @formfeed/testing; createMatchers({ env, client, warn }) builds them with your own environment, API client (for example a fake in tests) or warning function.

FORMFEED_API_KEY (and optionally FORMFEED_REGION, default eu, and FORMFEED_BASE_URL) enable the API-backed matcher.

Without a test runner​

formfeed test checks every template of the project, or one (formfeed test invoice-de), with every data set in data/*.json (--data <name> for one): it compiles and analyses the template, renders it, and compares the result with the approved snapshot in tests/__snapshots__/. A data set with an error diagnostic or a different snapshot fails the run with exit code 1; a data set without a snapshot is reported as without snapshot and does not fail. --update-snapshots (-u) writes the current output as the approved snapshot; commit the snapshots with the templates.

For an HTML template the snapshot is the assembled document (<data set>.html), followed by the rendered header and footer under <!-- formfeed:header --> and <!-- formfeed:footer --> lines: Chromium prints them from templates of their own, outside the document, and a changed footer has to fail the test like any other change. A Word or PowerPoint template is filled with each data set and its snapshot is the XML of the filled parts (<data set>.xml), pretty printed and filled without loading or drawing pictures, so the comparison runs offline and gives the same result every time. The PDF of such a template comes from the converter, so check its pages with formfeed render --output pdf or ci preview.