tests/robot_ops_journeys.rs
//! **Robot-UI ops journeys** — the interactive functions of the ops tabs that
//! aren't just navigation: the 🧬 nornir add-workspace form fields, the 📞 MCP
//! reload, and a nav+drew sweep of the remaining server-ops tabs (Search / Gates /
//! Bench / Warehouse / Release) whose forms need a live server.
//!
//! Run: `cargo test --features "viz" --test robot_ops_journeys`
#![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-ops-{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()],
);
let harness = Harness::builder()
.with_size(egui::Vec2::new(1600.0, 1000.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
);
}
}
/// 🧬 nornir root ops — nav + the root-ops surface drew (the add-workspace form
/// fields themselves render only in remote mode against a live server).
#[test]
fn robot_nornir_root_ops_drew() {
let mut robot = robot("nornir");
robot.click_label("🧬 nornir").expect("nornir header");
robot.run(2);
let scenario = Scenario::new("nornir-root-ops")
.step(Step::new("on Nornir").assert(Assert::State {
pointer: "/tab".into(),
expected: json!("Nornir"),
}))
.step(Step::new("root-ops pane drew").assert(Assert::Drew));
assert_all_pass(&robot.run_scenario(&scenario));
}
/// 📞 MCP — ⟳ Reload flips the loaded flag (reads the mcp_requests stats).
#[test]
fn robot_mcp_reload() {
let mut robot = robot("mcp");
robot.click_label("📞 MCP").expect("MCP header");
robot.run(2);
let scenario = Scenario::new("mcp-reload")
.step(Step::new("on Mcp").assert(Assert::State {
pointer: "/tab".into(),
expected: json!("Mcp"),
}))
.step(
Step::new("⟳ Reload the mcp_requests stats → loaded")
.action(Action::ClickLabel("⟳ Reload".into()))
.assert(Assert::State {
pointer: "/mcp/loaded".into(),
expected: json!(true),
}),
);
assert_all_pass(&robot.run_scenario(&scenario));
}
/// The server-backed ops tabs (forms need a live server) — nav + it drew, so each
/// surface still fills a robot-ui row from the real header button.
#[test]
fn robot_ops_tabs_nav_and_drew() {
let mut robot = robot("ops");
let scenario = Scenario::new("ops-nav")
.step(nav("🔍 Search", "Search"))
.step(Step::new("search drew").assert(Assert::Drew))
.step(nav("🚦 Gates", "Gates"))
.step(Step::new("gates drew").assert(Assert::Drew))
.step(nav("🗄 Warehouse", "Warehouse"))
.step(Step::new("warehouse drew").assert(Assert::Drew))
.step(nav("🚀 Release", "Release"))
.step(Step::new("release drew").assert(Assert::Drew));
assert_all_pass(&robot.run_scenario(&scenario));
}
fn nav(label: &str, id: &str) -> Step {
Step::new(format!("click {label} → {id}"))
.action(Action::ClickLabel(label.to_string()))
.assert(Assert::State {
pointer: "/tab".into(),
expected: json!(id),
})
}