facett-geomap/tests/robot_callchain.rs
//! **Robot / headless drive proof** for `facett_geomap::OsmView` (the slippy-map
//! viewer) — the input→PIXEL tail of the call chain.
//!
//! Drives a populated `OsmView` through `egui_kittest`'s real frame loop, applies
//! pan + zoom navigation, and asserts:
//! 1. the camera state changed correctly (zoom grew, centre moved, visible
//! counts respond) — read back from `state_json`, the observable seam,
//! 2. the pane is a queryable AccessKit Image node (robot read-back), and
//! 3. the render is non-blank AND confined to the widget rect (wgpu leg, skips
//! gracefully without a GPU).
//!
//! Run: `cargo test -p facett-geomap --test robot_callchain`
use std::cell::RefCell;
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
use facett_core::Facet;
use facett_geomap::{OsmView, osm2d::sample_dataset};
/// **Robot drive (CPU, always runs).** Build a populated viewer, render a first
/// frame (fits to data), zoom in hard at the centre, render again, and assert the
/// camera state moved the way the call chain says it should — read from
/// `state_json`, not pixels.
#[test]
fn robot_pan_zoom_changes_camera_state() {
let (points, ways) = sample_dataset(2_000);
let view = RefCell::new(OsmView::new("robot-osm", points, ways));
let mut harness = Harness::builder().with_size(egui::vec2(800.0, 600.0)).build_ui(|ui| {
view.borrow_mut().ui(ui);
});
// First frame fits to the data.
harness.run();
let z0 = view.borrow().state_json()["zoom"].as_f64().unwrap();
let vis0 = view.borrow().state_json()["visible_points"].as_u64().unwrap();
assert!(z0 > 0.0 && vis0 > 0, "fit shows the data: zoom {z0}, {vis0} visible");
// Drive a real zoom-in at the pane centre via the public navigation API
// (the same camera path the wheel handler calls), then re-render.
{
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(800.0, 600.0));
let mut v = view.borrow_mut();
for _ in 0..6 {
v.zoom_to_cursor(1.6, rect.center(), rect);
}
}
harness.run();
let z1 = view.borrow().state_json()["zoom"].as_f64().unwrap();
let vis1 = view.borrow().state_json()["visible_points"].as_u64().unwrap();
assert!(z1 > z0, "zooming in raised the zoom: {z1} > {z0}");
assert!(vis1 < vis0, "zooming in culled points out of view: {vis1} < {vis0}");
// Drive a pan and assert the centre moved.
let c0 = view.borrow().state_json()["center_merc"][0].as_f64().unwrap();
view.borrow_mut().pan_pixels(300.0, 0.0);
harness.run();
let c1 = view.borrow().state_json()["center_merc"][0].as_f64().unwrap();
assert!(c1 < c0, "panning +x moved the centre west: {c1} < {c0}");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-geomap.robot",
"pan_zoom_changes_camera_state",
z1 > z0 && vis1 < vis0 && c1 < c0,
&format!("zoom {z0:.0}->{z1:.0}, visible {vis0}->{vis1}, centre moved west"),
);
}
/// **Robot read-back (CPU, always runs).** The pane is a value-bearing AccessKit
/// Image node a robot driver / screen reader can query by its summary label.
#[test]
fn robot_pane_is_queryable() {
let (points, ways) = sample_dataset(1_000);
let view = RefCell::new(OsmView::new("OSM 2D", points, ways));
let mut harness = Harness::builder().with_size(egui::vec2(700.0, 520.0)).build_ui(|ui| {
view.borrow_mut().ui(ui);
});
harness.run();
// The summary label is "{title} — {n} visible points · {m} visible ways · zoom {z}".
let vis = view.borrow().state_json()["visible_points"].as_u64().unwrap();
let visw = view.borrow().state_json()["visible_ways"].as_u64().unwrap();
let zoom = view.borrow().state_json()["zoom"].as_f64().unwrap();
let label = format!("OSM 2D — {vis} visible points · {visw} visible ways · zoom {zoom:.0}");
let pane = harness.get_by_label(&label).accesskit_node();
assert_eq!(pane.role(), egui::accesskit::Role::Image, "pane is an Image node");
assert_eq!(pane.numeric_value(), Some(vis as f64), "visible point count rides the tree");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-geomap.robot",
"pane_node_queryable",
pane.role() == egui::accesskit::Role::Image && pane.numeric_value() == Some(vis as f64),
&format!("Image node, {vis} visible points"),
);
}
/// **Render confinement (wgpu).** The viewer paints a non-blank framebuffer whose
/// content is CONFINED to its allocated rect. The OsmView is allocated in the top
/// half; the bottom margin must stay untouched window background. Skips without a
/// GPU.
#[test]
fn robot_renders_non_blank_and_confined() {
let (points, ways) = sample_dataset(4_000);
let view = RefCell::new(OsmView::new("osm", points, ways));
let mut harness = Harness::builder().with_size(egui::vec2(640.0, 480.0)).build_ui(|ui| {
facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
let full = ui.available_rect_before_wrap();
let map_rect = egui::Rect::from_min_size(full.min, egui::vec2(full.width(), full.height() * 0.5));
ui.scope_builder(egui::UiBuilder::new().max_rect(map_rect), |ui| {
view.borrow_mut().ui(ui);
});
});
harness.run_steps(3);
let img = match harness.render() {
Ok(img) => img,
Err(e) => {
eprintln!("[robot] no wgpu backend ({e}) — confinement pixel leg skipped");
return;
}
};
let (w, h) = (img.width(), img.height());
let win_bg = *img.get_pixel(w / 2, h - 3);
let (wr, wg, wb) = (win_bg.0[0] as i32, win_bg.0[1] as i32, win_bg.0[2] as i32);
let far_from = |p: &image::Rgba<u8>| {
(p.0[0] as i32 - wr).abs() + (p.0[1] as i32 - wg).abs() + (p.0[2] as i32 - wb).abs() > 24
};
let map_h = (h as f32 * 0.5) as u32;
let margin_y0 = (h as f32 * 0.6) as u32;
let mut body_content = 0usize;
let mut body_total = 0usize;
let mut margin_painted = 0usize;
let mut margin_total = 0usize;
for (_x, y, p) in img.enumerate_pixels() {
if y < map_h {
body_total += 1;
if far_from(p) {
body_content += 1;
}
} else if y >= margin_y0 {
margin_total += 1;
if far_from(p) {
margin_painted += 1;
}
}
}
let body_ratio = body_content as f64 / body_total.max(1) as f64;
let margin_leak = margin_painted as f64 / margin_total.max(1) as f64;
eprintln!("[robot] map-rect content ratio {body_ratio:.4}, outside-widget leak {margin_leak:.4}");
assert!(body_ratio > 0.5, "the viewer rect is painted (ratio {body_ratio:.4})");
assert!(margin_leak < 0.02, "viewer content confined to its rect (leak {margin_leak:.4})");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-geomap.robot_confined",
"non_blank_and_confined",
body_ratio > 0.5 && margin_leak < 0.02,
&format!("map_rect_ratio={body_ratio:.4} outside_leak={margin_leak:.4}"),
);
}