tests/robot_full_nav.rs
//! **Robot-UI full navigation sweep** — click EVERY header tab through the real
//! AccessKit header buttons and assert `state_json["tab"]` becomes its debug id +
//! the pane drew. The robot-ui twin of the uiplane reachability walk, but through
//! the shipped header widgets. Plus the two sub-view panes (DepGraph/CallGraph)
//! via 🏛 Architecture, and the inject-assert negative (a ghost tab is a RED
//! action cell, never a panic).
//!
//! Run: `cargo test --features "viz" --test robot_full_nav`
#![cfg(feature = "viz")]
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use egui_kittest::Harness;
use nornir::viz::UrdrThreadsApp;
use nornir_robotui::egui;
use nornir_robotui::{Action, Assert, Observable, RobotSession, Scenario, Step, status};
use serde_json::json;
struct VizApp(UrdrThreadsApp);
impl Observable for VizApp {
fn state_json(&self) -> serde_json::Value {
self.0.state_json()
}
}
static SEQ: AtomicU64 = AtomicU64::new(0);
fn robot(tag: &str) -> RobotSession<'static, VizApp> {
unsafe { std::env::set_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG", "1") };
let n = SEQ.fetch_add(1, Ordering::Relaxed);
let root =
std::env::temp_dir().join(format!("nornir-fullnav-{tag}-{}-{n}", std::process::id()));
let _ = std::fs::create_dir_all(&root);
let app = UrdrThreadsApp::with_repos(
root,
"nornir".to_string(),
PathBuf::new(),
vec!["nornir".to_string()],
);
// Wide enough that all 15 second-row tabs (split=10 over 25 header tabs) fit
// on-screen so the LAST tab (🧠 Models) is hittable by AccessKit, not clipped.
let harness = Harness::builder()
.with_size(egui::Vec2::new(2400.0, 900.0))
.build_ui_state(|ui, app: &mut VizApp| app.0.draw_ui(ui), VizApp(app));
let mut r = RobotSession::new(harness, "nornir");
r.run(4);
r
}
fn assert_all_pass(rows: &[nornir_robotui::TestResultRow]) {
for r in rows {
assert_eq!(
r.status,
status::PASS,
"step {:?} failed: {}",
r.test_name,
r.message
);
}
}
/// The 25 clickable header tabs (DepGraph + CallGraph are NOT headers — they live
/// under 🏛 Architecture's sub-views, driven separately below).
const HEADER_TABS: &[(&str, &str)] = &[
("🧬 nornir", "Nornir"),
("🧵 Timeline", "Timeline"),
("🏛 Architecture", "Architecture"),
("🚇 Metro", "Metro"),
("🧬 Time Helix", "Helix"),
("🩸 Bloodstream", "Bloodstream"),
("🗂 Funnel", "Funnel"),
("⏳ Time Travel", "TimeTravel"),
("📡 Live Run", "LiveRun"),
("🚀 Release", "Release"),
("🗺 Knowledge", "Knowledge"),
("🗄 Warehouse", "Warehouse"),
("🏢 Wh Deck", "WarehouseDeck"),
("📞 MCP", "Mcp"),
("🔍 Search", "Search"),
("🚦 Gates", "Gates"),
("🛡 Security", "Security"),
("⚒ Dwarves", "Dwarves"),
("📖 Manual", "Manual"),
("🤖 Claude", "Claude"),
("🧠 Models", "Models"),
];
#[test]
fn robot_navigates_every_header_tab() {
let mut robot = robot("nav");
let mut scenario = Scenario::new("nav-all-tabs");
for (label, id) in HEADER_TABS {
scenario = scenario
.step(
Step::new(format!("click {label} → tab == {id}"))
.action(Action::ClickLabel((*label).to_string()))
.assert(Assert::State {
pointer: "/tab".into(),
expected: json!(id),
}),
)
.step(Step::new(format!("{id} pane drew")).assert(Assert::Drew));
}
let rows = robot.run_scenario(&scenario);
assert_eq!(rows.len(), HEADER_TABS.len() * 2, "two rows per tab");
assert_all_pass(&rows);
}
/// DepGraph + CallGraph are reached through 🏛 Architecture's sub-view selector.
#[test]
fn nav_reaches_depgraph_callgraph_via_architecture() {
let mut robot = robot("arch");
robot
.click_label("🏛 Architecture")
.expect("Architecture header");
robot.run(2);
let scenario = Scenario::new("nav-arch-subviews")
.step(
Step::new("select 🔗 Dep Graph sub-view → sub_view == Deps")
.action(Action::ClickLabel("🔗 Dep Graph".into()))
.assert(Assert::State {
pointer: "/architecture/sub_view".into(),
expected: json!("Deps"),
}),
)
.step(
Step::new("select 🕸 Call Graph sub-view → sub_view == CallGraph")
.action(Action::ClickLabel("🕸 Call Graph".into()))
.assert(Assert::State {
pointer: "/architecture/sub_view".into(),
expected: json!("CallGraph"),
}),
)
.step(Step::new("arch pane drew").assert(Assert::Drew));
assert_all_pass(&robot.run_scenario(&scenario));
}
/// NEGATIVE (inject-assert LAW): a ghost header label is a RED action row, not a
/// panic; a wrong /tab expectation is a RED assertion row.
#[test]
fn nav_negatives_land_as_red_cells_not_panics() {
let mut robot = robot("neg");
let scenario = Scenario::new("nav-neg")
.step(
Step::new("click a ghost tab → RED action (no such widget)")
.action(Action::ClickLabel("🛸 Nonexistent".into())),
)
.step(
Step::new("wrong tab expectation → RED assertion")
.action(Action::ClickLabel("🚀 Release".into()))
.assert(Assert::State {
pointer: "/tab".into(),
expected: json!("Funnel"),
}),
);
let rows = robot.run_scenario(&scenario);
assert_eq!(
rows[0].status,
status::FAIL,
"ghost click is a red action cell"
);
assert_eq!(
rows[1].status,
status::FAIL,
"wrong expectation is a red assertion cell"
);
}