facett-buildthing/tests/robot_deck.rs
//! **RED-when-broken robot proof for the build-thing DECK** (F2).
//!
//! The `BuildThingDeck` / `BuildGraphPane` / `view_pane` surface had no real robot
//! scenario — every test drove it through `update_json` / `pending_select_for_test`.
//! This renders the panes headless through a REAL `egui_kittest` AccessKit harness
//! (CPU, no wgpu) and drives them as clicks:
//!
//! 1. **VARIANTS pane** — click a variant `selectable_label` and assert
//! `state_json["selected"]` flips; click **▶ Build** and assert the raised
//! [`Msg::Build`], then (as a host would) apply the result and assert
//! `state_json["result"]`.
//! 2. **GRAPH pane** — a REAL pointer click on a graph node routes through
//! `draw_graph → clicked_node → pending_select` (graph_pane.rs), the actual path,
//! NOT the `pending_select_for_test` stand-in, and selects the variant it carries.
use std::cell::RefCell;
use std::rc::Rc;
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
use facett_buildthing::{BuildResult, BuildThingDeck, Msg, PANE_GRAPH, PANE_VARIANTS};
use nornir_build_thing::plugin::{BuildGraph, BuildMenu, GraphNode, NodeKind};
/// A holger-shaped menu: a SERVER group (embedded / mini / full) + a CLIENT group
/// (ui / agent), `full` the declared normal case. This fixture was PROMOTED into
/// the crate as [`facett_buildthing::demo_menu`] (the facett-demo gallery mounts it
/// as a seeded tab); this delegates so there is exactly one writer (LAW 5).
fn menu() -> BuildMenu {
facett_buildthing::demo_menu()
}
/// (1) The VARIANTS pane: a real click on a variant label selects it; **▶ Build**
/// raises a Build msg the host acts on, and the result reaches `state_json`.
#[test]
fn robot_clicks_a_variant_and_build_in_the_variants_pane() {
let deck = Rc::new(RefCell::new(BuildThingDeck::new(menu())));
let msgs = Rc::new(RefCell::new(Vec::<Msg>::new()));
let d = deck.clone();
let m = msgs.clone();
let mut h = Harness::builder().with_size(egui::vec2(380.0, 560.0)).build_ui(move |ui| {
let out = d.borrow_mut().view_pane(PANE_VARIANTS, ui);
m.borrow_mut().extend(out);
});
h.run();
// Opens on the declared normal case.
assert_eq!(deck.borrow().state_json()["selected"], "full", "the deck opens on `full`");
// Click the `mini` variant's selectable_label (a real AccessKit node). At this
// point `mini` is unique — the bottom "selected" echo still reads `full`.
h.get_by_label("mini").click();
h.run();
assert_eq!(deck.borrow().state_json()["selected"], "mini", "the click selected `mini`");
assert!(
msgs.borrow().iter().any(|x| matches!(x, Msg::SelectVariant(v) if v == "mini")),
"the click raised a SelectVariant msg the host can act on"
);
// Click ▶ Build → a Build msg is raised for the selected variant.
msgs.borrow_mut().clear();
h.get_by_label("▶ Build").click();
h.run();
assert!(
msgs.borrow().iter().any(|x| matches!(x, Msg::Build(v) if v == "mini")),
"▶ Build raised a Build msg for the selected variant"
);
// As the host would: record the outcome. The RESULT pane then reports it.
deck.borrow_mut().result = BuildResult {
variant: "mini".into(),
green: true,
headline: "built mini".into(),
artifacts: vec![("target/release/mini".into(), "6 MB".into())],
errors: Vec::new(),
running: false,
};
let sj = deck.borrow().state_json();
assert_eq!(sj["result"]["variant"], "mini", "the build result surfaces in state_json");
assert_eq!(sj["result"]["green"], true);
assert_eq!(sj["result"]["artifact_count"], 1);
}
/// (2) The GRAPH pane: a REAL pointer click on a node routes through
/// `draw_graph → clicked_node → pending_select` (the actual path, not the test
/// stand-in) and selects the variant the node carries.
#[test]
fn robot_clicks_a_graph_node_to_select_a_variant() {
let deck = Rc::new(RefCell::new(BuildThingDeck::new(menu())));
// A single Variant node → column 0, row 0 → world position (0,0), so it projects
// to the graph painter's centre (`origin = rect.center() + pan`, pan = 0). It
// selects `agent`, distinct from the deck's opening selection (`full`), so the
// transition is unambiguous.
deck.borrow_mut().set_graph(BuildGraph {
repo: "holger".into(),
root: "agent".into(),
nodes: vec![GraphNode {
id: "variant:agent".into(),
label: "agent".into(),
kind: NodeKind::Variant,
detail: None,
selects: Some("agent".into()),
}],
edges: Vec::new(),
});
assert_eq!(deck.borrow().state_json()["selected"], "full", "opens on `full`");
let msgs = Rc::new(RefCell::new(Vec::<Msg>::new()));
let d = deck.clone();
let m = msgs.clone();
let size = egui::vec2(360.0, 420.0);
let mut h = Harness::builder().with_size(size).build_ui(move |ui| {
let out = d.borrow_mut().view_pane(PANE_GRAPH, ui);
m.borrow_mut().extend(out);
});
h.run();
// The graph painter fills the pane under a two-row header (filter tabs + legend);
// the lone node sits at its centre (≈ y 230 of the 420 px pane). Click there — a
// real move+press+release, the same events kittest's `Node::click` fires. The chip
// hit box is 184×34 world px, forgiving enough that the exact centre is not needed.
let pos = egui::pos2(size.x * 0.5, 230.0);
h.hover_at(pos);
h.drag_at(pos);
h.drop_at(pos);
h.run();
h.run();
// The real path: `draw_graph` returned `clicked_node`, `BuildGraphPane::ui` mapped
// it to `pending_select`, and `view_pane`'s `take_msgs` drained it as a
// `SelectVariant` for the host — NOT the `pending_select_for_test` stand-in.
assert!(
msgs.borrow().iter().any(|x| matches!(x, Msg::SelectVariant(v) if v == "agent")),
"a real click on the graph node raised SelectVariant('agent') through draw_graph→pending_select, got {:?}",
msgs.borrow()
);
// A host acts on that msg — and the selection flips, the round trip the graph
// exists for (pick a build by clicking its node).
assert!(deck.borrow_mut().select("agent"), "the selection was `full`, the click asked for `agent`");
assert_eq!(deck.borrow().state_json()["selected"], "agent", "the host applied the graph-node selection");
}