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

nordisk/edda

crates/nornir-robotui/tests/watch_duality.rs

//! **Duality proof (headless half):** the SAME `demo::bump_scenario()` a human
//! watches via `robotui watch` is driven here HEADLESS through the unified
//! `run_scenario_mode(.., RunMode::Headless, ..)` seam and asserted to fill the
//! matrix. This is the CI half of "one scenario, both modes"; the watch half is
//! the visible window (`robotui watch`), which shares this exact `Scenario` +
//! `bump_draw` closure.

use nornir_robotui::{demo, run_scenario_mode, status, RunMode, RunOutcome, WatchConfig};

#[test]
fn demo_scenario_fills_the_matrix_headless() {
    let scenario = demo::bump_scenario();
    let outcome = run_scenario_mode(
        demo::BumpApp::default(),
        demo::bump_draw,
        &scenario,
        "robotui-demo",
        RunMode::Headless,
        WatchConfig::default(),
    )
    .expect("headless run always succeeds without the watch feature");

    let rows = match outcome {
        RunOutcome::Headless(rows) => rows,
        RunOutcome::Watched => panic!("Headless mode must return matrix rows"),
    };

    assert_eq!(rows.len(), scenario.steps.len(), "one matrix row per step");
    assert!(rows.iter().all(|r| r.suite == "bump-demo"), "suite carried through");
    assert!(rows.iter().all(|r| r.repo == "robotui-demo"), "repo stamped");
    assert!(rows.iter().all(|r| r.aspect == "robot-ui"), "aspect tags the robot run");
    let run_id = &rows[0].run_id;
    assert!(rows.iter().all(|r| &r.run_id == run_id), "all rows share one run id");
    assert!(
        rows.iter().all(|r| r.status == status::PASS),
        "every demo step passes headless: {:?}",
        rows.iter().map(|r| (&r.test_name, &r.status, &r.message)).collect::<Vec<_>>()
    );
}

#[test]
fn run_mode_default_is_headless() {
    assert_eq!(RunMode::default(), RunMode::Headless);
}

// ── TOML round-trip (feature `watch`): the SAME scenario serialized to disk and
//    reloaded is byte-for-byte the same steps, and can emit the canonical demo
//    `.toml` the `robotui watch <scenario.toml>` command loads. ────────────────
#[cfg(feature = "watch")]
#[test]
fn demo_scenario_toml_round_trips_and_can_emit_the_canonical_file() {
    use nornir_robotui::Scenario;

    let scenario = demo::bump_scenario();
    let toml = scenario.to_toml_string().expect("serialize demo scenario to TOML");
    let back = Scenario::from_toml_str(&toml).expect("parse it back");

    assert_eq!(back.suite, scenario.suite);
    assert_eq!(back.steps.len(), scenario.steps.len());
    for (a, b) in scenario.steps.iter().zip(back.steps.iter()) {
        assert_eq!(a.label, b.label, "step labels round-trip");
    }

    // Opt-in emit of the committed demo file (kept in the repo so the CLI has a
    // scenario to load): `ROBOTUI_WRITE_DEMO_TOML=<path> cargo test ... --features watch`.
    if let Ok(path) = std::env::var("ROBOTUI_WRITE_DEMO_TOML") {
        std::fs::create_dir_all(std::path::Path::new(&path).parent().unwrap()).unwrap();
        std::fs::write(&path, &toml).expect("write canonical demo TOML");
    }
}

/// A CHAINED assert must survive the TOML round-trip. If `Assert::All` did not
/// serialize, a `.toml` scenario carrying a chain would silently lose clauses on
/// load — the same defect the 2026-09-01 `Step::assert` fix removed, reappearing
/// on the disk path.
///
/// This checks the ASSERTS, not just the labels: the round-trip test above
/// compares only `step.label`, which cannot see an assertion going missing.
#[cfg(feature = "watch")]
#[test]
fn a_chained_assert_survives_the_toml_round_trip() {
    use nornir_robotui::{Assert, Scenario, Step};
    use serde_json::json;

    let scenario = Scenario::new("chain-toml").step(
        Step::new("three clauses")
            .assert(Assert::Drew)
            .assert(Assert::LabelVisible("Bump".into()))
            .assert(Assert::State {
                pointer: "/clicks".into(),
                expected: json!(1),
            }),
    );
    assert_eq!(
        scenario.steps[0].assert.clauses().len(),
        3,
        "precondition: the chain accumulated before we serialize it"
    );

    let toml = scenario.to_toml_string().expect("serialize a chained scenario");
    let back = Scenario::from_toml_str(&toml).expect("parse a chained scenario back");

    assert_eq!(
        back.steps[0].assert.clauses().len(),
        3,
        "all three clauses came back; TOML was:\n{toml}"
    );
    assert_eq!(
        format!("{:?}", back.steps[0].assert),
        format!("{:?}", scenario.steps[0].assert),
        "and they came back in order, unchanged"
    );
}