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

nordisk/facett

facett-holger/tests/robot.rs

//! **Robot-UI scenario + matrix-fill proof for facett-holger.**
//!
//! Drives the registry browser headless through the shared `nornir-robotui`
//! harness: click a repository selector in the REAL AccessKit tree, assert the
//! observable `state_json` flips `selected_repo`, and emit one `robot-ui` matrix
//! row per step (round-tripped through a `JsonFileSink` — the wire-once proof the
//! component is verified in a host). Plus an imperative journey asserting the
//! message path and a red-cell fidelity check.

use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use nornir_robotui::{
    Action, Assert, JsonFileSink, Observable, RobotSession, Scenario, Step, TestSink, status,
};
use serde_json::{Value, json};

use facett_core::Facet;
use facett_holger::{Msg, RegistryBrowser};

/// Newtype so we can `impl Observable` for the browser (orphan rule).
struct App(RegistryBrowser);

impl Observable for App {
    fn state_json(&self) -> Value {
        self.0.state_json()
    }
}

fn robot() -> RobotSession<'static, App> {
    let harness = Harness::builder()
        .with_size(egui::vec2(1000.0, 900.0))
        .build_ui_state(|ui, app: &mut App| app.0.ui(ui), App(RegistryBrowser::from_fixture()));
    RobotSession::new(harness, "facett-holger")
}

/// **Imperative journey**: drive the message path directly + prove the surface
/// paints non-blank at each step.
#[test]
fn browser_selects_repos_and_paints() {
    let mut b = RegistryBrowser::from_fixture();
    assert_eq!(b.state_json()["selected_repo"], "crates-mirror");

    for repo in ["npm-proxy", "docker-hub", "helm-charts"] {
        assert!(b.apply(Msg::SelectRepo(repo.into())));
        assert_eq!(b.state_json()["selected_repo"], repo);

        // The surface paints non-blank with the repo selectors present.
        let mut probe = RegistryBrowser::from_fixture();
        probe.select_repo(repo);
        let mut h = Harness::builder()
            .with_size(egui::vec2(900.0, 760.0))
            .build_ui(move |ui| probe.ui(ui));
        h.run();
        let selector = format!("▸ {repo}");
        assert!(h.query_by_label(&selector).is_some(), "repo selector {selector} painted");
    }
}

/// **Scenario form — fills the `robot-ui` matrix by really clicking the UI.**
#[test]
fn scenario_browses_repos_and_fills_the_matrix() {
    let mut robot = robot();
    robot.run(2);

    let scenario = Scenario::new("robot-holger-browse")
        .step(
            Step::new("the browser opens on the rust mirror")
                .assert(Assert::State { pointer: "/selected_repo".into(), expected: json!("crates-mirror") }),
        )
        .step(
            Step::new("the roster lists all eight repositories")
                .assert(Assert::State { pointer: "/repo_count".into(), expected: json!(8) }),
        )
        .step(
            Step::new("click the npm-proxy repository")
                .action(Action::ClickLabel("▸ npm-proxy".into()))
                .assert(Assert::State { pointer: "/selected_repo".into(), expected: json!("npm-proxy") }),
        )
        .step(
            Step::new("click the helm-charts repository")
                .action(Action::ClickLabel("▸ helm-charts".into()))
                .assert(Assert::State { pointer: "/selected_repo".into(), expected: json!("helm-charts") }),
        )
        .step(
            Step::new("the artifact listing still carries its columns")
                .assert(Assert::StateContains { pointer: "/artifacts/columns".into(), needle: "version".into() }),
        )
        .step(Step::new("the browser drew widgets").assert(Assert::Drew));

    let rows = robot.run_scenario(&scenario);
    assert_eq!(rows.len(), 6, "one matrix row per scenario step");
    assert!(rows.iter().all(|r| r.aspect == "robot-ui"), "rows tagged robot-ui");
    assert!(rows.iter().all(|r| r.suite == "robot-holger-browse"), "suite carried through");
    for r in &rows {
        assert_eq!(r.status, status::PASS, "step {:?} RED: {}", r.test_name, r.message);
    }

    // Prove the rows fill a matrix sink (round-trip a JsonFileSink).
    let sink_path =
        std::env::temp_dir().join(format!("facett-holger-robot-matrix-{}.jsonl", std::process::id()));
    let _ = std::fs::remove_file(&sink_path);
    let sink = JsonFileSink::new(&sink_path);
    sink.append(&rows).expect("ship the robot rows to the matrix sink");
    let back = sink.read_all().expect("read the matrix back");
    assert_eq!(back.len(), 6, "the matrix sink has all 6 robot rows");
    let _ = std::fs::remove_file(&sink_path);
}

/// **Port fidelity** — a WRONG expectation lands as a RED cell, not a panic.
#[test]
fn wrong_expectation_is_a_red_cell_not_a_panic() {
    let mut robot = robot();
    robot.run(2);
    let scenario = Scenario::new("holger-fidelity")
        .step(
            Step::new("good: opens on crates-mirror")
                .assert(Assert::State { pointer: "/selected_repo".into(), expected: json!("crates-mirror") }),
        )
        .step(
            Step::new("bad: repo_count == 99 (wrong)")
                .assert(Assert::State { pointer: "/repo_count".into(), expected: json!(99) }),
        )
        .step(
            Step::new("bad: click a repository that does not exist")
                .action(Action::ClickLabel("▸ nonesuch-repo".into())),
        );
    let rows = robot.run_scenario(&scenario);
    assert_eq!(rows.len(), 3);
    assert_eq!(rows[0].status, status::PASS);
    assert_eq!(rows[1].status, status::FAIL, "a wrong expectation is a red cell");
    assert!(rows[1].message.contains("99"), "the red cell records why: {}", rows[1].message);
    assert_eq!(rows[2].status, status::FAIL, "a missing-widget action is a red cell");
}