Testing templates
How to verify templates with engine-dmo check, the language server in your editor, describe template and describe route, render --json, and template tests in tests/templates/*.test.yaml with selector, JSON-LD and snapshot assertions.
Templates are checked at four levels, from cheapest to fullest. Each catches what the one before cannot see:
flowchart LR A["engine-dmo lsp<br/>as you type"] --> B["engine-dmo check<br/>every template, every site"] B --> C["engine-dmo render<br/>one real page"] C --> D["engine-dmo template test<br/>many pages, in CI"]
| Tool | Needs a database | Catches |
|---|---|---|
engine-dmo lsp in your editor | No | The check’s diagnostics as you type, with completion and hover |
engine-dmo check | No, with --no-db | Every template on every site against the project’s types, fields, routes, components, image transforms and the security rules |
engine-dmo render | Yes | One real page through the whole pipeline, with its errors, queries, assets, accessibility findings and images |
engine-dmo template test | Yes, seeded | Assertions on many pages’ output, for CI and upgrades |
engine-dmo check
The gate. It validates configuration, schema, templates and the deployment together, and reports every problem at once, each with its file, line, column, the source line and a hint.
engine-dmo check # everything, including database checks
engine-dmo check --no-db # skip checks that need a database (CI, image builds)
engine-dmo check --strict # configuration warnings are errors
engine-dmo check --json # { diagnostics, errors, warnings, strict, valid } | Flag | Effect |
|---|---|
--no-db | Skip checks that need a database |
--strict | Promote configuration warnings to errors (not template warnings; see below) |
--fix | Write a stable id for every configuration element without one, keeping comments, then check |
--contract-report | List template usage the next template contract version removes, each with its replacement |
--json | Machine-readable diagnostics |
It exits 0 when valid and 3 when the configuration (templates included) is invalid. Configuration still needs its environment variables set; --no-db only skips connecting.
Read every line, not the summary
Template errors are counted and fail the check, but template warnings are not. A warning about a template (a head without head(), a missing string, an uncredited image) is printed above the summary, and the summary still says 0 errors, 0 warnings. Configuration is valid. --strict does not turn a template warning into an error, and --json leaves template warnings out of diagnostics altogether. A fresh project from engine-dmo init shows this at once: its base.html has no head(), so check prints that warning and exits 0. Read the whole output, and in CI search it for warning: if template warnings must fail the build.
A template error is printed twice: once in the template checker’s short form (_shared/walk/entry.html:4:4: error: ...) and once in the configuration form with its error code (templates/_shared/walk/entry.html:4:4: ... and = error code MRD-1006). They are the same problem.
For templates it reports, among the rest:
- a template that does not compile;
- a variable, field, filter, tag or function that is not in the template contract for that file, with a “did you mean” (
"tone" is not in the template context (contract 1)); - a field the template’s type does not have, read on
entryor on the type’s own key (entry.sumary,walk.sumary); - component calls against their contracts (Components);
- image transforms and breakpoints the site does not define, and helper keywords that are refused (Images);
- the security rules: a variable in
hreforsrcwithout|url, in<script>without|json, in a style or an inline handler, thesafefilter (Frontend build); - per-request output inside
{% cache %}(Caching); - a
sigmx.fragment()naming a fragment no template declares; - a site copy whose shared original has changed (Multisite);
- advisory warnings: a head without
head()or a description, an image whose credit is printed nowhere, twopriority=trueimages on one page.
A shared template is checked once for every site whose chain reaches it, against that site.
What it does not report, so that only render shows it:
| Not checked | What happens at render |
|---|---|
A field read on a loop variable, a query() result, a relation’s item or a component prop ({% for w in items %}{{ w.photo }}) | Prints nothing, with no error or warning, even with template.strict: true |
A property a value does not have, such as entry.publishedAt.iso | Prints nothing |
A query() misuse: an unknown type, a field that is not index: true, an unknown operator (Errors) | The page answers 500 |
An unknown endpoint key in enquiryForm() or subscribeForm(), or .render() on either | Prints nothing |
| A required component slot when the call has no body at all | The render is refused |
t() in an email template | Prints nothing (Email templates) |
So render each kind of page once it has content, and read the output.
The language server
engine-dmo lsp is a Language Server Protocol server in the binary, so it always matches the engine that renders the templates. It needs no database and no secrets, and refuses a project whose engine.environment is production. Use engine-cms lsp for a project on engine-cms.
| Feature | What it gives |
|---|---|
| Diagnostics | On open and on every change, from the unsaved text: the compile, the security rules and the contract check of engine-dmo check, merged across every site whose chain holds the file. Each carries the check’s code and hint, narrowed to what it names |
| Completion | Globals with their keywords, the template’s type’s fields (entry.), a field value’s properties and helpers (entry.heroImage.), singletons., nav., filters after |, tags after {%, template files in include and extends, component names and their remaining props |
| Hover | A global’s or filter’s documentation, a field’s type, label, help and declaring file and line, a component’s contract |
| Go to definition | include, extends and import to the file the site’s chain resolves; a component call to its contract and markup; entry.<field>, singletons.<key> and nav.<key> to the configuration line that declares them |
It reloads the project when a configuration file or a component contract changes, so a field added to a schema file completes on the next keystroke. A configuration that fails to load keeps the last good project and publishes the loader’s errors on the configuration files.
The server speaks over standard input and output (--stdio, the default and only transport). It finds the project from --project, else the workspace root the editor sends, else the working directory. Give it your templates as HTML files.
Neovim 0.11 or later, in the project’s .nvim.lua or your configuration:
vim.lsp.config('meridian', {
cmd = { 'engine-dmo', 'lsp' },
filetypes = { 'html', 'htmldjango' },
root_markers = { 'config/engine.yaml' },
})
vim.lsp.enable('meridian') Look before you render
These need no database. describe template loads the configuration, so every ${VAR} it uses without a default must be set, even to a placeholder (DATABASE_URL=postgres://localhost/none); template api needs none:
engine-dmo describe template listing/entry.html --site town
engine-dmo template api --site town --json describe template says which file a name resolves to along the site’s chain, what it extends and includes (and which file each resolves to), and which type it renders. template api is the complete template surface of a site: globals, filters, tags, field types with their properties and helpers, the project’s types and fields, routes and components. engine-dmo docs serve shows the same in a browser.
With a database, describe route renders a path and prints what the template received:
engine-dmo describe route /whats-on/film-club --json It reports the route that answered, the template the site’s chain resolved, and the whole template context with real values: the variables, functions, filters and components available. Write a template against it.
Render a page
engine-dmo render /whats-on/film-club # the HTML on stdout, an image table on stderr
engine-dmo render /whats-on/film-club --json # the report below
engine-dmo render /en/whats-on --locale fr --json # the same page in another locale
engine-dmo render / --fragment results # only the named fragments, as a script's request gets them render boots the engine against the configured database, read-only, and renders the path through the real middleware and pipeline in process, with no server. A traced render records no page view and no search log entry.
| Flag | Meaning |
|---|---|
--site key | The site (default: the default site) |
--locale code | The locale; the path is then the unprefixed address, and the locale’s prefix or domain is added |
--host host | The Host the request carries (default: the site’s first domain, or the locale’s under the domain strategy) |
--fragment a,b | Render only these fragments; a fragment the page did not render is a failure |
--json | The report |
The --json report:
| Key | Value |
|---|---|
status, site, locale, request, kind, template | The response status, what answered and the template file |
html | The page |
errors, warnings | Render problems, such as a template error with its file and line, or a second priority=true |
queries | The database statements the page ran |
assets | The stylesheets, scripts and other files the page references |
a11y | Page-level accessibility findings (one h1, a title, the language) |
images | Each image: transform, sizes, ladder, formats, each derivative’s bytes and whether it is stored |
It exits 1 when the status is 400 or above or the render failed. Log lines (the engine starting, the signing key warning, the request) go to standard error before the page or the failure, so read standard output for the page and the exit status for the result; a failure to reach the database prints those lines and then MRD-3001.
render has no visitor and no session: csrfInput(), csrfToken() and csrf print nothing, and a drawn form’s _csrf field is empty. Check token handling on a running server.
Template tests
Template tests render pages through the real pipeline against the seeded database and assert on the output. Put them in tests/templates/*.test.yaml (or one directory below):
# tests/templates/site.test.yaml
- name: the home page lists what is on
path: /
template: page/entry.html
expect:
contains: ["Coming up", "Thursday Film Club"]
selector: { "h1": "Riverside Arts", "#form-newsletter": { count: 1 } }
lang: en-GB
headers: { Content-Type: text/html }
- name: a news story renders its title and hero
fixture: seed:article/new-season
template: article/entry.html
expect:
selector: { "article h1": { count: 1 }, "article picture img": { count: 1 } }
jsonld: { "@type": "Article" }
not_contains: ["undefined", "%!s("]
- name: a weekly event links its calendar file
fixture: seed:event/thursday-film-club
expect:
selector: { "a[download]": { attr: { href: /whats-on/thursday-film-club.ics } } }
- name: the events index is unchanged
path: /whats-on
locale: en-GB
snapshot: true
- name: an address with no page is not found
path: /no-such-page
expect:
status: 404 engine-dmo seed apply
engine-dmo template test A test
| Key | Meaning |
|---|---|
name | Required; what the test is called in the output |
path | The page’s address, starting with /, without the locale prefix when locale is given |
fixture | seed:<type>/<slug>: the seeded document’s address through its type’s route on the site. Use path for a nested or parented address |
site | The site’s key; the default site otherwise |
locale | The locale |
fragment | Fragments to request, as the Meridian-Fragment header names them |
template | The template the page must render with |
snapshot | true records the page’s markup and compares later runs with it |
expect | The assertions |
A test has exactly one of path and fixture.
Assertions
| Key | Passes when |
|---|---|
status | The response status is this (200 when not given) |
contains, not_contains | The page contains, or does not contain, each string |
selector | Each CSS selector’s first match has the given text; or, as a map, count matches, its text equals, its text contains, its attr values equal |
jsonld | Some object in the page’s JSON-LD (inside @graph and nested objects too) holds these values |
lang | The html element’s lang |
headers | Each header’s value contains the given text |
Every failing assertion is reported, not only the first.
Selectors support type selectors, #id, .class, [attr], [attr=value], [attr~=value], [attr^=value], [attr*=value], *, the descendant combinator (a space), the child combinator (>) and groups separated by commas.
Snapshots
A test with snapshot: true, or every test under --snapshots, records the page’s markup in tests/templates/__snapshots__/<file>/<name>.html on its first run and compares later runs with it, reporting the first line that differs. The CSRF token’s value and asset version queries are blanked first, since they change between runs. Commit the snapshots; --update records them again after an intended change.
Running
engine-dmo template test # every test
engine-dmo template test --filter "news" # tests whose name or file contains "news"
engine-dmo template test --snapshots # snapshot every test
engine-dmo template test --update # record snapshots again
engine-dmo template test --json # { passed, failed, tests: [{ name, file, line, passed, failures, snapshot }] } It prints one line per test with its file and line, the failures beneath, and a count. A failing test exits 1. A problem in a test file (a test with no name, with neither or both of path and fixture, a malformed fixture, a selector it cannot read) is reported at its line and column together with the rest, nothing renders, and the run exits 3:
tests/templates/bad.test.yaml:5:12: test "both" names both a path and a fixture; give one The tests read the database engine-dmo seed apply wrote. A test of content that is not there fails on its status.
In development
Under engine-dmo serve --dev:
- a template error renders a development error page with the file, the line and the source, instead of the site’s error template;
dump(value)and thedumpfilter render a navigable tree of a value, anddump()the whole context; both are development only;devModeis true, for markup you want only while developing;?_profileon a page links to its profile: the render’s spans, queries and each cache block’s hit, miss or skip;- templates are read afresh on every render.
In CI
engine-dmo check --no-db --strict > check.log 2>&1 || { cat check.log; exit 1; }
cat check.log; ! grep -q 'warning:' check.log # fail on template warnings too
engine-dmo build --no-db --strict # with a frontend build
engine-dmo migrate up && engine-dmo schema apply && engine-dmo seed apply
engine-dmo template test --json The first lines need no database, but they still load the configuration, so set DATABASE_URL and every other ${VAR} without a default to a placeholder; a signing_key read from a variable must be at least 32 characters even then. The last three need a disposable database with DATABASE_URL pointing at it.
Related
- CLI reference: every command and flag.
- Template API reference: the generated surface.
- Error codes.
Caching
How the page cache and the cache tag for template fragments work, what a template does that makes a page or a block uncacheable, how cached output is invalidated on publish, and the configuration keys that bound it.
Features
What the engine does for a website beyond content and templates, in both bundles, and which page explains each feature.