Files
blog/test/build-integrity.test.js

87 lines
3.3 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
const siteDir = path.resolve("_site");
function getAllHtmlFiles(dir) {
let results = [];
const list = fs.readdirSync(dir);
for (const file of list) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
results = results.concat(getAllHtmlFiles(filePath));
} else if (file.endsWith(".html")) {
results.push(filePath);
}
}
return results;
}
test("Build & Link Integrity: Output inspection and link verification", () => {
// Ensure fresh build exists
if (!fs.existsSync(path.join(siteDir, "index.html"))) {
execSync("npm run build", {
stdio: "pipe",
env: { ...process.env, SITE_DOMAIN: process.env.SITE_DOMAIN || "riverrooks.dev" }
});
}
// 1. Core Pages Exist
assert.ok(fs.existsSync(path.join(siteDir, "index.html")), "Root _site/index.html must exist");
assert.ok(fs.existsSync(path.join(siteDir, "dispatches/index.html")), "_site/dispatches/index.html must exist");
assert.ok(fs.existsSync(path.join(siteDir, "assets/css/main.css")), "_site/assets/css/main.css must exist");
const htmlFiles = getAllHtmlFiles(siteDir);
assert.ok(htmlFiles.length >= 4, "Must compile all core pages and articles");
for (const htmlFile of htmlFiles) {
const relHtml = path.relative(siteDir, htmlFile);
const content = fs.readFileSync(htmlFile, "utf-8");
// 2. HTML Boilerplate Invariants
assert.match(content, /<!DOCTYPE html>/i, `${relHtml} must contain <!DOCTYPE html>`);
assert.match(content, /<meta\s+name="viewport"/i, `${relHtml} must have viewport meta tag`);
assert.match(content, /<link\s+rel="stylesheet"\s+href="\/assets\/css\/main\.css(?:\?[^"]*)?">/i, `${relHtml} must link to main.css`);
// 3. Internal Link Integrity (No 404s!)
const internalLinks = [...content.matchAll(/href="(\/[^"#?]*)"/g)].map((m) => m[1]);
for (const link of internalLinks) {
if (link.startsWith("/feed.xml")) continue; // dynamic RSS route
// Normalize link to file on disk: /dispatches/ -> _site/dispatches/index.html or _site/dispatches.html
let targetPath;
if (link.endsWith("/")) {
targetPath = path.join(siteDir, link, "index.html");
} else if (path.extname(link)) {
targetPath = path.join(siteDir, link);
} else {
targetPath = path.join(siteDir, `${link}.html`);
if (!fs.existsSync(targetPath)) {
targetPath = path.join(siteDir, link, "index.html");
}
}
assert.ok(
fs.existsSync(targetPath),
`Broken internal link in ${relHtml}: href="${link}" resolves to missing file: ${targetPath}`
);
}
// 4. Anchor Link Integrity (TOC anchor links match actual heading IDs)
const anchorLinks = [...content.matchAll(/href="#([^"]+)"/g)].map((m) => m[1]);
for (const anchor of anchorLinks) {
// Ignore skip-to-content or standard body anchors
if (anchor === "main-content") continue;
const expectedIdRegex = new RegExp(`id="${anchor}"`, "i");
assert.match(
content,
expectedIdRegex,
`Broken TOC anchor link in ${relHtml}: href="#${anchor}" has no corresponding id="${anchor}"`
);
}
}
});