Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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