facett-graph/tests/robot_node_addressing.rs
//! **The headless within-pane robot-drive proof for `facett-graph::GraphView`.**
//!
//! Before this, the 2D node/edge skin painted its nodes as bare
//! `painter.circle_filled` dots — no AccessKit node, so a headless driver could
//! select the graph *pane* but could not reach a *specific node* inside it (the
//! recorded `probe_only` gap for 17/18 tabs). Now each node rides into the
//! AccessKit tree as a clickable `Button` keyed on its **stable label**.
//!
//! This drives a real `GraphView` through the SAME `Locate` → `Act(Click)` path a
//! `cmd_json` driver uses — `RobotSession::find_id` is exactly `CmdHost::locate`,
//! and `RobotSession::click_by_id` is exactly what `CmdHost::act(Action::ClickById)`
//! runs — then asserts over `state_json` that clicking node **"B"** selects **B**,
//! not A, not "a click happened". Red-when-broken: clicking id "B" must select B
//! (not A); a non-existent node must fail to `Locate` (`find_id` → `None`).
use egui_kittest::Harness;
use nornir_robotui::{Observable, RobotSession, Target};
use serde_json::json;
use facett_core::{hash_color, Facet, Scene};
use facett_graph::GraphView;
/// Newtype so we can `impl Observable` for the view (orphan rule) — the host app
/// the robot session drives.
struct App(GraphView);
impl Observable for App {
fn state_json(&self) -> serde_json::Value {
self.0.state_json()
}
}
/// A three-node triangle with DISTINCT labels A/B/C — so each node is uniquely
/// addressable by its stable label (the common system-map / coderoom / graph case).
fn triangle() -> Scene {
let mut s = Scene::new();
let a = s.node("A", hash_color("A"));
let b = s.node("B", hash_color("B"));
let c = s.node("C", hash_color("C"));
s.edge(a, b);
s.edge(b, c);
s.edge(c, a);
s
}
fn robot() -> RobotSession<'static, App> {
let harness = Harness::builder()
.with_size(egui::vec2(700.0, 700.0))
.build_ui_state(|ui, app: &mut App| app.0.ui(ui), App(GraphView::new(triangle())));
RobotSession::new(harness, "facett-graph-nodes")
}
/// **Locate a specific node by its stable label, click it, and prove the RIGHT node
/// (B, not A) became selected in `state_json`** — the within-pane drive unlock.
#[test]
fn robot_clicks_a_specific_graph_node_by_stable_id() {
let mut robot = robot();
// Settle a couple of frames so the layout + AccessKit tree exist before Locate.
robot.run(2);
// Nothing selected initially; the addressable targets are discoverable.
assert_eq!(robot.state()["selected"], json!(null), "starts unselected");
assert_eq!(
robot.state()["node_targets"],
json!(["A", "B", "C"]),
"the addressable node targets are discoverable in state_json"
);
// RED-WHEN-BROKEN #1: a NON-EXISTENT node must fail to Locate (None).
assert_eq!(robot.find_id(&Target::label("does-not-exist")), None, "a bogus node must not resolve");
// Locate { Target::label("B") } → a real AccessKit node id (the `CmdHost::locate`
// path), then Act(Click) it by that id (the `CmdHost::act(ClickById)` path).
let b_id = robot.find_id(&Target::label("B")).expect("node B located by its stable label");
robot.click_by_id(b_id).expect("click node B by id");
// RED-WHEN-BROKEN #2: the click selected B — the RIGHT node, not A, not "some".
assert_eq!(robot.state()["selected"], json!("B"), "clicking B selected B");
assert_ne!(robot.state()["selected"], json!("A"), "it did NOT select A");
// A DIFFERENT node → a DIFFERENT selection: Locate + click "A", selection flips.
let a_id = robot.find_id(&Target::label("A")).expect("node A located");
assert_ne!(a_id, b_id, "distinct nodes carry distinct stable AccessKit ids");
robot.click_by_id(a_id).expect("click node A by id");
assert_eq!(robot.state()["selected"], json!("A"), "clicking A moved the selection to A");
// Re-clicking A toggles the selection back off (the live click behaviour).
robot.click_by_id(a_id).expect("re-click node A by id");
assert_eq!(robot.state()["selected"], json!(null), "re-click toggled the selection off");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"GraphView",
"robot_locate_act_selects_specific_node",
robot.state()["selected"] == json!(null),
"Locate B → click → selected B (not A); Locate A → click → selected A; re-click cleared",
);
}
/// **The AccessKit-tree proof, straight through `egui_kittest`.** Each painted node
/// is a queryable `Button` findable by its stable label, and a real click selects
/// it by that label — the FC-4/FC-5 half the `find_id`/`click_by_id` path rides on.
#[test]
fn painted_nodes_are_queryable_buttons_and_click_selects_by_label() {
use egui_kittest::kittest::{NodeT, Queryable};
use std::cell::RefCell;
use std::rc::Rc;
let view = Rc::new(RefCell::new(GraphView::new(triangle())));
let v2 = view.clone();
let mut harness =
Harness::builder().with_size(egui::vec2(700.0, 700.0)).build_ui(move |ui| v2.borrow_mut().ui(ui));
harness.run();
// Each node rode into the AccessKit tree as a Button findable by its label.
let b = harness.get_by_label("B").accesskit_node();
assert_eq!(b.role(), egui::accesskit::Role::Button, "painted node B is a queryable Button (FC-4)");
// A real click selects B, keyed on the stable label (FC-5).
harness.get_by_label("B").click();
harness.run();
assert_eq!(view.borrow().selected(), Some("B"), "click selects by stable label");
assert_eq!(view.borrow().state_json()["selected"], "B");
// Clicking a different node moves the selection; it never lands on the wrong one.
harness.get_by_label("C").click();
harness.run();
assert_eq!(view.borrow().selected(), Some("C"), "a different node → a different selection");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"GraphView",
"fc4_fc5_node_addressable_by_stable_label",
view.borrow().selected() == Some("C"),
"graph nodes are AccessKit Buttons; click selects the addressed node by label",
);
}