87 lines
3.3 KiB
JavaScript
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}"`
|
|
);
|
|
}
|
|
}
|
|
});
|