test(cli): pin the viewer tooltip half — INFO maps and md() (refs #125)
The headless viewer suite covered only the DOT half (normalizeModel/ genDot); the tooltip half — INFO.B (collapsed), INFO.C (expanded cluster), INFO.P (ports), and md()'s markdown-to-HTML step — was asserted by no test. viewer_tooltip.mjs pins the current shapes; graph-viewer.js only hoists md() above the browser guard and exports it (location-only move, no behaviour change), so the pin can drive the real conversion headlessly. Surfaced by the grounding-check on the #125 composite-doc spec: its viewer assumptions had no current-behaviour pin. refs #125
This commit is contained in:
@@ -186,12 +186,16 @@ function genDot(def, role, expandedSet, showTypes) {
|
||||
}
|
||||
|
||||
// === styled tooltip + interaction ============================================
|
||||
// `md` is a pure string transform (no DOM access) hoisted above the document
|
||||
// guard so it can be exported for headless testing (see module.exports below);
|
||||
// this is a location-only move, not a behaviour change — it is still called
|
||||
// exactly the same way from `showTip` inside the browser-only block.
|
||||
const md = (s) => s.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/`(.+?)`/g, "<code>$1</code>").replace(/\n/g, "<br>");
|
||||
// The DOM bootstrap runs only in the browser. Under a node test harness there is
|
||||
// no `document`/`Viz`, so the whole interaction block is skipped (the pure parts
|
||||
// — normalizeModel, genDot — are exported below for headless testing).
|
||||
// — normalizeModel, genDot, md — are exported below for headless testing).
|
||||
if (typeof document !== "undefined") {
|
||||
const tip = document.getElementById("tip");
|
||||
const md = (s) => s.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/`(.+?)`/g, "<code>$1</code>").replace(/\n/g, "<br>");
|
||||
function showTip(text, x, y) {
|
||||
tip.innerHTML = md(text); tip.style.display = "block";
|
||||
const w = tip.offsetWidth, h = tip.offsetHeight;
|
||||
@@ -253,6 +257,8 @@ Viz.instance().then(v => { viz = v; show(); }).catch(e => { document.getElementB
|
||||
|
||||
// Headless export: the node DOT-id guard requires this file (after setting
|
||||
// global.window) and exercises the pure generator. No effect in the browser.
|
||||
// `md` is included solely so the tooltip guard (viewer_tooltip.mjs) can pin its
|
||||
// markdown-to-HTML conversion; it is unchanged, just now reachable headlessly.
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = { normalizeModel, genDot };
|
||||
module.exports = { normalizeModel, genDot, md };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Headless tooltip guard for the `aura graph` viewer (graph-viewer.js).
|
||||
//
|
||||
// Property protected: the INFO maps genDot populates (INFO.B for a node body,
|
||||
// INFO.C for an expanded composite's cluster frame, INFO.P for a port) and the
|
||||
// md() markdown-to-HTML step that turns an INFO entry into the #tip div's
|
||||
// innerHTML all keep their CURRENT exact string shape. Before this guard the DOT
|
||||
// half of the viewer (normalizeModel/genDot output) was covered by the sibling
|
||||
// viewer_*.mjs suite, but the INFO population (addInfo, the composite-cluster
|
||||
// caption) and md() were asserted by no test — a silent wording/markup change
|
||||
// there would reach the browser tooltip undetected.
|
||||
//
|
||||
// It loads the real viewer module and drives the exported pure `genDot` (for
|
||||
// INFO.B/INFO.C/INFO.P) and `md` (for the markdown step). `node` is REQUIRED: a
|
||||
// skipped guard is not a guard.
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// One composite ("sub", holding a primitive "double") + one root-level
|
||||
// primitive ("gauge"), wired as: root.nodes = { "0": <sub composite ref>, "1":
|
||||
// <gauge primitive> }. Small enough that every INFO entry below is traceable by
|
||||
// hand; big enough to cover collapsed-composite, expanded-composite, and
|
||||
// primitive INFO shapes in one fixture.
|
||||
const model = {
|
||||
root: {
|
||||
nodes: {
|
||||
"0": { comp: "sub" },
|
||||
"1": { prim: {
|
||||
name: "g", type: "Mul2", role: "node",
|
||||
params: [["k", "f64"]],
|
||||
bound: [],
|
||||
ins: [["f64", "any", "a"]],
|
||||
outs: [["y", "f64"]],
|
||||
} },
|
||||
},
|
||||
edges: [],
|
||||
},
|
||||
composites: {
|
||||
sub: {
|
||||
inputs: [["f64", "any", "x"]],
|
||||
outputs: [["y", "f64"]],
|
||||
nodes: {
|
||||
"0": { prim: {
|
||||
name: "double", type: "Mul2", role: "node",
|
||||
params: [], bound: [],
|
||||
ins: [["f64", "any", "a"]],
|
||||
outs: [["y", "f64"]],
|
||||
} },
|
||||
},
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// COMP (used by genDot to resolve a composite reference by name) is captured at
|
||||
// require time from window.AURA_MODEL — mirrors the browser bootstrap and the
|
||||
// sibling viewer_nested_depth.mjs guard.
|
||||
global.window = { AURA_MODEL: model };
|
||||
const { normalizeModel, genDot, md } = require(join(here, "..", "assets", "graph-viewer.js"));
|
||||
const ROOT = normalizeModel(model).root;
|
||||
|
||||
const fail = (msg) => { console.error(msg); process.exit(1); };
|
||||
const expect = (actual, expected, msg) => {
|
||||
if (actual !== expected) {
|
||||
fail(`${msg}\n--- expected ---\n${JSON.stringify(expected)}\n--- actual ---\n${JSON.stringify(actual)}`);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Property 1: collapsed composite — INFO.B carries type/role + params line.
|
||||
{
|
||||
const { info } = genDot(ROOT, "root", new Set(), false);
|
||||
expect(
|
||||
info.B["n0"],
|
||||
"**sub** <span class='dim'>· composite</span>\nparams: <span class='dim'>none</span>",
|
||||
"collapsed composite INFO.B[n0] shape changed"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Property 2: expanded composite — INFO.C carries the cluster caption.
|
||||
{
|
||||
const { info } = genDot(ROOT, "root", new Set(["n0"]), false);
|
||||
expect(
|
||||
info.C["clust_n0"],
|
||||
"**sub** <span class='dim'>· composite · click frame to collapse</span>",
|
||||
"expanded composite INFO.C[clust_n0] shape changed"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Property 3: primitive node — INFO.B body line + INFO.P port entries.
|
||||
{
|
||||
const { info } = genDot(ROOT, "root", new Set(), false);
|
||||
expect(
|
||||
info.B["n1"],
|
||||
"**Mul2** <span class='dim'>· node</span>\nparams: `k:f64`",
|
||||
"primitive INFO.B[n1] shape changed"
|
||||
);
|
||||
expect(info.P["P_n1_i0"], "**a** · `f64`", "primitive input port INFO.P[P_n1_i0] shape changed");
|
||||
expect(info.P["P_n1_o0"], "**y** · `f64`", "primitive output port INFO.P[P_n1_o0] shape changed");
|
||||
}
|
||||
|
||||
// --- Property 4: md() — the markdown-to-HTML step the #tip div consumes.
|
||||
{
|
||||
expect(md("**x**\n`y`"), "<b>x</b><br><code>y</code>", "md() markdown conversion changed");
|
||||
// End-to-end: md() applied to the real INFO.B[n1] string from Property 3 —
|
||||
// bold the type, wrap the param signature in <code>, turn the newline into
|
||||
// <br>, and leave the hand-authored <span class='dim'> markup untouched
|
||||
// (md() only rewrites **, `` ` ``, and \n — it does not touch raw HTML).
|
||||
const { info } = genDot(ROOT, "root", new Set(), false);
|
||||
expect(
|
||||
md(info.B["n1"]),
|
||||
"<b>Mul2</b> <span class='dim'>· node</span><br>params: <code>k:f64</code>",
|
||||
"md() applied to a real INFO.B entry changed shape"
|
||||
);
|
||||
}
|
||||
|
||||
console.log("OK — INFO.B/INFO.C/INFO.P tooltip shapes and md() conversion pinned.");
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,39 @@
|
||||
//! Integration guard: the `aura graph` viewer's tooltip INFO maps (INFO.B for a
|
||||
//! node body, INFO.C for an expanded composite's cluster frame, INFO.P for a
|
||||
//! port) and the md() markdown-to-HTML step keep their current exact shape.
|
||||
//!
|
||||
//! The property lives in JavaScript (`addInfo`/`genDot`/`md` in
|
||||
//! assets/graph-viewer.js), so this shells out to `node` running the headless
|
||||
//! guard `tests/viewer_tooltip.mjs`, which loads the real viewer module and
|
||||
//! drives the exported `genDot`/`md` over a minimal collapsed+expanded
|
||||
//! composite / primitive fixture.
|
||||
//!
|
||||
//! `node` is REQUIRED: if it is not on PATH this test FAILS (a skipped guard is
|
||||
//! not a guard).
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn viewer_tooltip_info_and_markdown_shapes_are_pinned() {
|
||||
let script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("viewer_tooltip.mjs");
|
||||
assert!(script.exists(), "guard script missing at {}", script.display());
|
||||
|
||||
let out = match Command::new("node").arg(&script).output() {
|
||||
Ok(out) => out,
|
||||
Err(e) => panic!(
|
||||
"node is required for the viewer tooltip guard but could not be run \
|
||||
({e}); install Node.js or ensure `node` is on PATH"
|
||||
),
|
||||
};
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"viewer tooltip guard failed (exit {:?}).\n--- node stdout ---\n{stdout}\n--- node stderr ---\n{stderr}",
|
||||
out.status.code()
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user