dwarves-gui/tests/robot_build_scene_fixture.rs
//! **ROBOT-UI: the DECLARED FIXTURE affordance, through real widgets.**
//!
//! A recipe may declare that a per-site parameter can be filled by a fixture the forge
//! mints itself (`[build.installer.fixtures] hub_conf = "wg-hub"`). The scene must offer
//! that — and ONLY that — where the recipe says so: the scene knows no fixture by name
//! any more than it knows a product by name. So this file stages a constellation with one
//! installer cell declaring two parameters, one with a fixture and one without, and
//! drives the scene through the live AccessKit tree:
//!
//! * RED — the parameter WITHOUT a declared fixture has no "använd fixtur" control at all;
//! * GREEN — the one WITH it has an addressable checkbox, unticked; ticking it puts the
//! CLI's own `@fixture` word into the settings the build will be given;
//! * the staged text file is a MULTI-LINE value in the scene's own state (the 08-16
//! friction: a hub.conf in a single-line field);
//! * the result row for an applied fixture is covered by `robot_build_scene.rs`
//! (`outcome.applied`), since pressing BYGG here would start a real build.
use facett_core::Facet;
use nornir_robotui::egui_kittest::Harness;
use nornir_robotui::{Observable, RobotSession, Target};
use dwarves_gui::build_forge::DeclaredForge;
use dwarves_gui::build_scene::BuildScene;
struct SceneApp(BuildScene);
impl Observable for SceneApp {
fn state_json(&self) -> serde_json::Value {
Facet::state_json(&self.0)
}
}
/// The fixture's label as the recipe below spells it — asserted, never assumed, by
/// reading it back out of the scene's state.
const FIXTURE_TOGGLE: &str = "hub_conf: använd fixtur (wg-hub)";
const NO_FIXTURE_TOGGLE: &str = "site: använd fixtur (wg-hub)";
/// One checkout, one exe atom, one installer cell with two declared parameters:
/// `site` (a cmdline token, no fixture) and `hub_conf` (a staged text file WITH a
/// declared `wg-hub` fixture). Not any product's names.
fn staged() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let repo = dir.path().join("orten");
std::fs::create_dir_all(repo.join(".nornir")).expect("mkdir");
std::fs::write(
repo.join("Cargo.toml"),
"[package]\nname = \"orten\"\nversion = \"1.0.0\"\nedition = \"2021\"\n\n[dependencies]\n",
)
.expect("manifest");
std::fs::write(
repo.join("Cargo.lock"),
"version = 4\n\n[[package]]\nname = \"orten\"\nversion = \"1.0.0\"\n",
)
.expect("lockfile");
std::fs::write(
repo.join(".nornir").join("build-variants.toml"),
r#"
[build]
base_suite = "ORIGINAL"
[[build.variant]]
name = "orten-server"
package = "orten"
bin = "orten-server"
role = "server"
default = true
suites = ["ORIGINAL"]
kind = "exe"
# The installer pointer variant: carries the cell's name (the Iso shape).
[[build.variant]]
name = "orten-installer"
role = "server"
default = false
suites = ["ORIGINAL"]
kind = "installer"
linkage = "fat"
[[build.installer]]
name = "orten-installer"
variant = "orten-server"
volume_id = "ORTEN_INSTALL"
boot_params = ["korp.entry=orten"]
[[build.installer.params]]
name = "site"
label = "Site name"
kind = "text"
required = false
boot_param = "korp.site"
[[build.installer.params]]
name = "hub_conf"
label = "Hub declaration (public data)"
kind = "text"
required = false
staged_file = "hub.conf"
[build.installer.fixtures]
hub_conf = "wg-hub"
"#,
)
.expect("declaration");
dir
}
fn session(root: &std::path::Path) -> RobotSession<'_, SceneApp> {
let scene = BuildScene::new(Box::new(DeclaredForge::at(root.to_path_buf())));
let harness = Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(1400.0, 1000.0))
.build_ui_state(
|ui, app: &mut SceneApp| Facet::ui(&mut app.0, ui),
SceneApp(scene),
);
let mut robot = RobotSession::new(harness, "dwarves-gui");
robot.run(2);
robot
}
/// The settings the installer CELL's column declares, as the scene's state carries
/// them. The cell hangs off the row of the appliance it LAYS DOWN (`variant =
/// "orten-server"`), as column `installer:<cell>` — the pointer variant's own row
/// carries the kind, not the cell's declared inputs.
fn installer_settings(robot: &RobotSession<'_, SceneApp>) -> (String, Vec<serde_json::Value>) {
let rows = robot.state_at("/rows").expect("rows");
for row in rows.as_array().expect("rows is a list") {
if row["name"] != "orten-server" {
continue;
}
for t in row["targets"].as_array().expect("targets") {
let name = t["name"].as_str().unwrap_or_default();
if t["kind"] == "installer" && t["declared"] == true && name.starts_with("installer:") {
return (
name.to_string(),
t["settings"].as_array().cloned().unwrap_or_default(),
);
}
}
}
panic!("no declared installer:<cell> column on the orten-server row: {rows}");
}
#[test]
fn the_fixture_toggle_is_offered_only_where_the_recipe_declares_it_and_supplies_at_fixture() {
let dir = staged();
let mut robot = session(dir.path());
robot
.type_into("artefakt", "orten-server")
.expect("the artefakt field");
robot.click_label("sök").expect("the sök button");
robot.run(2);
robot
.click_label("orten-server")
.expect("the appliance row is a real widget");
robot.run(2);
let (column, settings) = installer_settings(&robot);
robot
.click_label(&column)
.expect("the installer column is clickable");
robot.run(3);
assert_eq!(robot.state_at("/target"), Some(serde_json::json!(column)));
// ── the DECLARATION reached the scene's state, verbatim ─────────────────
let hub = settings
.iter()
.find(|s| s["key"] == "hub_conf")
.expect("hub_conf declared");
let site = settings
.iter()
.find(|s| s["key"] == "site")
.expect("site declared");
assert_eq!(
hub["fixture"], "wg-hub",
"the declared fixture, by its label: {hub}"
);
assert_eq!(
hub["multiline"], true,
"a staged text file is a multi-line value: {hub}"
);
assert_eq!(
site["fixture"],
serde_json::Value::Null,
"no fixture declared for site: {site}"
);
assert_eq!(
site["multiline"], false,
"a cmdline token is one line: {site}"
);
// ── RED: no toggle where no fixture is declared ───────────────────────
assert!(
!robot.label_visible(NO_FIXTURE_TOGGLE),
"a fixture toggle was drawn for `site`, whose recipe declares none — an affordance \
nobody can honestly use is the same defect as an invented column"
);
// ── GREEN: an addressable, unticked checkbox where one IS declared ───
assert!(
robot.label_visible(FIXTURE_TOGGLE),
"the declared fixture has no control at all"
);
assert_eq!(
robot.field_checked(&Target::Label(FIXTURE_TOGGLE.into())),
Some(false),
"the fixture control is not a checkbox with a state"
);
assert_ne!(
robot.state_at("/settings/hub_conf"),
Some(serde_json::json!("@fixture")),
"@fixture was supplied before anybody asked for it"
);
// Tick it: the settings the build is given now carry the CLI's own word.
robot
.click_label(FIXTURE_TOGGLE)
.expect("the fixture checkbox is clickable");
robot.run(2);
assert_eq!(
robot.field_checked(&Target::Label(FIXTURE_TOGGLE.into())),
Some(true),
"clicking the checkbox did not change the WIDGET's state"
);
assert_eq!(
robot.state_at("/settings/hub_conf"),
Some(serde_json::json!("@fixture")),
"the tick never reached the settings the build would be given"
);
assert_eq!(robot.state_at("/can_build"), Some(serde_json::json!(true)));
// Untick: back to an operator's empty field, not a stale sentinel.
robot
.click_label(FIXTURE_TOGGLE)
.expect("the fixture checkbox is clickable twice");
robot.run(2);
assert_eq!(
robot.state_at("/settings/hub_conf"),
Some(serde_json::json!(""))
);
}