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

nordisk/facett

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