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

nordisk/facett

facett-app/tests/robot_chrome.rs

//! **The ROBOT-UI row for the scene's WINDOW CHROME** (L4 — every UI surface is
//! robot-tested). A real `nornir-robotui` [`RobotSession`] drives a real
//! [`facett_app::scene`] headlessly: it clicks the minimise / maximise / restore /
//! fold controls in each pane's title bar by their AccessKit labels, and every
//! `(action → expected)` step becomes one `robot-ui` [`TestResultRow`].
//!
//! ## The assertion is GEOMETRY, not a boolean
//! This crate already learnt the lesson that `assert!(flag)` proves nothing about a
//! UI. Every chrome step here asserts a **rect** out of the render's `state_json`:
//! `/panes/<name>/alloc` is the rect the parent allotted that pane, so
//!
//! - maximising `mid` must make `/panes/top/alloc/3` (its HEIGHT) exactly `0.0`,
//! - minimising `bot` must make its height exactly `CHROME_BAR`,
//! - restoring must put the *identical* number back.
//!
//! A chrome control that flips its flag without moving a pixel fails these rows.
//!
//! Run: `cargo test -p facett-app --test robot_chrome -- --nocapture`

use egui_kittest::Harness;
use facett_app::scene::{self, Axis, NavState, Pane, Scene, SceneHost};
use nornir_robotui::{Action, Assert, Observable, RobotSession, Scenario, Step};
use nornir_testmatrix::{JsonFileSink, TestResultRow, TestSink, render_matrix, status};
use serde_json::{Value, json};

/// The app under the robot: a scene, its runtime state, and the last render's
/// `state_json` — which IS the observable surface (`Observable::state_json`).
struct ChromeApp {
    scene: Scene,
    nav: NavState,
    last: Value,
}

impl ChromeApp {
    /// A three-way VERTICAL split, so each child's allotted extent is its HEIGHT and
    /// "collapsed to just the title bar" is a number you can read.
    fn new() -> Self {
        let scene = Scene::new(
            "chrome-robot",
            Pane::split_n(
                "root",
                Axis::Vertical,
                vec![0.5, 0.3, 0.2],
                vec![
                    Pane::slot("top", "a").title("Top"),
                    Pane::slot("mid", "b").title("Mid").foldable(true),
                    Pane::slot("bot", "c").title("Bot"),
                ],
            ),
        );
        let nav = NavState::new(&scene);
        Self { scene, nav, last: Value::Null }
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        self.last = scene::render(&self.scene, &mut self.nav, ui, &mut SlotHost);
    }
}

impl Observable for ChromeApp {
    fn state_json(&self) -> Value {
        self.last.clone()
    }
}

/// Draws a labelled body per slot, so "the pane's contents left the tree" is checkable.
struct SlotHost;
impl SceneHost for SlotHost {
    fn slot(&mut self, id: &str, ui: &mut egui::Ui) -> Value {
        ui.label(format!("BODY:{id}"));
        json!({ "slot": id })
    }
}

/// `Assert::State` over a pane's allotted rect component (`2` = width, `3` = height).
fn alloc_is(pane: &str, idx: usize, expected: f64) -> Assert {
    Assert::State { pointer: format!("/panes/{pane}/alloc/{idx}"), expected: json!(expected) }
}

