facett-menu/tests/robot.rs
//! **MENU ROBOT** — the command palette driven end-to-end with **no egui and no
//! GPU**, over the same seams a headless host uses: open the palette, type a
//! filter, read `state_json` to prove the fuzzy list narrowed, select the top
//! hit, and assert the `Effect::Selected` the host would run is emitted.
//!
//! This is the inject-and-assert LAW, not no-crash: every step feeds a real input
//! and asserts a real observable (the hit list, the selection, the effect). Each
//! stage emits ONE `functional_status` row (`facett-menu`) so the palette's whole
//! open→filter→pick loop is provably covered in the nornir warehouse.
//!
//! Run: `cargo test -p facett-menu --features testmatrix --test robot`
use facett_core::{Facet, harness};
use facett_menu::{Command, Effect, MenuView, Msg};
/// Gated one-line emit shim (real row emitter with `--features testmatrix`,
/// compiled-out no-op otherwise — same shape as the crate's own wrapper).
#[cfg(feature = "testmatrix")]
fn emit(component: &str, check: &str, ok: bool, detail: &str) {
nornir_testmatrix::functional_status(component, check, ok, detail);
}
#[cfg(not(feature = "testmatrix"))]
fn emit(_component: &str, _check: &str, _ok: bool, _detail: &str) {}
fn commands() -> Vec<Command> {
vec![
Command::new("copy", "Copy", "Edit").shortcut("Ctrl+C"),
Command::new("cut", "Cut", "Edit").enabled(false),
Command::new("paste", "Paste", "Edit"),
Command::new("case.new", "New case", "Case").keywords(["create", "investigation"]),
Command::new("case.open", "Open case", "Case"),
Command::new("view.zoom", "Zoom in", "View"),
]
}
#[test]
fn palette_open_filter_select_emits_effect() {
let mut m = MenuView::new("Commands", commands());
// ── 1. open the palette ───────────────────────────────────────────────────
harness::drive(&mut m, [Msg::OpenPalette]);
let opened = m.is_palette_open();
assert!(opened, "palette must be open after OpenPalette");
emit("facett-menu", "robot_open", opened, "OpenPalette → palette_open=true");
// ── 2. type a filter ("case") and read state_json ─────────────────────────
harness::drive(&mut m, [Msg::SetQuery("case".into())]);
let j = Facet::state_json(&m);
let hits: Vec<String> =
j["hits"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_string()).collect();
// The fuzzy filter narrowed to exactly the two Case commands, Case-first.
let filtered_ok = !hits.is_empty()
&& hits.iter().all(|id| id.starts_with("case."))
&& !hits.contains(&"cut".to_string()); // disabled command never surfaces
assert!(filtered_ok, "state_json hits should be case.* only, got {hits:?}");
assert_eq!(j["query"], "case");
emit("facett-menu", "robot_filter", filtered_ok, &format!("query=case hits={hits:?}"));
// ── 2b. NON-HEADLESS render lane: the robot must also prove the pane PAINTS,
// not merely that its state moved. Drive the real egui painter (no OS window)
// over the open, filtered palette and assert it tessellated real geometry —
// the same `drew()` vertex proof every other facett pane's robot-UI carries.
let report = harness::render_sized(&mut m, (640.0, 480.0));
let drew = report.drew();
assert!(drew, "the open command palette must paint real geometry, got {} verts", report.vertices);
emit("facett-menu", "robot_render", drew, &format!("open palette drew {} verts", report.vertices));
// ── 3. select the highlighted (top) hit → an Effect is emitted ────────────
let top = hits[0].clone();
let effects = m.update(Msg::SelectHighlighted);
let effect_ok = effects == vec![Effect::Selected(top.clone())];
assert!(effect_ok, "SelectHighlighted must emit Selected({top}), got {effects:?}");
// selecting records the pick and closes the palette
assert_eq!(m.last_selected(), Some(top.as_str()));
assert!(!m.is_palette_open(), "selecting closes the palette");
emit("facett-menu", "robot_select", effect_ok, &format!("selected={top} closed={}", !m.is_palette_open()));
// ── 4. the same loop through the pure JSON Facet seam ─────────────────────
let mut m2 = MenuView::new("Commands", commands());
Facet::update_json(&mut m2, "\"open_palette\"");
Facet::update_json(&mut m2, "{\"set_query\":\"paste\"}");
let hits2: Vec<String> = Facet::state_json(&m2)["hits"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
Facet::update_json(&mut m2, "{\"select\":\"paste\"}");
let json_ok = hits2 == vec!["paste".to_string()] && m2.last_selected() == Some("paste");
assert!(json_ok, "JSON seam should filter+select paste, hits={hits2:?} sel={:?}", m2.last_selected());
emit("facett-menu", "robot_json_seam", json_ok, &format!("hits={hits2:?} selected=paste"));
emit(
"facett-menu",
"robot_summary",
opened && filtered_ok && drew && effect_ok && json_ok,
"open→filter→render→select→effect + json-seam all green",
);
}