Files
thatguygriffandClaude Opus 5 aebc72b85e Add CLAUDE.md and the test harness
CLAUDE.md records the Nova constraints this extension is built around —
established by reading Nova 14.1's bundle and probing its preview server,
and expensive to re-derive: no previewer extension point, generated pages
must live under the preview root, dot-directories are served, and no API
reaches into a preview WebView. It also flags the one unverified
assumption, that a tab showing Preview still appears in
nova.workspace.textEditors, with its symptom and fix.

Tests/ is the harness that built the extension, kept rather than left in a
scratchpad: a stubbed Nova runtime backed by the real filesystem, node
suites for generation and the four close paths, and a headless-browser
render test that skips itself without a browser. It caught the ./ doubling
in rebased links and checkbox state not surviving serialization.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TueXpMi7eAtgWPuMr4ii52
2026-08-20 23:02:08 -03:00

97 lines
4.2 KiB
JavaScript

// Renders a fixture through the real render.js in a headless browser, because
// everything the page does — Markdown, Mermaid, the DOM fix-ups — only happens
// where there is a DOM. Skips itself when no browser can be found.
const fs = require("fs");
const path = require("path");
const os = require("os");
const { execFileSync } = require("child_process");
const EXT = path.join(__dirname, "..");
const { buildDocument } = require(path.join(EXT, "Scripts", "render.js"));
function findBrowser() {
if (process.env.MERMAID_PREVIEW_CHROME) return process.env.MERMAID_PREVIEW_CHROME;
const cache = path.join(os.homedir(), "Library", "Caches", "ms-playwright");
if (fs.existsSync(cache)) {
for (const entry of fs.readdirSync(cache)) {
const candidate = path.join(cache, entry, "chrome-headless-shell-mac-arm64", "chrome-headless-shell");
if (fs.existsSync(candidate)) return candidate;
}
}
const chrome = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
return fs.existsSync(chrome) ? chrome : null;
}
const browser = findBrowser();
if (!browser) {
console.log("SKIP no headless browser found (set MERMAID_PREVIEW_CHROME to one)");
process.exit(0);
}
const work = path.join(__dirname, ".tmp", "render");
fs.rmSync(work, { recursive: true, force: true });
fs.mkdirSync(path.join(work, "out", "assets", "vendor"), { recursive: true });
fs.mkdirSync(path.join(work, "docs"), { recursive: true });
for (const name of ["preview.css", "preview.js"]) {
fs.copyFileSync(path.join(EXT, "Assets", name), path.join(work, "out", "assets", name));
}
for (const name of ["markdown-it.min.js", "mermaid.min.js"]) {
fs.copyFileSync(path.join(EXT, "Assets", "vendor", name), path.join(work, "out", "assets", "vendor", name));
}
const markdown = fs.readFileSync(path.join(__dirname, "fixtures", "sample.md"), "utf8");
fs.writeFileSync(path.join(work, "docs", "sample.md"), markdown);
const results = [];
const check = (name, ok) => results.push([ok ? "PASS" : "FAIL", name]);
function render(theme) {
const page = path.join(work, "out", `sample-${theme}.html`);
fs.writeFileSync(
page,
buildDocument({
markdown,
title: "sample.md",
sourcePath: path.join(work, "docs", "sample.md"),
sourceRelDir: "../docs/",
assetsRelDir: "assets",
mode: "markdown",
theme,
contentWidth: 860,
})
);
return execFileSync(
browser,
["--headless", "--disable-gpu", "--no-sandbox", "--virtual-time-budget=10000", "--dump-dom", `file://${page}`],
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 32 * 1024 * 1024 }
);
}
const dom = render("auto");
// Only the rendered <main>. The Markdown source is embedded further down the
// page as data, so a naive slice to end-of-document would find anything.
const body = dom.slice(dom.indexOf('id="content"'), dom.indexOf('id="preview-data"'));
check("both valid diagrams drawn", (dom.match(/<svg/g) || []).length === 2);
check("the broken diagram reports in place", dom.includes("mermaid-error-message") && dom.includes("is-error"));
check("front matter is not rendered", !body.includes("Front matter should be hidden"));
check("headings get ids", dom.includes('<h2 id="a-second-heading"'));
check("in-document anchors are left alone", dom.includes('href="#a-second-heading"'));
check("relative links are rebased once", dom.includes('href="../docs/other.md"') && !dom.includes("docs/./"));
check("relative images are rebased", dom.includes('src="../docs/img/logo.png"'));
check("task list becomes checkboxes", (dom.match(/<input type="checkbox"/g) || []).length === 2);
check("checked state survives", dom.includes('checked=""'));
check("wide tables scroll on their own", dom.includes('class="table-scroll"'));
check("content is revealed", !body.includes('aria-busy="true"'));
const dark = render("dark");
check("dark theme reaches the page", dark.includes('data-theme="dark"'));
check("dark theme still draws diagrams", (dark.match(/<svg/g) || []).length === 2);
for (const [status, name] of results) console.log(`${status} ${name}`);
process.exitCode = results.some(([s]) => s === "FAIL") ? 1 : 0;