Files
Mermaid-Preview-Nova/Tests/nova-stub.js
T
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

108 lines
4.2 KiB
JavaScript

// A minimal stand-in for Nova's extension runtime, backed by the real fs, so the
// extension's own Scripts/*.js can be exercised outside Nova.
const fs = require("fs");
const nodePath = require("path");
class Disposable {
constructor(fn) { this.fn = fn; this.disposed = false; }
dispose() { if (!this.disposed) { this.disposed = true; if (this.fn) this.fn(); } }
}
class CompositeDisposable {
constructor() { this.items = []; }
add(d) { this.items.push(d); }
dispose() { for (const d of this.items) d.dispose(); this.items = []; }
}
class Range {
constructor(start, end) { this.start = start; this.end = end; }
}
class FakeEditor {
constructor(path, text) {
this.document = {
path, uri: `file://${path}`, isUntitled: false, isRemote: false,
syntax: path.endsWith(".md") ? "markdown" : "html",
length: text.length,
getTextInRange: () => text,
};
this.handlers = { save: [], stop: [], destroy: [] };
}
onDidSave(cb) { this.handlers.save.push(cb); return new Disposable(); }
onDidStopChanging(cb) { this.handlers.stop.push(cb); return new Disposable(); }
onDidDestroy(cb) { this.handlers.destroy.push(cb); return new Disposable(); }
fireDestroy() { for (const cb of this.handlers.destroy.slice()) cb(this); }
}
function build({ root, extensionPath }) {
const config = new Map();
const commands = new Map();
const configApi = {
get: (k) => (config.has(k) ? config.get(k) : null),
set: (k, v) => config.set(k, v),
onDidChange: () => new Disposable(),
};
const nova = {
extension: { path: extensionPath },
config: configApi,
commands: {
register: (name, cb) => { commands.set(name, cb); return new Disposable(() => commands.delete(name)); },
},
path: {
join: (...a) => nodePath.join(...a),
dirname: (p) => nodePath.dirname(p),
basename: (p) => nodePath.basename(p),
extname: (p) => nodePath.extname(p),
isAbsolute: (p) => nodePath.isAbsolute(p),
relative: (a, b) => nodePath.relative(a, b),
splitext: (p) => { const e = nodePath.extname(p); return [p.slice(0, p.length - e.length), e]; },
},
fs: {
F_OK: fs.constants.F_OK,
access: (p) => fs.existsSync(p),
mkdir: (p) => fs.mkdirSync(p),
copy: (a, b) => fs.copyFileSync(a, b),
remove: (p) => fs.unlinkSync(p),
rmdir: (p) => fs.rmdirSync(p),
listdir: (p) => fs.readdirSync(p),
stat: (p) => { const st = fs.statSync(p); return { isDirectory: () => st.isDirectory() }; },
open: (p, mode) => {
if (mode === "w") {
const chunks = [];
return { write: (s) => chunks.push(s), close: () => fs.writeFileSync(p, chunks.join("")) };
}
const text = fs.readFileSync(p, "utf8");
return { read: () => text, close: () => {} };
},
},
workspace: {
path: root,
previewRootPath: root,
config: { get: () => null, set: () => {} },
textEditors: [],
activeTextEditor: null,
openFile: async (p) => {
let editor = nova.workspace.textEditors.find((e) => e.document.path === p);
if (!editor) {
editor = new FakeEditor(p, require('fs').readFileSync(p, 'utf8'));
nova.workspace.textEditors.push(editor);
}
return editor;
},
showInformativeMessage: (m) => messages.push(["info", m]),
showErrorMessage: (m) => messages.push(["error", m]),
showActionPanel: (m, o, cb) => cb(0),
},
};
const messages = [];
nova.__messages = messages;
nova.__invoke = (name, ...args) => commands.get(name)(...args);
nova.__close = (editor) => {
nova.workspace.textEditors = nova.workspace.textEditors.filter((e) => e !== editor);
editor.fireDestroy();
};
return nova;
}
module.exports = { build, FakeEditor, Disposable, CompositeDisposable, Range };