facett-app/tests/robot_chrome.rs
//! **The ROBOT-UI row for the scene's WINDOW CHROME** (L4 — every UI surface is
//! robot-tested). A real `nornir-robotui` [`RobotSession`] drives a real
//! [`facett_app::scene`] headlessly: it clicks the minimise / maximise / restore /
//! fold controls in each pane's title bar by their AccessKit labels, and every
//! `(action → expected)` step becomes one `robot-ui` [`TestResultRow`].
//!
//! ## The assertion is GEOMETRY, not a boolean
//! This crate already learnt the lesson that `assert!(flag)` proves nothing about a
//! UI. Every chrome step here asserts a **rect** out of the render's `state_json`:
//! `/panes/<name>/alloc` is the rect the parent allotted that pane, so
//!
//! - maximising `mid` must make `/panes/top/alloc/3` (its HEIGHT) exactly `0.0`,
//! - minimising `bot` must make its height exactly `CHROME_BAR`,
//! - restoring must put the *identical* number back.
//!
//! A chrome control that flips its flag without moving a pixel fails these rows.
//!
//! Run: `cargo test -p facett-app --test robot_chrome -- --nocapture`
use egui_kittest::Harness;
use facett_app::scene::{self, Axis, NavState, Pane, Scene, SceneHost};
use nornir_robotui::{Action, Assert, Observable, RobotSession, Scenario, Step};
use nornir_testmatrix::{JsonFileSink, TestResultRow, TestSink, render_matrix, status};
use serde_json::{Value, json};
/// The app under the robot: a scene, its runtime state, and the last render's
/// `state_json` — which IS the observable surface (`Observable::state_json`).
struct ChromeApp {
scene: Scene,
nav: NavState,
last: Value,
}
impl ChromeApp {
/// A three-way VERTICAL split, so each child's allotted extent is its HEIGHT and
/// "collapsed to just the title bar" is a number you can read.
fn new() -> Self {
let scene = Scene::new(
"chrome-robot",
Pane::split_n(
"root",
Axis::Vertical,
vec![0.5, 0.3, 0.2],
vec![
Pane::slot("top", "a").title("Top"),
Pane::slot("mid", "b").title("Mid").foldable(true),
Pane::slot("bot", "c").title("Bot"),
],
),
);
let nav = NavState::new(&scene);
Self { scene, nav, last: Value::Null }
}
fn ui(&mut self, ui: &mut egui::Ui) {
self.last = scene::render(&self.scene, &mut self.nav, ui, &mut SlotHost);
}
}
impl Observable for ChromeApp {
fn state_json(&self) -> Value {
self.last.clone()
}
}
/// Draws a labelled body per slot, so "the pane's contents left the tree" is checkable.
struct SlotHost;
impl SceneHost for SlotHost {
fn slot(&mut self, id: &str, ui: &mut egui::Ui) -> Value {
ui.label(format!("BODY:{id}"));
json!({ "slot": id })
}
}
/// `Assert::State` over a pane's allotted rect component (`2` = width, `3` = height).
fn alloc_is(pane: &str, idx: usize, expected: f64) -> Assert {
Assert::State { pointer: format!("/panes/{pane}/alloc/{idx}"), expected: json!(expected) }
}
#[test]
fn robot_drives_the_pane_window_chrome() {
let harness = Harness::builder()
.with_size(egui::vec2(1000.0, 700.0))
.build_ui_state(|ui, app: &mut ChromeApp| app.ui(ui), ChromeApp::new());
let mut robot = RobotSession::new(harness, "facett");
robot.run(2);
// ── The BASELINE geometry every later assertion is measured against. ──────────
let base = robot.state();
let h = |st: &Value, p: &str| st["panes"][p]["alloc"][3].as_f64().unwrap_or(f64::NAN);
let (h_top, h_mid, h_bot) = (h(&base, "top"), h(&base, "mid"), h(&base, "bot"));
assert!(
h_top > 100.0 && h_mid > 100.0 && h_bot > 100.0,
"baseline: all three panes have real height ({h_top} / {h_mid} / {h_bot})"
);
// ── The scenario: real clicks on the chrome controls, geometry read back. ─────
let scenario = Scenario::new("scene-window-chrome")
.step(
Step::new("every pane ships a chrome title bar by DEFAULT (no host opt-in)")
.action(Action::Step)
.assert(Assert::State { pointer: "/panes/top/chrome/spec/bar".into(), expected: json!(true) }),
)
.step(
Step::new("the minimise control is present and addressable")
.action(Action::None)
.assert(Assert::LabelVisible("minimize top".into())),
)
.step(
Step::new("click MAXIMIZE on `mid` → its sibling `top` is allotted ZERO height")
.action(Action::ClickLabel("maximize mid".into()))
.assert(alloc_is("top", 3, 0.0)),
)
.step(
Step::new("…and `bot` is zeroed too — a maximise hides EVERY sibling")
.action(Action::None)
.assert(alloc_is("bot", 3, 0.0)),
)
.step(
Step::new("…and the maximised pane took the WHOLE split extent (the root's own height)")
.action(Action::None)
.assert(Assert::State { pointer: "/panes/mid/alloc/3".into(), expected: json!(h(&base, "root")) }),
)
.step(
Step::new("…and the state names which control fired")
.action(Action::None)
.assert(Assert::State { pointer: "/nav/last_chrome".into(), expected: json!("maximize:mid") }),
)
.step(
Step::new("click RESTORE on `mid` → `top` gets its EXACT prior height back")
.action(Action::ClickLabel("restore mid".into()))
.assert(alloc_is("top", 3, h_top)),
)
.step(
Step::new("…and `bot` too — restore is exact, not approximate")
.action(Action::None)
.assert(alloc_is("bot", 3, h_bot)),
)
.step(
Step::new("click MINIMIZE on `bot` → it collapses to exactly the title bar's height")
.action(Action::ClickLabel("minimize bot".into()))
.assert(alloc_is("bot", 3, scene::CHROME_BAR as f64)),
)
.step(
Step::new("…and its BODY occupies exactly zero height — the content really left")
.action(Action::None)
.assert(Assert::State { pointer: "/panes/bot/content_height".into(), expected: json!(0.0) }),
)
.step(
Step::new("…and its siblings are still laid out (minimise ≠ maximise)")
.action(Action::None)
.assert(Assert::StateContains { pointer: "/panes/top/minimized".into(), needle: "false".into() }),
)
.step(
Step::new("click RESTORE on `bot` → its exact prior height returns")
.action(Action::ClickLabel("restore bot".into()))
.assert(alloc_is("bot", 3, h_bot)),
)
.step(
Step::new("click FOLD on `mid` → its body leaves the tree, its bar stays")
.action(Action::ClickLabel("fold mid".into()))
.assert(Assert::State { pointer: "/nav/folded/mid".into(), expected: json!(true) }),
)
.step(
Step::new("…the folded pane's BODY occupies zero height")
.action(Action::None)
.assert(Assert::State { pointer: "/panes/mid/content_height".into(), expected: json!(0.0) }),
)
.step(
// THE distinction between the two collapses, in one number: fold hides the
// body but KEEPS the pane's slot; minimise hands the slot back.
Step::new("…but a FOLD keeps the pane's slot (unlike a minimise)")
.action(Action::None)
.assert(Assert::State { pointer: "/panes/mid/alloc/3".into(), expected: json!(h_mid) }),
)
.step(
Step::new("click UNFOLD on `mid` → the body is back")
.action(Action::ClickLabel("unfold mid".into()))
.assert(Assert::State { pointer: "/slots/b/slot".into(), expected: json!("b") }),
)
.step(
Step::new("the draggable divider is still reachable after all of that")
.action(Action::None)
.assert(Assert::StateContains { pointer: "/panes/root/resizable".into(), needle: "true".into() }),
);
let rows = robot.run_scenario(&scenario);
// ── RED-when-broken, outside the scenario too: the pane really is back where it
// started, to the exact point. ────────────────────────────────────────────
let end = robot.state();
assert_eq!(
[h(&end, "top"), h(&end, "mid"), h(&end, "bot")],
[h_top, h_mid, h_bot],
"after maximise → restore → minimise → restore → fold → unfold, the layout is EXACTLY where it began"
);
// ── Persist the rows as real matrix cells and round-trip them back. ───────────
let target_dir = std::env::var_os("CARGO_TARGET_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("target"));
std::fs::create_dir_all(&target_dir).ok();
let out = target_dir.join("robot-matrix-chrome.jsonl");
std::fs::remove_file(&out).ok();
let sink = JsonFileSink::new(&out);
sink.append(&rows).expect("append robot-ui rows");
let read_back: Vec<TestResultRow> = sink.read_all().expect("read robot-ui rows back");
assert_eq!(read_back.len(), rows.len(), "every chrome row round-tripped through the sink");
assert!(read_back.iter().all(|r| r.aspect == "robot-ui"), "every row is a robot-ui aspect row");
let failed: Vec<&TestResultRow> = rows.iter().filter(|r| r.status != status::PASS).collect();
eprintln!("\n══ facett-app SCENE WINDOW-CHROME ROBOT DRIVE ══");
for r in &rows {
let mark = if r.status == status::PASS { "✓" } else { "✗" };
eprintln!(" {mark} [{}] {}{}", r.status, r.test_name, if r.message.is_empty() { String::new() } else { format!(" — {}", r.message) });
}
eprintln!("── {}/{} chrome steps PASSED ──", rows.len() - failed.len(), rows.len());
eprintln!("\n{}", render_matrix(&read_back));
eprintln!("rows → {}\n", out.display());
assert!(failed.is_empty(), "robot-ui chrome steps FAILED: {:#?}", failed.iter().map(|r| (&r.test_name, &r.message)).collect::<Vec<_>>());
}