Unit Tests
Real tests for this site's pure filters, run live at build time and gating every real pull request.
Live Implementation
These numbers aren't typed in. This exact page's build ran the real test suite with Node's built-in test runner and reports exactly what happened, right now:
- 14 / 14 tests passing
[
{
"name": "pluralize uses the singular form for exactly one",
"passed": true
},
{
"name": "pluralize adds an s by default for anything else",
"passed": true
},
{
"name": "pluralize uses an explicit plural when given one",
"passed": true
},
{
"name": "excerpt returns short text unchanged",
"passed": true
},
{
"name": "excerpt truncates long text with an ellipsis",
"passed": true
},
{
"name": "formatNumber adds thousands separators",
"passed": true
},
{
"name": "formatNumber leaves small numbers unchanged",
"passed": true
},
{
"name": "timeAgo reports today for the current moment",
"passed": true
},
{
"name": "timeAgo reports a pluralized unit for the past",
"passed": true
},
{
"name": "timeAgo uses the singular form for exactly one unit",
"passed": true
},
{
"name": "toJson produces indented, parseable JSON",
"passed": true
},
{
"name": "jsonLd produces parseable JSON",
"passed": true
},
{
"name": "jsonLd escapes < so </script> can never appear literally",
"passed": true
},
{
"name": "demosToItemList builds real ListItem entries with correct positions",
"passed": true
}
] How It Works
Every filter this site adds a demo for — pluralize, excerpt, formatNumber, timeAgo, toJson, jsonLd, demosToItemList — is a small, pure function: same input, same output, no Eleventy, no DOM, nothing to mock. That’s exactly the shape Node’s own built-in test runner wants, so this site uses it directly instead of adding a testing framework as a dependency:
import { test } from "node:test";
import assert from "node:assert/strict";
import pluralize from "../src/_11ty/filters/pluralize.js";
test("pluralize uses the singular form for exactly one", () => {
assert.equal(pluralize(1, "demo"), "1 demo");
}); node --test finds and runs every *.test.js file under test/ with zero configuration. No dependency was added for this — node:test and node:assert have shipped inside Node itself since version 18.
Making the results actually live
Most sites would stop at “there’s a test suite.” This one goes one step further: a global data file runs the real suite, programmatically, at the exact moment this page builds, using the same node:test module’s run() function instead of shelling out to a CLI:
import { run } from "node:test";
const stream = run({ files: [testFile] });
for await (const event of stream) {
if (event.type === "test:pass") pass++;
if (event.type === "test:fail") fail++;
} The numbers in the Live Implementation above aren’t written by hand anywhere — they’re the actual result of actually running the actual tests, for this actual build. If a test ever failed, this page would show that failure the next time it built, not a stale “✔ all passing” someone forgot to update.
Folder Structure
test/filters.test.js ← the real tests
src/_data/testResults.js ← runs them at build time, feeds this page
.github/workflows/lighthouse-ci.yml ← runs npm test on every real pull request Important Files
test/filters.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import pluralize from "../src/_11ty/filters/pluralize.js";
import excerpt from "../src/_11ty/filters/excerpt.js";
import formatNumber from "../src/_11ty/filters/formatNumber.js";
import timeAgo from "../src/_11ty/filters/timeAgo.js";
import toJson from "../src/_11ty/filters/toJson.js";
import jsonLd from "../src/_11ty/filters/jsonLd.js";
import demosToItemList from "../src/_11ty/filters/demosToItemList.js";
test("pluralize uses the singular form for exactly one", () => {
assert.equal(pluralize(1, "demo"), "1 demo");
});
test("pluralize adds an s by default for anything else", () => {
assert.equal(pluralize(0, "demo"), "0 demos");
assert.equal(pluralize(3, "demo"), "3 demos");
});
test("pluralize uses an explicit plural when given one", () => {
assert.equal(pluralize(2, "story", "stories"), "2 stories");
});
test("excerpt returns short text unchanged", () => {
assert.equal(excerpt("short text", 20), "short text");
});
test("excerpt truncates long text with an ellipsis", () => {
const text = "one two three four five six seven";
assert.equal(excerpt(text, 3), "one two three…");
});
test("formatNumber adds thousands separators", () => {
assert.equal(formatNumber(1234567), "1,234,567");
});
test("formatNumber leaves small numbers unchanged", () => {
assert.equal(formatNumber(42), "42");
});
test("timeAgo reports today for the current moment", () => {
assert.equal(timeAgo(new Date()), "today");
});
test("timeAgo reports a pluralized unit for the past", () => {
const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
assert.equal(timeAgo(threeDaysAgo), "3 days ago");
});
test("timeAgo uses the singular form for exactly one unit", () => {
const oneDayAgo = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000);
assert.equal(timeAgo(oneDayAgo), "1 day ago");
});
test("toJson produces indented, parseable JSON", () => {
const output = toJson({ a: 1 });
assert.equal(JSON.parse(output).a, 1);
assert.match(output, /\n/);
});
test("jsonLd produces parseable JSON", () => {
const output = jsonLd({ headline: "Test" });
assert.deepEqual(JSON.parse(output), { headline: "Test" });
});
test("jsonLd escapes < so </script> can never appear literally", () => {
const output = jsonLd({ title: "</script><script>alert(1)</script>" });
assert.equal(output.includes("</script>"), false);
assert.equal(JSON.parse(output).title, "</script><script>alert(1)</script>");
});
test("demosToItemList builds real ListItem entries with correct positions", () => {
const demos = [
{ url: "/demos/a/", data: { title: "A" } },
{ url: "/demos/b/", data: { title: "B" } },
];
const list = demosToItemList(demos, "https://example.com");
assert.deepEqual(list, [
{ "@type": "ListItem", position: 1, url: "https://example.com/demos/a/", name: "A" },
{ "@type": "ListItem", position: 2, url: "https://example.com/demos/b/", name: "B" },
]);
}); src/_data/testResults.js
import { run } from "node:test";
import path from "node:path";
export default async function () {
const testFile = path.resolve(process.cwd(), "test/filters.test.js");
const stream = run({ files: [testFile] });
let pass = 0;
let fail = 0;
const results = [];
for await (const event of stream) {
if (event.type === "test:pass") {
pass++;
results.push({ name: event.data.name, passed: true });
}
if (event.type === "test:fail") {
fail++;
results.push({ name: event.data.name, passed: false });
}
}
return { pass, fail, total: pass + fail, results };
} Notes
- These tests run in the real GitHub Actions workflow that gates pull requests, before the site even builds — a broken filter fails the check, it doesn’t just sit unnoticed in a
test/folder nobody runs. - Writing these caught a real mistake: an early version of the
pluralizetest assertedpluralize(0, "demo")should return"0 demo". The actual function was already correct — zero is not one, so it correctly pluralizes to"0 demos"— the test itself had the bug. That’s the whole point of a test: it doesn’t know which side is wrong until you look.