dwarves-gui/tests/robot_five_leaf_body.rs
//! **robot_five_leaf_body** — the ⚒ Build Thing tab's body is a DECLARED tree of
//! leaves, rendered by the SHIPPED shell; the operator can DRAG every divider inside
//! it, FOLD every leaf, and MAXIMISE the graph.
//!
//! The name is from the five-leaf days and is kept so the lane that named it still
//! finds it. There are THREE leaves now (2026-08-26, the owner: "byt"): the settings
//! renderer is a GRAPH, so recipes · matrix · settings · kernel · console collapsed
//! to **graph | menu | console** — the kernel a node of the graph (gap #6), the matrix
//! the graph's variants, the settings the selected node's menu. `tests/robot_kernel_in_build.rs`
//! drives the four recipe shapes of the graph; this file drives the leaves, their two
//! dividers, the folds, and the maximise.
//!
//! ```text
//! build-thing ── Split(H) graph | side
//! └─ side ── Split(V) menu / console
//! ```
//!
//! | claim | RED when |
//! |---|---|
//! | the SHIPPED shell binds all three declared leaves as foldable, chromed, distinctly named panes, and renders no kernel / matrix / settings leaf | a leaf is dropped, or a retired pane comes back |
//! | every divider inside the tab is a real handle a pointer drags, both children moving oppositely with their sum conserved | a split loses `resizable`, or a handle is chrome only |
//! | folding any leaf drops only its own slot | fold reaches the wrong pane |
//! | MAXIMISING the graph gives it the whole tab body, its siblings zero extent by `maximize`, and restore returns the ratios | the graph cannot be enlarged, or restore forgets the drag |
//!
//! The canvas stays at 1400×900 for the reason the previous revision recorded: the tab
//! strip is not in the loop (active tab set through `NavState`), so this suite stays
//! independent of strip-width defects.
#![cfg(not(target_arch = "wasm32"))]
use dwarves_gui::build_thing_scene::{self, CONSOLE, GRAPH, LEAVES, MENU, TAB};
use dwarves_gui::tabbed::{dwarves_scene, TabbedView, BODY};
use dwarves_gui::{build_native_app, tab_keys, tab_labels};
use facett_app::scene::{self, Content, Layout, NavState, Pane, SceneHost};
use facett_core::Facet;
use serde_json::{json, Value};
use egui_kittest::Harness;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
/// The EXACT shipped native shell, offline-safe (dead endpoint, nonexistent ledger) —
/// the same builder `main` runs, so what is asserted is what ships.
fn offline_app() -> TabbedView {
build_native_app(
"http://127.0.0.1:0".into(),
PathBuf::from("/nonexistent/dwarves-gui/robot-five-leaf"),
)
}
/// A host that draws one queryable label per slot — mirrors `robot_scene.rs`'s
/// `TestHost`, so the render below exercises the SAME `dwarves_scene()` value the app
/// ships without dragging every real pane's I/O into a geometry test.
struct TestHost;
impl SceneHost for TestHost {
fn slot(&mut self, id: &str, ui: &mut egui::Ui) -> Value {
ui.label(format!("SLOT:{id}"));
json!({ "slot": id })
}
}
fn matrix(name: &str, ok: bool, desc: &str) {
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status("dwarves-gui", name, ok, desc);
let _ = (name, ok, desc);
}
/// A rendered pane's allotted rect `(x, y, w, h)` — the apportionment its parent handed
/// it this frame, which is the geometry oracle (`rect` can lag a frame).
fn alloc(state: &Value, pane: &str) -> (f32, f32, f32, f32) {
let r = &state["panes"][pane]["alloc"];
let f = |i: usize| {
r.get(i)
.and_then(Value::as_f64)
.unwrap_or_else(|| panic!("pane `{pane}` reports an alloc rect: {}", state["panes"]))
as f32
};
(f(0), f(1), f(2), f(3))
}
fn extent(state: &Value, pane: &str, along_x: bool) -> f32 {
let (_, _, w, h) = alloc(state, pane);
if along_x {
w
} else {
h
}
}
/// The centre of a split pane's first drag handle.
fn sep_center(state: &Value, pane: &str) -> egui::Pos2 {
let s = &state["panes"][pane]["separators"][0];
let f = |i: usize| {
s.get(i).and_then(Value::as_f64).unwrap_or_else(|| {
panic!(
"pane `{pane}` exposes a drag handle: {}",
state["panes"][pane]
)
}) as f32
};
egui::pos2(f(0) + f(2) / 2.0, f(1) + f(3) / 2.0)
}
/// Every SPLIT in the declared ⚒ body, as `(name, [child names])`, in tree order.
fn declared_splits() -> Vec<(String, Vec<String>)> {
fn walk(p: &Pane, out: &mut Vec<(String, Vec<String>)>) {
if let Content::Children(kids) = &p.content {
if matches!(p.layout, Layout::Split { .. }) {
out.push((
p.name.clone(),
kids.iter().map(|k| k.name.clone()).collect(),
));
}
for k in kids {
walk(k, out);
}
}
}
let mut out = Vec::new();
walk(&build_thing_scene::body(), &mut out);
out
}
/// Render the shipped scene with the ⚒ tab active — the ONE shape the tab has.
fn render_hammer() -> (Harness<'static>, Rc<RefCell<NavState>>, Rc<RefCell<Value>>) {
let scene = dwarves_scene(&tab_labels());
let idx = tab_keys()
.iter()
.position(|k| k == TAB)
.expect("⚒ is a shipped tab");
let nav = Rc::new(RefCell::new(NavState::new(&scene)));
nav.borrow_mut().set_active_tab(BODY, idx);
let state = Rc::new(RefCell::new(Value::Null));
let (s, n, sc) = (state.clone(), nav.clone(), scene.clone());
let mut h = Harness::builder()
.with_size(egui::vec2(1400.0, 900.0))
.build_ui(move |ui| {
*s.borrow_mut() = scene::render(&sc, &mut n.borrow_mut(), ui, &mut TestHost);
});
h.run();
(h, nav, state)
}
// ── 0. no retired leaf — the kernel, the matrix and the settings are the graph and the menu ──
/// ROBOT SCENARIO — **the shipped shell renders NO kernel, matrix or settings leaf in
/// ⚒, in any state.** Three leaves, nothing marked absent, all three slots filled.
///
/// RED WHEN REVERTED: declare a retired leaf again in `build_thing_scene::body` and
/// the roster names it.
#[test]
fn robot_the_shipped_shell_has_only_the_three_leaves_in_the_hammer_tab() {
let app = offline_app();
let st = Facet::state_json(&app);
let panes: Vec<String> = st["panes"]
.as_array()
.expect("the shell reports its pane roster")
.iter()
.map(|v| v.as_str().unwrap_or_default().to_string())
.collect();
for retired in [
"build-thing-kernel",
"build-thing-matrix",
"build-thing-settings",
"build-thing-recipes",
] {
assert!(
!panes.iter().any(|p| p == retired),
"a retired LEAF `{retired}` is bound in the shipped shell: {panes:?}"
);
}
let absent: Vec<String> = st["absent"]
.as_array()
.map(|a| {
a.iter()
.map(|v| v.as_str().unwrap_or_default().to_string())
.collect()
})
.unwrap_or_default();
assert!(
absent.is_empty(),
"nothing in the shipped shell is gated by runtime absence any more: {absent:?}"
);
let (_h, _nav, state) = render_hammer();
let render = state.borrow();
for leaf in LEAVES {
assert!(
render["slots"].get(leaf).is_some(),
"`{leaf}` fills its slot: {}",
render["slots"]
);
}
matrix(
"hammer_three_leaves_no_retired_pane",
true,
"the SHIPPED dwarves shell (build_native_app, offline-safe) binds three ⚒ leaves (graph, menu, console) and no kernel/matrix/settings/recipes pane, marks nothing absent, and a 1400x900 render fills all three slots — the kernel is a node of the build graph, the matrix its variants, the settings the selected node's menu",
);
}
// ── 1. every declared leaf is addressable ─────────────────────────────────────
/// ROBOT SCENARIO — **all three ⚒ leaves are DECLARED, foldable, chromed panes of the
/// shipped shell with distinct names.**
#[test]
fn robot_all_declared_leaves_are_addressable_and_distinct() {
let app = offline_app();
let st = Facet::state_json(&app);
let panes: Vec<String> = st["panes"]
.as_array()
.expect("the shell reports its pane roster")
.iter()
.map(|v| v.as_str().unwrap_or_default().to_string())
.collect();
let missing: Vec<&str> = LEAVES
.iter()
.copied()
.filter(|l| !panes.iter().any(|p| p == l))
.collect();
assert!(missing.is_empty(), "the ⚒ body DECLARES three leaves and the shipped shell must bind every one; missing {missing:?} of {panes:?}");
let dressed = LEAVES.iter().all(|leaf| {
st["foldable"].as_array().unwrap().iter().any(|n| n == leaf)
&& st["chrome"].as_array().unwrap().iter().any(|n| n == leaf)
&& st["folded"].get(*leaf).is_some()
});
assert!(
dressed,
"every declared ⚒ leaf is a foldable, chromed, addressable scene pane: {st}"
);
let mut sorted = panes.clone();
sorted.sort();
sorted.dedup();
assert_eq!(
sorted.len(),
panes.len(),
"duplicate pane name in the shipped shell: {panes:?}"
);
matrix(
"hammer_three_leaves_addressable",
true,
&format!("the ⚒ tab declares THREE named leaves ({}) — each a foldable, chromed, distinctly named scene pane of the shipped shell", LEAVES.join(", ")),
);
}
// ── 2. every divider really drags ─────────────────────────────────────────────
/// ROBOT SCENARIO — **every divider inside the ⚒ tab is a real draggable handle.**
#[test]
fn robot_every_divider_inside_the_hammer_tab_really_drags() {
let splits = declared_splits();
assert_eq!(
splits.len(),
2,
"the ⚒ body declares two dividers (graph|side, menu|console): {splits:?}"
);
let st = Facet::state_json(&offline_app());
let draggable: Vec<String> = st["draggable_splits"]
.as_array()
.expect("the shell reports its draggable splits")
.iter()
.map(|v| v.as_str().unwrap_or_default().to_string())
.collect();
for (name, _) in &splits {
assert!(
draggable.contains(name),
"`{name}` is a DRAGGABLE divider of the shipped shell: {draggable:?}"
);
}
let mut dragged = 0usize;
for (name, kids) in &splits {
assert_eq!(
kids.len(),
2,
"`{name}` divides exactly two children: {kids:?}"
);
let (mut h, _nav, state) = render_hammer();
let (sw, sh) = {
let s = &state.borrow()["panes"][name.as_str()]["separators"][0];
(
s[2].as_f64().expect("a handle rect") as f32,
s[3].as_f64().unwrap() as f32,
)
};
let along_x = sw < sh;
let d = 90.0_f32;
let (a, b) = (&kids[0], &kids[1]);
let (a0, b0) = (
extent(&state.borrow(), a, along_x),
extent(&state.borrow(), b, along_x),
);
let span0 = a0 + b0;
assert!(
a0 > 20.0 && b0 > 20.0,
"`{name}` has two LIVE children: {a}={a0} {b}={b0}"
);
let from = sep_center(&state.borrow(), name);
let to = if along_x {
egui::pos2(from.x + d, from.y)
} else {
egui::pos2(from.x, from.y + d)
};
h.event(egui::Event::PointerMoved(from));
h.drag_at(from);
h.event(egui::Event::PointerMoved(to));
h.run();
h.drop_at(to);
h.run();
let (a1, b1) = (
extent(&state.borrow(), a, along_x),
extent(&state.borrow(), b, along_x),
);
assert!(
(a1 - a0 - d).abs() < 26.0,
"dragging `{name}`'s divider {d}pt grew `{a}` by ~{d}: {a0} -> {a1}"
);
assert!(
(b0 - b1 - d).abs() < 26.0,
"…and shrank `{b}` by the same ~{d}: {b0} -> {b1}"
);
assert!(
((a1 + b1) - span0).abs() < 8.0,
"`{name}` conserves its extent: {span0} -> {}",
a1 + b1
);
dragged += 1;
}
assert_eq!(dragged, 2, "both ⚒ dividers were driven");
matrix(
"hammer_dividers_drag",
true,
"every divider inside the ⚒ tab is a real handle a pointer drags: both grow one child ~90pt, shrink its sibling the same, and conserve the sum",
);
}
// ── 3. the leaves fold independently, and the roster is data ──────────────────
/// ROBOT SCENARIO — **folding one ⚒ leaf drops ONLY its own slot**; the shell reports
/// the declared leaf roster as data so a driver never has to hard-code it.
#[test]
fn robot_hammer_leaves_fold_independently_and_the_roster_is_data() {
let st = Facet::state_json(&offline_app());
let reported: Vec<String> = st["body_leaves"][TAB]
.as_array()
.unwrap_or_else(|| panic!("the shell reports the ⚒ leaf roster as data: {st}"))
.iter()
.map(|v| v.as_str().unwrap_or_default().to_string())
.collect();
assert_eq!(
reported,
LEAVES.to_vec(),
"the shell's reported ⚒ leaf roster is the declared one, in reading order"
);
let n = |k: &str| st[k].as_array().map(Vec::len).unwrap_or(0);
assert_eq!(
n("split_bodies") + n("leaf_bodies") + n("single_pane_bodies"),
dwarves_gui::DWARVES_TABS.len(),
"every shipped tab is decomposed, leaved, or honestly single-pane: {st}"
);
let (mut h, nav, state) = render_hammer();
for leaf in LEAVES {
nav.borrow_mut().set_fold(leaf, true);
h.run();
let gone = state.borrow()["slots"].get(leaf).is_none();
let others = LEAVES
.iter()
.filter(|l| **l != leaf)
.all(|s| state.borrow()["slots"].get(*s).is_some());
assert!(
gone && others,
"folding `{leaf}` drops ONLY its own slot — its siblings keep drawing: {}",
state.borrow()["slots"]
);
nav.borrow_mut().set_fold(leaf, false);
h.run();
}
matrix(
"hammer_leaves_fold_independently",
true,
&format!("the fold vocabulary reaches inside the ⚒ tab: folding any of {GRAPH}/{MENU}/{CONSOLE} drops exactly its own slot and leaves its siblings drawing, and the shell publishes the declared roster under body_leaves"),
);
}
// ── 4. the graph is ENLARGEABLE: maximise / restore ───────────────────────────
/// ROBOT SCENARIO — **maximising the graph leaf gives it the whole tab body; its
/// siblings are allotted zero extent BY `maximize`; restore returns the ratios —
/// including a divider the operator had dragged.**
///
/// The owner, 2026-08-26: the graph pane MUST be enlargeable, and the kconfig subgraph
/// must be readable at full pane size. The mechanism is facett-scene's own
/// (`NavState::set_maximized` / `toggle_maximize`, `hidden_by: "maximize"`), driven
/// headless the way folds are.
///
/// RED WHEN REVERTED: make the graph leaf a fixed-size child, or let `toggle_maximize`
/// forget the ratios, and the alloc / restore assertions below fail.
#[test]
fn robot_the_graph_leaf_maximises_to_the_tab_body_and_restores_the_ratios() {
let (mut h, nav, state) = render_hammer();
// Drag the graph|side divider first, so restore has a NON-default ratio to return to.
let (from_w, from) = {
let s = state.borrow();
(extent(&s, GRAPH, true), sep_center(&s, TAB))
};
let to = egui::pos2(from.x + 120.0, from.y);
h.event(egui::Event::PointerMoved(from));
h.drag_at(from);
h.event(egui::Event::PointerMoved(to));
h.run();
h.drop_at(to);
h.run();
let dragged_w = extent(&state.borrow(), GRAPH, true);
assert!(
(dragged_w - from_w - 120.0).abs() < 26.0,
"the drag took: {from_w} -> {dragged_w}"
);
// MAXIMISE the graph.
nav.borrow_mut().set_maximized(GRAPH);
h.run();
h.run();
{
let s = state.borrow();
let (bx, by, bw, bh) = alloc(&s, TAB);
let (gx, gy, gw, gh) = alloc(&s, GRAPH);
assert!(
(gw - bw).abs() < 2.0
&& (gh - bh).abs() < 40.0
&& (gx - bx).abs() < 2.0
&& gy >= by - 1.0,
"the maximised graph is allotted the tab body: body {:?} graph {:?}",
(bx, by, bw, bh),
(gx, gy, gw, gh)
);
// The siblings live under the `side` split; the scene records the hidden pane
// at the level the maximise cut — the split itself — and its children draw
// nothing (no slot filled, or zero extent).
let side = format!("{TAB}-side");
assert_eq!(
s["panes"][&side]["hidden_by"],
json!("maximize"),
"`{side}` is hidden BY the maximise: {}",
s["panes"][&side]
);
for sib in [MENU, CONSOLE] {
let hidden = s["panes"][sib]["hidden_by"] == json!("maximize");
let w = s["panes"][sib]["alloc"][2].as_f64().unwrap_or(0.0);
let hh = s["panes"][sib]["alloc"][3].as_f64().unwrap_or(0.0);
assert!(
hidden || w == 0.0 || hh == 0.0 || s["slots"].get(sib).is_none(),
"`{sib}` is allotted nothing while the graph is maximised: {}",
s["panes"][sib]
);
}
assert!(
s["slots"].get(GRAPH).is_some(),
"the maximised graph still fills its slot"
);
}
// RESTORE — the dragged ratio comes back, not the default.
nav.borrow_mut().toggle_maximize(GRAPH);
h.run();
h.run();
{
let s = state.borrow();
let w = extent(&s, GRAPH, true);
assert!(
(w - dragged_w).abs() < 4.0,
"restore returns the DRAGGED ratio: {dragged_w} -> {w}"
);
for sib in [MENU, CONSOLE] {
assert!(
extent(&s, sib, false) > 20.0,
"`{sib}` is back after restore"
);
}
}
matrix(
"hammer_graph_maximises_and_restores",
true,
"the ⚒ graph leaf maximises through facett-scene's NavState (alloc == the tab body, menu and console hidden_by=maximize at zero extent) and restore returns the graph|side ratio the operator had dragged to, not the default",
);
}