#[test]
fn robot_drives_the_pane_window_chrome() {
    let harness = Harness::builder()
        .with_size(egui::vec2(1000.0, 700.0))
        .build_ui_state(|ui, app: &mut ChromeApp| app.ui(ui), ChromeApp::new());
    let mut robot = RobotSession::new(harness, "facett");
    robot.run(2);

    // ── The BASELINE geometry every later assertion is measured against. ──────────
    let base = robot.state();
    let h = |st: &Value, p: &str| st["panes"][p]["alloc"][3].as_f64().unwrap_or(f64::NAN);
    let (h_top, h_mid, h_bot) = (h(&base, "top"), h(&base, "mid"), h(&base, "bot"));
    assert!(
        h_top > 100.0 && h_mid > 100.0 && h_bot > 100.0,
        "baseline: all three panes have real height ({h_top} / {h_mid} / {h_bot})"
    );

    // ── The scenario: real clicks on the chrome controls, geometry read back. ─────
    let scenario = Scenario::new("scene-window-chrome")
        .step(
            Step::new("every pane ships a chrome title bar by DEFAULT (no host opt-in)")
                .action(Action::Step)
                .assert(Assert::State { pointer: "/panes/top/chrome/spec/bar".into(), expected: json!(true) }),
        )
        .step(
            Step::new("the minimise control is present and addressable")
                .action(Action::None)
                .assert(Assert::LabelVisible("minimize top".into())),
        )
        .step(
            Step::new("click MAXIMIZE on `mid` → its sibling `top` is allotted ZERO height")
                .action(Action::ClickLabel("maximize mid".into()))
                .assert(alloc_is("top", 3, 0.0)),
        )
        .step(
            Step::new("…and `bot` is zeroed too — a maximise hides EVERY sibling")
                .action(Action::None)
                .assert(alloc_is("bot", 3, 0.0)),
        )
        .step(
            Step::new("…and the maximised pane took the WHOLE split extent (the root's own height)")
                .action(Action::None)
                .assert(Assert::State { pointer: "/panes/mid/alloc/3".into(), expected: json!(h(&base, "root")) }),
        )
        .step(
            Step::new("…and the state names which control fired")
                .action(Action::None)
                .assert(Assert::State { pointer: "/nav/last_chrome".into(), expected: json!("maximize:mid") }),
        )
        .step(
            Step::new("click RESTORE on `mid` → `top` gets its EXACT prior height back")
                .action(Action::ClickLabel("restore mid".into()))
                .assert(alloc_is("top", 3, h_top)),
        )
        .step(
            Step::new("…and `bot` too — restore is exact, not approximate")
                .action(Action::None)
                .assert(alloc_is("bot", 3, h_bot)),
        )
        .step(
            Step::new("click MINIMIZE on `bot` → it collapses to exactly the title bar's height")
                .action(Action::ClickLabel("minimize bot".into()))
                .assert(alloc_is("bot", 3, scene::CHROME_BAR as f64)),
        )
        .step(
            Step::new("…and its BODY occupies exactly zero height — the content really left")
                .action(Action::None)
                .assert(Assert::State { pointer: "/panes/bot/content_height".into(), expected: json!(0.0) }),
        )
        .step(
            Step::new("…and its siblings are still laid out (minimise ≠ maximise)")
                .action(Action::None)
                .assert(Assert::StateContains { pointer: "/panes/top/minimized".into(), needle: "false".into() }),
        )
        .step(
            Step::new("click RESTORE on `bot` → its exact prior height returns")
                .action(Action::ClickLabel("restore bot".into()))
                .assert(alloc_is("bot", 3, h_bot)),
        )
        .step(
            Step::new("click FOLD on `mid` → its body leaves the tree, its bar stays")
                .action(Action::ClickLabel("fold mid".into()))
                .assert(Assert::State { pointer: "/nav/folded/mid".into(), expected: json!(true) }),
        )
        .step(
            Step::new("…the folded pane's BODY occupies zero height")
                .action(Action::None)
                .assert(Assert::State { pointer: "/panes/mid/content_height".into(), expected: json!(0.0) }),
        )
        .step(
            // THE distinction between the two collapses, in one number: fold hides the
            // body but KEEPS the pane's slot; minimise hands the slot back.
            Step::new("…but a FOLD keeps the pane's slot (unlike a minimise)")
                .action(Action::None)
                .assert(Assert::State { pointer: "/panes/mid/alloc/3".into(), expected: json!(h_mid) }),
        )
        .step(
            Step::new("click UNFOLD on `mid` → the body is back")
                .action(Action::ClickLabel("unfold mid".into()))
                .assert(Assert::State { pointer: "/slots/b/slot".into(), expected: json!("b") }),
        )
        .step(
            Step::new("the draggable divider is still reachable after all of that")
                .action(Action::None)
                .assert(Assert::StateContains { pointer: "/panes/root/resizable".into(), needle: "true".into() }),
        );

    let rows = robot.run_scenario(&scenario);

    // ── RED-when-broken, outside the scenario too: the pane really is back where it
    //    started, to the exact point. ────────────────────────────────────────────
    let end = robot.state();
    assert_eq!(
        [h(&end, "top"), h(&end, "mid"), h(&end, "bot")],
        [h_top, h_mid, h_bot],
        "after maximise → restore → minimise → restore → fold → unfold, the layout is EXACTLY where it began"
    );

    // ── Persist the rows as real matrix cells and round-trip them back. ───────────
    let target_dir = std::env::var_os("CARGO_TARGET_DIR")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("target"));
    std::fs::create_dir_all(&target_dir).ok();
    let out = target_dir.join("robot-matrix-chrome.jsonl");
    std::fs::remove_file(&out).ok();
    let sink = JsonFileSink::new(&out);
    sink.append(&rows).expect("append robot-ui rows");
    let read_back: Vec<TestResultRow> = sink.read_all().expect("read robot-ui rows back");
    assert_eq!(read_back.len(), rows.len(), "every chrome row round-tripped through the sink");
    assert!(read_back.iter().all(|r| r.aspect == "robot-ui"), "every row is a robot-ui aspect row");

    let failed: Vec<&TestResultRow> = rows.iter().filter(|r| r.status != status::PASS).collect();
    eprintln!("\n══ facett-app SCENE WINDOW-CHROME ROBOT DRIVE ══");
    for r in &rows {
        let mark = if r.status == status::PASS { "✓" } else { "✗" };
        eprintln!("  {mark} [{}] {}{}", r.status, r.test_name, if r.message.is_empty() { String::new() } else { format!("  — {}", r.message) });
    }
    eprintln!("── {}/{} chrome steps PASSED ──", rows.len() - failed.len(), rows.len());
    eprintln!("\n{}", render_matrix(&read_back));
    eprintln!("rows → {}\n", out.display());

    assert!(failed.is_empty(), "robot-ui chrome steps FAILED: {:#?}", failed.iter().map(|r| (&r.test_name, &r.message)).collect::<Vec<_>>());
}