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
97 lines
4.2 KiB
JavaScript
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;
|