dwarves-gui/tests/robot_explorer_links.rs
//! **🧭 Explorer links carry their selection** (owner bug on loki, 0aa9c50: following
//! the Build Thing `tunnr` opened ⚒ Build Thing with nothing selected).
//!
//! The robot drives the SHIPPED shell (`TabbedView`) holding the Explorer and the tabs
//! it links to, over a real forge (Explorer service + code-room proxy) in front of a
//! mock nornir, and a staged constellation the ⚒ Build Thing scene reads. Each link is
//! clicked and the TARGET tab's own state is asserted:
//!
//! * Build Thing `tunnr` (variant `tunnr-cli` picked) → ⚒ Build Thing resolved on
//! tunnr, row `tunnr-cli`;
//! * workspace → 🌲 Tree on that workspace;
//! * pipeline → Pipeline on that file;
//! * a run of the history → Forge with that run pinned.
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use dwarves::explorer::lane::{spawn_forge, FakeRunner};
use dwarves_gui::build_forge::DeclaredForge;
use dwarves_gui::build_scene::BuildScene;
use dwarves_gui::explorer::ExplorerTab;
use dwarves_gui::front_tree::FrontTree;
use dwarves_gui::tabbed::{LeafPanel, SplitPanel, TabPanel, TabbedView};
use egui_kittest::kittest::Queryable;
use facett_core::{Facet, Panel};
fn st(shell: &Arc<Mutex<TabbedView>>) -> Value {
Facet::state_json(&*shell.lock().unwrap())
}
fn tell(shell: &Arc<Mutex<TabbedView>>, msg: &str) {
Facet::update_json(&mut *shell.lock().unwrap(), msg);
}
use nornir_warehouse_embed::testkit::{spawn_router, MockCodeRoom, WorkspacesLog};
use serde_json::{json, Value};
const PROJECT: &str = r#"
[project]
name = "shop"
[mimir]
name = "shop"
[[workspaces]]
name = "shop"
[[build_things]]
name = "tunnr"
repo = "tunnr"
[[pipelines]]
name = "tunnr-ci"
repo = "tunnr"
file = "tunnr.runa.ron"
"#;
/// The forge's warehouse (the registered project) and a constellation of checkouts:
/// `tunnr` declares two variants and a pipeline; `dwarves` holds the Pipeline tab's
/// own file (its repo root).
fn lay_out(dir: &Path) {
let projects = dir.join("warehouse/projects");
std::fs::create_dir_all(&projects).unwrap();
std::fs::write(projects.join("shop.dwarves.toml"), PROJECT).unwrap();
let tunnr = dir.join("checkouts/tunnr");
std::fs::create_dir_all(tunnr.join(".nornir")).unwrap();
std::fs::create_dir_all(tunnr.join("src")).unwrap();
std::fs::write(tunnr.join("Cargo.toml"), "[package]\nname = \"tunnr\"\nversion = \"0.2.1\"\nedition = \"2021\"\n\n[[bin]]\nname = \"tunnr-vm\"\npath = \"src/main.rs\"\n").unwrap();
std::fs::write(tunnr.join("src/main.rs"), "fn main() {}\n").unwrap();
std::fs::write(
tunnr.join("Cargo.lock"),
"version = 4\n\n[[package]]\nname = \"tunnr\"\nversion = \"0.2.1\"\n",
)
.unwrap();
std::fs::write(
tunnr.join(".nornir/build-variants.toml"),
r#"
[build]
base_suite = "ORIGINAL"
[[build.variant]]
name = "tunnr-vm"
package = "tunnr"
bin = "tunnr-vm"
role = "server"
default = true
suites = ["ORIGINAL"]
kind = "exe"
[[build.variant]]
name = "tunnr-cli"
package = "tunnr"
bin = "tunnr-vm"
role = "tool"
default = false
suites = ["ORIGINAL"]
kind = "exe"
"#,
)
.unwrap();
std::fs::write(
tunnr.join("tunnr.runa.ron"),
r#"(name: "tunnr-ci", stages: [(name: "lint", steps: [CargoTest]), (name: "build", needs: ["lint"], steps: [Build(variant: "tunnr-vm")])])"#,
)
.unwrap();
let dwarves = dir.join("checkouts/dwarves");
std::fs::create_dir_all(&dwarves).unwrap();
std::fs::write(
dwarves.join("dwarves.runa.ron"),
r#"(name: "dwarves-forge", stages: [(name: "lint", steps: [CargoTest])])"#,
)
.unwrap();
}
fn board() -> Value {
json!({
"state": "complete",
"members": [{ "member": "tunnr", "phase": "landed", "sha": "t1", "units": 1, "done": 1, "own_units": 1 }],
"counts": { "done": 1 },
"distinct_units": 1,
"units": [{ "key": "t", "package": "path+ws:tunnr#tunnr@0.2.1", "target": "bin:tunnr-vm", "mode": "check", "test": false, "features": [], "members": ["tunnr"], "shared_by": 1, "state": "done", "deps": [] }],
})
}
fn step_until(
h: &mut egui_kittest::Harness<'_>,
shell: &Arc<Mutex<TabbedView>>,
secs: u64,
what: &str,
cond: impl Fn(&Value) -> bool,
) -> Value {
let t0 = Instant::now();
loop {
h.step();
let s = st(shell);
if cond(&s) {
return s;
}
assert!(
t0.elapsed() < Duration::from_secs(secs),
"{what}: never happened — active {} · explorer {} · build-thing {}",
s["active_tab"],
s["explorer"],
s["build-thing"]
);
std::thread::sleep(Duration::from_millis(15));
}
}
#[test]
fn every_explorer_link_opens_its_tab_on_the_selection() {
let dir = tempfile::tempdir().unwrap();
lay_out(dir.path());
let checkouts = dir.path().join("checkouts");
// the Pipeline tab's repo root: the dwarves checkout of this constellation
std::env::set_var("DWARVES_REPO_ROOT", checkouts.join("dwarves"));
let registry = WorkspacesLog::default();
registry.set_member_states(&[("tunnr", "ok")]);
let room = MockCodeRoom::answering(&[
("Coderoom.Units", board()),
(
"Coderoom.Artifacts",
json!({ "state": "empty_fact", "count": 0, "artifacts": [] }),
),
]);
let nornir = spawn_router(move |mut s| {
s.add_service(
nornir_warehouse_embed::pb::workspaces_server::WorkspacesServer::new(registry),
)
.add_service(nornir_warehouse_embed::pb::code_room_server::CodeRoomServer::new(room))
});
nornir_warehouse_embed::register_workspace_descriptor(
&nornir.endpoint,
"",
"shop",
"[workspace]\nname = \"shop\"\n",
"monitored",
)
.unwrap();
let forge = spawn_forge(
nornir.endpoint.clone(),
String::new(),
dir.path().join("warehouse"),
Some(checkouts.clone()),
Arc::new(FakeRunner::ok(Duration::from_millis(200))),
);
let explorer: Box<dyn Panel> =
Box::new(ExplorerTab::new(forge.endpoint.clone(), String::new()));
let build_thing: Box<dyn LeafPanel> =
Box::new(BuildScene::new(Box::new(DeclaredForge::at(checkouts.clone()))).hosted());
let tree: Box<dyn Panel> = Box::new(FrontTree::new(
forge.endpoint.clone(),
String::new(),
String::new(),
));
let pipeline: Box<dyn SplitPanel> = Box::new(dwarves_gui::pipeline::pipeline_view());
let forge_tab: Box<dyn Panel> = dwarves_embed::panel(forge.endpoint.clone());
let tabs: Vec<(String, TabPanel)> = vec![
("Explorer".into(), explorer.into()),
("Build Thing".into(), build_thing.into()),
("Tree".into(), tree.into()),
("Pipeline".into(), pipeline.into()),
("Forge".into(), forge_tab.into()),
];
let shell = Arc::new(Mutex::new(TabbedView::new("links", tabs)));
let s2 = shell.clone();
let mut h = egui_kittest::Harness::builder()
.with_size(egui::vec2(1600.0, 1100.0))
.build_ui(move |ui| Facet::ui(&mut *s2.lock().unwrap(), ui));
// ── Build Thing `tunnr`, variant `tunnr-cli` → ⚒ Build Thing on tunnr ──
step_until(&mut h, &shell, 20, "the project tree", |s| {
s["explorer"]["tree"]
.as_array()
.is_some_and(|t| t.iter().any(|n| n == "build_thing:tunnr"))
});
let s = st(&shell);
assert_eq!(
s["build-thing"]["recipe"],
Value::Null,
"RED before the link: nothing is resolved in ⚒ Build Thing"
);
h.get_by_label("🔨 tunnr").click();
step_until(&mut h, &shell, 20, "tunnr's variants", |s| {
s["explorer"]["variants"] == json!(["tunnr-vm", "tunnr-cli"])
});
h.get_by_label("tunnr-cli").click();
h.step();
h.get_by_label("open ⚒ Build Thing").click();
let s = step_until(&mut h, &shell, 20, "⚒ Build Thing open on tunnr", |s| {
s["active_tab"] == "Build Thing" && !s["build-thing"]["recipe"].is_null()
});
assert_eq!(
s["build-thing"]["artifact"], "tunnr",
"{}",
s["build-thing"]
);
assert_eq!(
s["build-thing"]["row"], "tunnr-cli",
"the picked variant is the selected row: {}",
s["build-thing"]
);
assert_eq!(s["explorer"]["last_link"]["tab"], "build-thing");
// ── workspace → 🌲 Tree on that workspace ──
tell(&shell, r#"{"set_tab":"Explorer"}"#);
h.step();
h.get_by_label("📚 shop").click();
step_until(&mut h, &shell, 20, "the workspace", |s| {
s["explorer"]["detail_kind"] == "workspace"
});
h.get_by_label("open 🌲 Tree").click();
let s = step_until(&mut h, &shell, 20, "🌲 Tree open on shop", |s| {
s["active_tab"] == "Tree"
});
assert_eq!(s["tree"]["workspace"], "shop", "{}", s["tree"]);
// ── a run → Forge, that run pinned ──
tell(&shell, r#"{"set_tab":"Explorer"}"#);
h.step();
h.get_by_label("▶ Build workspace").click();
let s = step_until(&mut h, &shell, 60, "a run in the history", |s| {
s["explorer"]["builds"]
.as_array()
.is_some_and(|b| !b.is_empty())
});
let run = s["explorer"]["builds"][0].as_str().unwrap().to_string();
h.get_by_label(run.as_str()).click();
let s = step_until(&mut h, &shell, 20, "Forge open on the run", |s| {
s["active_tab"] == "Forge"
});
assert_eq!(s["forge"]["selected_run_id"], json!(run), "{}", s["forge"]);
// ── pipeline → Pipeline on that file (a sibling checkout's) ──
tell(&shell, r#"{"set_tab":"Explorer"}"#);
h.step();
h.get_by_label("🔀 tunnr-ci").click();
step_until(&mut h, &shell, 20, "the pipeline", |s| {
s["explorer"]["detail_kind"] == "pipeline"
});
h.get_by_label("open Pipeline").click();
let s = step_until(&mut h, &shell, 20, "Pipeline open on tunnr.runa.ron", |s| {
s["active_tab"] == "Pipeline"
});
assert_eq!(
(
s["pipeline"]["pipeline_file"].as_str(),
s["pipeline"]["pipeline_name"].as_str()
),
(Some("tunnr.runa.ron"), Some("tunnr-ci")),
"{}",
s["pipeline"]
);
}