diff --git a/tests/reading-list.test.js b/tests/reading-list.test.js deleted file mode 100644 index 388b98e..0000000 --- a/tests/reading-list.test.js +++ /dev/null @@ -1,328 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname, join } from "node:path"; -import { READING_LIST } from "../data/reading-list.js"; -import { - escapeHtml, - groupByCategory, - isValidEntry, - isValidUrl, - renderReadingList, -} from "../js/reading-list.js"; - -const root = join(dirname(fileURLToPath(import.meta.url)), ".."); -const readingHtml = readFileSync(join(root, "reading.html"), "utf8"); -const indexHtml = readFileSync(join(root, "index.html"), "utf8"); -const contactHtml = readFileSync(join(root, "contact.html"), "utf8"); -const rendererSource = readFileSync(join(root, "js/reading-list.js"), "utf8"); - -const ANCHOR_RE = /]*>([^<]*)<\/a>/g; -const ENTRY_LI_RE = /
  • /g; -const SECTION_RE = /
    { - assert.match(readingHtml, /]*id="reading-list"/); - assert.match( - readingHtml, - /]*type="module"[^>]*src="js\/reading-list\.js"/ - ); -}); - -test("home and contact pages link to the reading list page from the nav", () => { - assert.match(indexHtml, /Reading<\/a>/); - assert.match(contactHtml, /Reading<\/a>/); -}); - -test("reading page nav marks the reading link as the current page", () => { - assert.match( - readingHtml, - /]*aria-current="page"[^>]*>Reading<\/a>/ - ); -}); - -test("renderer module loads the reading list data file", () => { - assert.match( - rendererSource, - /import\s*\{\s*READING_LIST\s*\}\s*from\s*"\.\.\/data\/reading-list\.js"/ - ); -}); - -// --------------------------------------------------------------------------- -// Data file tests: every entry is renderable, note stays optional -// --------------------------------------------------------------------------- - -test("every entry in the data file has a title, a category, and a valid http(s) url", () => { - assert.ok(Array.isArray(READING_LIST), "data file exports an array"); - assert.ok(READING_LIST.length > 0, "data file has at least one entry"); - for (const entry of READING_LIST) { - assert.ok( - typeof entry.title === "string" && entry.title.trim().length > 0, - `entry needs a title: ${JSON.stringify(entry)}` - ); - assert.ok( - typeof entry.category === "string" && entry.category.trim().length > 0, - `entry needs a category: ${JSON.stringify(entry)}` - ); - assert.ok( - isValidUrl(entry.url), - `entry url must be an absolute http(s) url: ${entry.url}` - ); - assert.ok( - entry.note === undefined || - (typeof entry.note === "string" && entry.note.length > 0), - `note must be a non-empty string or absent: ${JSON.stringify(entry)}` - ); - } -}); - -test("seed data includes an entry without a note so the optional path is exercised", () => { - assert.ok( - READING_LIST.some((entry) => !entry.note), - "expected at least one entry with no note" - ); -}); - -// --------------------------------------------------------------------------- -// Renderer tests: grouping, links, notes, and robustness -// --------------------------------------------------------------------------- - -test("renders links grouped by category from the data file", () => { - const html = renderReadingList(READING_LIST); - const categories = [...new Set(READING_LIST.map((entry) => entry.category))]; - - assert.equal((html.match(SECTION_RE) || []).length, categories.length); - for (const category of categories) { - assert.ok( - html.includes(`>${escapeHtml(category)}`), - `expected a heading for category ${category}` - ); - } - for (const entry of READING_LIST) { - assert.ok( - html.includes(escapeHtml(entry.title)), - `expected title ${entry.title}` - ); - assert.ok(html.includes(entry.url), `expected url ${entry.url}`); - } -}); - -test("each rendered link has a valid href and shows its title, with the note shown only when present", () => { - const fixture = [ - { - category: "Testing", - title: "With a note", - url: "https://example.com/with-note", - note: "One line of context.", - }, - { - category: "Testing", - title: "Without a note", - url: "https://example.com/without-note", - }, - ]; - const html = renderReadingList(fixture); - - const anchors = [...html.matchAll(ANCHOR_RE)]; - assert.equal(anchors.length, 2); - for (const [, href, title] of anchors) { - assert.ok(isValidUrl(href), `href must be a valid http(s) url: ${href}`); - assert.ok( - fixture.some((entry) => entry.title === title && entry.url === href), - `rendered link ${title} -> ${href} must match a data entry` - ); - } - - // All reading links point at external sites, so each carries the - // rel="noopener noreferrer" hardening attribute. - assert.equal( - (html.match(/rel="noopener noreferrer"/g) || []).length, - anchors.length, - "every rendered link carries rel=\"noopener noreferrer\"" - ); - - assert.ok(html.includes("One line of context."), "note is rendered when present"); - assert.equal( - (html.match(/

    /g) || []).length, - 1, - "no note paragraph for the entry without a note" - ); -}); - -test("renders correctly against a 500+ entry fixture", () => { - const fixture = makeFixture(500, ["A", "B", "C", "D", "E"]); - - const start = performance.now(); - const html = renderReadingList(fixture); - const elapsed = performance.now() - start; - - assert.ok( - elapsed < 5000, - `rendering 500 entries should stay fast, took ${elapsed.toFixed(1)}ms` - ); - - // Every entry rendered exactly once. - assert.equal((html.match(ENTRY_LI_RE) || []).length, fixture.length); - assert.equal((html.match(ANCHOR_RE) || []).length, fixture.length); - assert.ok(html.includes("https://example.com/articles/1")); - assert.ok(html.includes("https://example.com/articles/500")); - - // Notes rendered only for entries that have them. - assert.equal( - (html.match(/

    /g) || []).length, - fixture.filter((entry) => entry.note).length - ); - - // Grouped by category: one section per category, containing exactly that - // category's entries, in the first-seen category order. - const expectedOrder = [...new Set(fixture.map((entry) => entry.category))]; - const expectedCounts = new Map(); - for (const entry of fixture) { - expectedCounts.set(entry.category, (expectedCounts.get(entry.category) || 0) + 1); - } - - const sections = html.split(/

    ]*>/).slice(1); - assert.equal(sections.length, expectedOrder.length); - sections.forEach((section, index) => { - const category = expectedOrder[index]; - const heading = section.match(/]*>([^<]*)<\/h2>/); - assert.ok(heading, `section ${index} should have a heading`); - assert.equal(heading[1], category); - assert.equal( - (section.match(ENTRY_LI_RE) || []).length, - expectedCounts.get(category), - `section for ${category} should contain exactly its own entries` - ); - }); -}); - -test("escapes HTML in titles, notes, and categories", () => { - const nasty = [ - { - category: 'Design ', - title: '', - url: "https://example.com/?a=1&b=2", - note: 'note "quoted" & bold', - }, - ]; - const html = renderReadingList(nasty); - - assert.ok(!html.includes("