Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

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"
    );
}