|
| 1 | +/** |
| 2 | + * Footer NAV_LINKS structural lint (auto-qa). |
| 3 | + * |
| 4 | + * Pins the structural invariants of the Footer's NAV_LINKS array. Two |
| 5 | + * concerns it catches: |
| 6 | + * |
| 7 | + * (a) Mis-shaped entries (missing label/href, or `external: true` on |
| 8 | + * a path-relative href, which would render as a broken absolute |
| 9 | + * link) |
| 10 | + * (b) Documentation link target — currently the unstable external |
| 11 | + * `https://docs.futarchy.fi`. PR #41 (open) replaces this with |
| 12 | + * in-app `/documents` (alias `/docs`). The test accepts EITHER |
| 13 | + * so it stays green across the transition, and pins which states |
| 14 | + * are valid so a typo or accidental third value (`/doc`, |
| 15 | + * `https://github.com/...`) surfaces immediately. |
| 16 | + * |
| 17 | + * Static-grep style — no import, since the Footer is a Next.js JSX file |
| 18 | + * and node:test's strict ESM doesn't resolve extension-less relative |
| 19 | + * imports the way Next does. |
| 20 | + */ |
| 21 | + |
| 22 | +import { test } from 'node:test'; |
| 23 | +import assert from 'node:assert/strict'; |
| 24 | +import { readFileSync } from 'node:fs'; |
| 25 | + |
| 26 | +const FOOTER_PATH = new URL('../../src/components/common/Footer.jsx', import.meta.url); |
| 27 | + |
| 28 | +function parseNavLinks() { |
| 29 | + const src = readFileSync(FOOTER_PATH, 'utf8'); |
| 30 | + const m = src.match(/const NAV_LINKS = \[([\s\S]*?)\];/); |
| 31 | + assert.ok(m, 'NAV_LINKS array not found in Footer.jsx — has the file been refactored?'); |
| 32 | + |
| 33 | + // Pull each `{ ... }` object literal out of the array body. Avoids |
| 34 | + // pulling in a JS parser dep — the array shape is small and stable. |
| 35 | + const entries = []; |
| 36 | + const re = /\{\s*([^{}]+?)\s*\}/g; |
| 37 | + let item; |
| 38 | + while ((item = re.exec(m[1])) !== null) { |
| 39 | + const obj = {}; |
| 40 | + // Match key: value pairs. Handles single-quoted strings and bare |
| 41 | + // booleans. |
| 42 | + const pairRe = /(\w+)\s*:\s*('([^']*)'|true|false)/g; |
| 43 | + let p; |
| 44 | + while ((p = pairRe.exec(item[1])) !== null) { |
| 45 | + obj[p[1]] = p[3] !== undefined ? p[3] |
| 46 | + : p[2] === 'true' ? true |
| 47 | + : p[2] === 'false' ? false : p[2]; |
| 48 | + } |
| 49 | + entries.push(obj); |
| 50 | + } |
| 51 | + return entries; |
| 52 | +} |
| 53 | + |
| 54 | +const NAV_LINKS = parseNavLinks(); |
| 55 | + |
| 56 | +test('Footer parser — extracts at least one NAV_LINKS entry', () => { |
| 57 | + assert.ok(NAV_LINKS.length > 0, |
| 58 | + 'NAV_LINKS parsed as empty — the regex extractor is broken or the array is empty.'); |
| 59 | +}); |
| 60 | + |
| 61 | +test('Footer — every NAV_LINKS entry has a label and an href', () => { |
| 62 | + for (const e of NAV_LINKS) { |
| 63 | + assert.ok(typeof e.label === 'string' && e.label.length > 0, |
| 64 | + `entry missing label: ${JSON.stringify(e)}`); |
| 65 | + assert.ok(typeof e.href === 'string' && e.href.length > 0, |
| 66 | + `entry missing href: ${JSON.stringify(e)}`); |
| 67 | + } |
| 68 | +}); |
| 69 | + |
| 70 | +test('Footer — `external: true` entries have an absolute http(s) href', () => { |
| 71 | + for (const e of NAV_LINKS) { |
| 72 | + if (e.external === true) { |
| 73 | + assert.ok(/^https?:\/\//.test(e.href), |
| 74 | + `entry "${e.label}" is marked external but href is not absolute: ${e.href}`); |
| 75 | + } |
| 76 | + } |
| 77 | +}); |
| 78 | + |
| 79 | +test('Footer — non-external entries have a path-relative href', () => { |
| 80 | + for (const e of NAV_LINKS) { |
| 81 | + if (e.external !== true) { |
| 82 | + assert.ok(e.href.startsWith('/'), |
| 83 | + `entry "${e.label}" is not marked external but href is not path-relative: ${e.href}. ` + |
| 84 | + `If the link is external, set external: true.`); |
| 85 | + } |
| 86 | + } |
| 87 | +}); |
| 88 | + |
| 89 | +test('PR #41 — Documentation link target is one of the accepted values', () => { |
| 90 | + // Accepted set spans the pre- and post-PR-#41 worlds. If the link |
| 91 | + // gets pointed at a third value (typo, accidental redirect to a |
| 92 | + // GitHub URL, etc.) this test surfaces it. |
| 93 | + const ACCEPTED_DOCS_HREFS = new Set([ |
| 94 | + 'https://docs.futarchy.fi', // pre-PR-#41 (current) |
| 95 | + '/documents', // post-PR-#41 |
| 96 | + '/docs', // post-PR-#41 alias |
| 97 | + ]); |
| 98 | + const docs = NAV_LINKS.find(e => e.label === 'Documentation'); |
| 99 | + assert.ok(docs, 'no NAV_LINKS entry with label "Documentation"'); |
| 100 | + assert.ok(ACCEPTED_DOCS_HREFS.has(docs.href), |
| 101 | + `Documentation href "${docs.href}" is not in the accepted set ` + |
| 102 | + `[${[...ACCEPTED_DOCS_HREFS].join(', ')}]. ` + |
| 103 | + `Either fix the link or add the new value to ACCEPTED_DOCS_HREFS in this test.`); |
| 104 | +}); |
| 105 | + |
| 106 | +test('PR #41 — Status link is the canonical status URL', () => { |
| 107 | + // Adjacent invariant — the same NAV_LINKS array also holds Status, |
| 108 | + // which has historically been a deployment regression target. Pin it. |
| 109 | + const status = NAV_LINKS.find(e => e.label === 'Status'); |
| 110 | + assert.ok(status, 'no NAV_LINKS entry with label "Status"'); |
| 111 | + assert.equal(status.href, 'https://status.futarchy.fi', |
| 112 | + `Status link drifted from canonical URL; got "${status.href}"`); |
| 113 | + assert.equal(status.external, true, |
| 114 | + `Status link must be marked external: true`); |
| 115 | +}); |
0 commit comments