dwarves-gui/tests/robot_code_map.rs
//! **ROBOT-UI: the 🏛 code room is a map you can zoom, fly and read.**
//!
//! The tab over a canned `Viz.CodeMap` (one repo, a dependency, three function cards),
//! driven through egui's real input — the same events a mouse makes:
//!
//! 1. the mouse WHEEL over the canvas changes the camera's distance (zoom to the
//! cursor), and the canvas consumes it;
//! 2. CLICKING a head in 🎯 Focus selects it and the camera flies to it;
//! 3. RESTING the pointer on a head unfolds its source (its first line shown), and the
//! wheel over the panel scrolls it;
//! 4. Esc closes it.
//!
//! A live render (`live_code_map_render`, ignored) draws the map of a running server
//! read-only and writes a PNG.
#![cfg(not(target_arch = "wasm32"))]
use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};
use dwarves_gui::coderoom3d::{testing::map_room, CodeRoom3DTab, CARDS_DIST};
use egui::{Event, Modifiers, MouseWheelUnit, PointerButton, Pos2, TouchPhase};
use egui_kittest::Harness;
use facett_core::Facet;
use serde_json::{json, Value};
fn wheel(h: &Harness<'_>, at: Pos2, lines: f32) {
h.event(Event::PointerMoved(at));
h.event(Event::MouseWheel { unit: MouseWheelUnit::Line, delta: egui::vec2(0.0, lines), phase: TouchPhase::Move, modifiers: Modifiers::NONE });
}
fn click(h: &Harness<'_>, at: Pos2) {
h.event(Event::PointerMoved(at));
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
}
fn at(v: &Value) -> Pos2 {
Pos2::new(v["at"][0].as_f64().unwrap() as f32, v["at"][1].as_f64().unwrap() as f32)
}
/// Step until `done` (a worker answers off-thread), at most a few seconds.
fn settle(h: &mut Harness<'_>, tab: &Rc<RefCell<CodeRoom3DTab>>, done: impl Fn(&Value) -> bool) -> Value {
let t0 = Instant::now();
loop {
h.step();
let s = tab.borrow().state_json();
if done(&s) || t0.elapsed() > Duration::from_secs(10) {
return s;
}
std::thread::sleep(Duration::from_millis(10));
}
}
#[test]
fn the_code_map_zooms_on_the_wheel_flies_to_a_clicked_head_and_unfolds_its_source() {
let tab = Rc::new(RefCell::new(CodeRoom3DTab::with_source(map_room(), "w")));
tab.borrow_mut().update_json(r#"{"wait":true}"#);
let t = tab.clone();
let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui(move |ui| t.borrow_mut().ui(ui));
let s = settle(&mut h, &tab, |s| s["canvas"].is_array());
assert_eq!(s["from_room"], true, "{s}");
let canvas = s["canvas"].as_array().unwrap().iter().map(|x| x.as_f64().unwrap() as f32).collect::<Vec<_>>();
let mid = Pos2::new((canvas[0] + canvas[2]) * 0.5, (canvas[1] + canvas[3]) * 0.5);
// 1. the wheel zooms: the camera comes closer, and the canvas took the event
let d0 = s["camera"]["dist"].as_f64().unwrap();
wheel(&h, mid, 3.0);
let s = settle(&mut h, &tab, |s| s["wheel_events"].as_u64() > Some(0));
let d1 = s["camera"]["dist"].as_f64().unwrap();
assert!(d1 < d0 * 0.8, "the wheel zooms in: {d0} → {d1}");
wheel(&h, mid, -3.0);
let s = settle(&mut h, &tab, |s| s["wheel_events"].as_u64() > Some(1));
assert!(s["camera"]["dist"].as_f64().unwrap() > d1 * 1.2, "and back out");
// fly in over the cards: the tile is asked and its heads are drawn
tab.borrow_mut().update_json(&json!({ "camera": { "target": [20.0, 0.0, 32.0], "dist": 40.0, "yaw": 0.0, "pitch": 1.0 }, "nav": "focus" }).to_string());
let s = settle(&mut h, &tab, |s| s["heads_screen"].as_array().is_some_and(|a| a.len() == 3));
assert_eq!(s["lod"], "cards", "{s}");
assert!((s["camera"]["dist"].as_f64().unwrap() as f32) < CARDS_DIST);
assert_eq!(s["splines_drawn"], 2, "{s}");
// 2. a click on `total`'s head in 🎯 Focus: selected, and the camera flies to it
let total = s["heads_screen"].as_array().unwrap().iter().find(|x| x["card"] == 1).unwrap().clone();
click(&h, at(&total));
let s = settle(&mut h, &tab, |s| s["selected"] == 1 && s["camera"]["flying"] == false && s["camera"]["target"][0].as_f64().unwrap() > 25.0);
assert_eq!(s["selected"], 1, "{s}");
let tx = s["camera"]["target"][0].as_f64().unwrap();
assert!((tx - 26.5).abs() < 0.5, "the camera landed on total's card (x 26.5): {tx}");
// the click pinned total's source open; Esc closes it
h.key_press(egui::Key::Escape);
let s = settle(&mut h, &tab, |s| s["panels"].as_array().is_some_and(Vec::is_empty));
assert!(s["panels"].as_array().unwrap().is_empty(), "Esc closes the source: {s}");
// 3. resting on `checkout`'s head unfolds its source; the wheel over it scrolls it
tab.borrow_mut().update_json(&json!({ "camera": { "target": [24.0, 0.0, 34.0], "dist": 55.0, "yaw": 0.0, "pitch": 1.0 }, "nav": "stay" }).to_string());
let s = settle(&mut h, &tab, |s| s["heads_screen"].as_array().is_some_and(|a| a.iter().any(|x| x["card"] == 0)));
let checkout = at(s["heads_screen"].as_array().unwrap().iter().find(|x| x["card"] == 0).unwrap());
h.event(Event::PointerMoved(checkout));
let s = settle(&mut h, &tab, |s| s["panels"][0]["first_line"].is_string());
assert_eq!(s["hovered"], 0, "{s}");
assert_eq!(s["panels"][0]["first_line"], "pub fn checkout(cart: &Cart) -> u64 {", "{s}");
assert_eq!(s["panels"][0]["pinned"], false, "a resting pointer opens it, unpinned");
let panel = at(&s["panels_screen"][0]);
wheel(&h, panel, -2.0);
let s = settle(&mut h, &tab, |s| s["panels"][0]["scroll"].as_f64() > Some(0.0));
assert!(s["panels"][0]["scroll"].as_f64().unwrap() > 0.0, "the wheel over the source scrolls it: {s}");
assert_ne!(s["panels"][0]["first_line"], "pub fn checkout(cart: &Cart) -> u64 {");
assert!((s["camera"]["dist"].as_f64().unwrap() - 55.0).abs() < 1e-3, "and does not zoom the map");
// 4. a click on the panel pins it
click(&h, panel);
let s = settle(&mut h, &tab, |s| s["panels"][0]["pinned"] == true);
assert_eq!(s["panels"][0]["pinned"], true, "{s}");
if let Ok(png) = std::env::var("DWARVES_CODE_MAP_PNG") {
match h.render() {
Ok(img) => {
img.save(&png).expect("write png");
println!("wrote {png}");
}
Err(e) => println!("no render: {e}"),
}
}
}
/// The live render: `DWARVES_LIVE_FORGE=http://10.125.73.187:7900 DWARVES_LIVE_WS=nordisk
/// DWARVES_CODE_MAP_PNG=/tmp/code-map.png [DWARVES_CODE_MAP_FIND=nornir]
/// cargo test -p dwarves-gui --test robot_code_map live_code_map_render -- --ignored --nocapture`.
/// Read-only: it asks and looks, it clicks nothing.
#[test]
#[ignore]
fn live_code_map_render() {
let Ok(live) = std::env::var("DWARVES_LIVE_FORGE") else {
eprintln!("DWARVES_LIVE_FORGE not set — nothing to render");
return;
};
let ws = std::env::var("DWARVES_LIVE_WS").unwrap_or_else(|_| "nordisk".into());
let token = std::env::var("DWARVES_LIVE_TOKEN").unwrap_or_default();
let tab = Rc::new(RefCell::new(CodeRoom3DTab::new(live, token, ws)));
let t = tab.clone();
let mut h = Harness::builder().with_size(egui::vec2(1800.0, 1200.0)).build_ui(move |ui| t.borrow_mut().ui(ui));
let t0 = Instant::now();
let deadline = Duration::from_secs(std::env::var("DWARVES_CODE_MAP_WAIT_S").ok().and_then(|s| s.parse().ok()).unwrap_or(240));
while t0.elapsed() < deadline {
h.step();
let s = tab.borrow().state_json();
if s["from_room"] == true || (s["pending"] == false && s["queued"].as_array().is_some_and(Vec::is_empty) && s["answer"]["state"] != "pending") {
break;
}
std::thread::sleep(Duration::from_millis(200));
}
if let Ok(cam) = std::env::var("DWARVES_CODE_MAP_CAMERA") {
tab.borrow_mut().update_json(&json!({ "camera": serde_json::from_str::<Value>(&cam).unwrap() }).to_string());
}
if let Ok(q) = std::env::var("DWARVES_CODE_MAP_FIND") {
tab.borrow_mut().update_json(&json!({ "find": q }).to_string());
tab.borrow_mut().update_json(r#"{"wait":true}"#);
}
// DWARVES_CODE_MAP_GO=1: stand over the first function found, close enough for its
// card, and rest the pointer on its head (its source unfolds)
let go = std::env::var("DWARVES_CODE_MAP_GO").is_ok();
let mut target_card: Option<u64> = None;
if go {
let s = tab.borrow().state_json();
if let Some(h) = s["hits_list"].as_array().and_then(|a| a.iter().find(|h| h["what"] == "fn")) {
let r: Vec<f64> = h["rect"].as_array().unwrap().iter().map(|x| x.as_f64().unwrap()).collect();
let dist: f64 = std::env::var("DWARVES_CODE_MAP_DIST").ok().and_then(|d| d.parse().ok()).unwrap_or(45.0);
target_card = h["card"].as_u64();
tab.borrow_mut().update_json(&json!({ "camera": { "target": [(r[0] + r[2]) * 0.5 + 8.0, 0.0, (r[1] + r[3]) * 0.5 + 4.0], "dist": dist, "yaw": 0.0, "pitch": 1.0 } }).to_string());
}
}
// let the tiles the camera sees land
let t1 = Instant::now();
while t1.elapsed() < deadline {
h.step();
let s = tab.borrow().state_json();
if s["pending"] == false && s["queued"].as_array().is_some_and(Vec::is_empty) {
break;
}
std::thread::sleep(Duration::from_millis(200));
}
for _ in 0..5 {
h.step();
}
if let Some(card) = target_card {
let s = tab.borrow().state_json();
if let Some(head) = s["heads_screen"].as_array().and_then(|a| a.iter().find(|x| x["card"] == card)) {
h.event(Event::PointerMoved(at(head)));
let t2 = Instant::now();
while t2.elapsed() < Duration::from_secs(60) {
h.step();
if tab.borrow().state_json()["panels"][0]["first_line"].is_string() {
break;
}
std::thread::sleep(Duration::from_millis(50));
}
for _ in 0..5 {
h.step();
}
}
}
let s = tab.borrow().state_json();
let mut short = s.clone();
short["hits_list"] = json!(s["hits_list"].as_array().map(Vec::len));
short["heads_screen"] = json!(s["heads_screen"].as_array().map(Vec::len));
short["plates_screen"] = json!(s["plates_screen"].as_array().map(Vec::len));
println!("{}", serde_json::to_string_pretty(&short).unwrap());
if let Ok(png) = std::env::var("DWARVES_CODE_MAP_PNG") {
match h.render() {
Ok(img) => {
img.save(&png).expect("write png");
println!("wrote {png}");
}
Err(e) => println!("no render: {e}"),
}
}
}