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