From c8b5acf6f06a32ec2c79a352afaff3a25461a5c9 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sat, 11 Jul 2026 18:30:58 +0200 Subject: [PATCH] =?UTF-8?q?test(cli):=20pin=20the=20viewer=20tooltip=20hal?= =?UTF-8?q?f=20=E2=80=94=20INFO=20maps=20and=20md()=20(refs=20#125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/aura-cli/assets/graph-viewer.js | 12 ++- crates/aura-cli/tests/viewer_tooltip.mjs | 121 +++++++++++++++++++++++ crates/aura-cli/tests/viewer_tooltip.rs | 39 ++++++++ 3 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 crates/aura-cli/tests/viewer_tooltip.mjs create mode 100644 crates/aura-cli/tests/viewer_tooltip.rs diff --git a/crates/aura-cli/assets/graph-viewer.js b/crates/aura-cli/assets/graph-viewer.js index a812f9d..7cbdce9 100644 --- a/crates/aura-cli/assets/graph-viewer.js +++ b/crates/aura-cli/assets/graph-viewer.js @@ -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, "$1").replace(/`(.+?)`/g, "$1").replace(/\n/g, "
"); // 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, "$1").replace(/`(.+?)`/g, "$1").replace(/\n/g, "
"); 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 }; } diff --git a/crates/aura-cli/tests/viewer_tooltip.mjs b/crates/aura-cli/tests/viewer_tooltip.mjs new file mode 100644 index 0000000..e924709 --- /dev/null +++ b/crates/aura-cli/tests/viewer_tooltip.mjs @@ -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": , "1": +// }. 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** · composite\nparams: none", + "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** · composite · click frame to collapse", + "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** · node\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`"), "x
y", "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 , turn the newline into + //
, and leave the hand-authored 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"]), + "Mul2 · node
params: k:f64", + "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); diff --git a/crates/aura-cli/tests/viewer_tooltip.rs b/crates/aura-cli/tests/viewer_tooltip.rs new file mode 100644 index 0000000..e31a59f --- /dev/null +++ b/crates/aura-cli/tests/viewer_tooltip.rs @@ -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() + ); +}