Testing
Components are functions returning strings, so they need no renderer, no DOM and no test harness beyond whatever the project already runs.
import { test } from 'node:test';
import assert from 'node:assert/strict';
const Item = (label: string) => html`<li>${label}</li>`;
test('escapes the label', () => {
assert.equal(Item('a < b').markup, '<li>a < b</li>');
});Assert the markup is clean
check() returns the problems in a rendered page, so the general-purpose assertion is that there are none.
import { check } from '@itsy/html/check';
test('the page is well formed', () => {
assert.deepEqual(check(Page(data)), []);
});That one line covers unclosed tags, mismatched end tags, nesting a browser would rewrite, duplicate attributes, duplicate ids, id references pointing nowhere, any URL the guard blocked, and the accessibility rules. Run it over the page rather than a fragment — the ids and the references are only visible at page level, and so are the <html>, the <title> and the for targets that three of the accessibility rules read.
Pass { a11y: false } for the markup check on its own.
When it fails, the array says where:
for (const p of check(Page(data))) console.log('rule' in p ? p.rule : p.code, p.message, p.near);Confirm the dev build
In the production build check() always returns []. A suite that resolves the production build will pass every markup assertion, whatever the markup is.
Run tests with the condition set:
node --conditions=development --test test/*.test.tsAnd to be certain the suite cannot pass vacuously, assert that the checks are on. check.enabled is false on the production build:
test('the checks are active', () => assert(check.enabled));TIP
Vitest resolves through Vite, which applies the development condition in dev. When it is unclear which build a runner resolved, the assertion above answers it in one run.
Storybook
A decorator runs the check on every story, so a broken component fails right in the story being viewed.
// .storybook/preview.ts
export const decorators = [
(story) => {
const markup = story().markup;
for (const p of check(markup)) console.warn(`[${'rule' in p ? p.rule : `html ${p.code}`}] ${p.message}`, p.near);
return markup;
},
];Snapshots
Snapshot view.markup, never the Html itself — a serializer will otherwise record an object.
expect(Page(data).markup).toMatchSnapshot(); // or the runner's equivalentWhitespace in the output is stable: static markup collapses to single spaces, so reindenting a template does not churn the snapshot. Reflowing a line does, since a newline becomes a space.