Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
5.4 kB · 145 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146/** * Checks the FAQ and About content files against the schema in * src/content/render.ts. * * tsc catches a value of the wrong type, but not a key the renderer does not * know: an extra or misspelled tag on a block type-checks and then renders as * nothing, so a paragraph disappears from the live page with no error. This * rejects any key the renderer would not read. * * Run with `npm test`. Node's own runner, so there is nothing to install. */import { test } from "node:test";import assert from "node:assert/strict";import { readFile } from "node:fs/promises";
import { ABOUT_PAGES } from "../src/destinations.ts";
const read = async (name) => JSON.parse( await readFile(new URL(`../src/content/${name}.json`, import.meta.url)), );
const faq = await read("faq");const about = await read("about");
/** The keys an object is allowed to carry, and nothing else. */function keys(value, allowed, where) { assert.ok( value && typeof value === "object" && !Array.isArray(value), `${where} is not an object`, ); for (const key of Object.keys(value)) { assert.ok(allowed.includes(key), `${where} has unknown key "${key}"`); }}
function checkText(text, where) { if (typeof text === "string") { assert.ok(text.length > 0, `${where} is empty`); return; } assert.ok(Array.isArray(text), `${where} is neither a string nor a run`); for (const [i, run] of text.entries()) { if (typeof run === "string") { // A run is one paragraph's worth of text, joined with nothing between // the pieces — that is what lets a link sit tight against the comma // after it. Two bare strings side by side is therefore always a // sentence running into the next one with no space, and it renders that // way silently. Separate paragraphs are a `credits` block. assert.notEqual( typeof text[i + 1], "string", `${where}[${i}] and [${i + 1}] are both plain text, and will render ` + `with nothing between them`, ); continue; } const at = `${where}[${i}]`; if ("b" in run) { keys(run, ["b"], at); assert.equal(typeof run.b, "string", `${at}.b is not a string`); } else { keys(run, ["a", "href"], at); assert.equal(typeof run.a, "string", `${at}.a is not a string`); assert.match(run.href, /^https?:\/\/|^\//, `${at}.href is not a link`); } }}
function checkBlocks(blocks, where) { assert.ok(blocks.length > 0, `${where} has no blocks`); for (const [i, block] of blocks.entries()) { const at = `${where}[${i}]`; if ("p" in block) { keys(block, ["p"], at); checkText(block.p, `${at}.p`); } else if ("list" in block) { keys(block, ["list"], at); block.list.forEach((item, j) => checkText(item, `${at}.list[${j}]`)); } else if ("credits" in block) { keys(block, ["credits"], at); for (const notice of block.credits) { assert.equal(typeof notice, "string", `${at}.credits is not strings`); } } else { assert.fail(`${at} is not a p, list or credits block`); } }}
function checkHead(content, where, allowed) { keys(content, ["head", ...allowed], where); // The title is the hidden h1 a screen reader gets. An empty one is a route // with no name. keys(content.head, ["title"], `${where}.head`); assert.ok(content.head.title.length > 0, `${where}.head.title is empty`);}
test("faq.json fits the schema", () => { checkHead(faq, "faq", ["questions"]); faq.questions.forEach((question, i) => { const at = `faq.questions[${i}]`; keys(question, ["q", "open", "blocks"], at); assert.ok(question.q.length > 0, `${at}.q is empty`); checkBlocks(question.blocks, `${at}.blocks`); });});
// The page has to read as answers rather than as a row of shut drawers, and// more than one open drawer defeats that.test("faq.json opens exactly the first question", () => { const open = faq.questions.filter((question) => question.open === true); assert.equal(open.length, 1); assert.equal(faq.questions[0].open, true);});
test("about.json fits the schema", () => { checkHead(about, "about", ["lead", "pages"]); checkText(about.lead, "about.lead");
for (const [id, page] of Object.entries(about.pages)) { const at = `about.pages.${id}`; keys(page, ["blurb", "sections"], at); checkText(page.blurb, `${at}.blurb`); (page.sections ?? []).forEach((section, i) => { const where = `${at}.sections[${i}]`; keys(section, ["heading", "blocks"], where); assert.ok(section.heading.length > 0, `${where}.heading is empty`); checkBlocks(section.blocks, `${where}.blocks`); }); }});
// tsc catches a missing page — about.json is typed as a Record over the ids —// but not one whose copy is there and empty, which renders as a card with a// heading and nothing under it. The FAQ is the exception: its copy is faq.json,// and the About screen puts that page up with faqCard().test("every page under About has copy, and every page with copy is one", () => { const ids = ABOUT_PAGES.map((page) => page.id); assert.deepEqual(Object.keys(about.pages).sort(), [...ids].sort()); for (const [id, page] of Object.entries(about.pages)) { if (id === "faq") continue; assert.ok(page.sections?.length, `about.pages.${id} has no sections`); }});