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

nordisk/facett

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