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