facett-map/tests/empty_pane_gestures.rs
//! **An empty map pane is still a control** (stinky `seam-17`).
//!
//! `MapView::view` allocates its canvas with `Sense::click_and_drag()` and then — in
//! exactly the two states where a user most wants to move the camera, nothing loaded
//! and a degenerate envelope — returned before it read a single gesture. A drag went
//! nowhere. A wheel notch went nowhere. The only way out of an empty view was a control
//! outside the pane.
//!
//! It is the same defect `MapFacet::ui` records against itself one widget over ("korp's
//! call site says 'persist the camera the user drove' — the user could not drive it"),
//! and it survived because `Sense::click_and_drag()` is the part everyone checks.
//!
//! # What is asserted
//!
//! The **messages the widget returns**, which is its applied output: `view` is an ELM
//! view, it mutates nothing, and `Vec<Msg>` is the entire effect it has on the world.
//! A `resp.dragged()` assertion would be a state round-trip on egui, not on this code.
//!
//! The pane is driven through a real `egui::Context` with real `RawInput` — a press,
//! then a move — so the drag is egui's own, not a simulated flag.
//!
//! Run: `cargo test -p facett-map --test empty_pane_gestures`
use egui::{Modifiers, Pos2, RawInput, Rect, Vec2, pos2, vec2};
use facett_map::{MapView, Msg};
/// Drive `MapView::view` across several frames and hand back the LAST frame's messages.
///
/// egui resolves an interaction at the START of a frame against the rects the PREVIOUS
/// frame registered, so a gesture needs a warm-up: one frame to lay the widget out, one
/// to put the pointer on it, one to press, one to move. Fewer frames and the widget has
/// no interaction to report — which is what the control test in this file exists to
/// catch, and did on the first draft.
fn drive(map: &MapView, frames: Vec<Vec<egui::Event>>) -> Vec<Msg> {
let ctx = egui::Context::default();
let mut last = Vec::new();
// Warm-up: lay the widget out so the next frame has a rect to hit-test against.
for _ in 0..2 {
let input = RawInput {
screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))),
..Default::default()
};
ctx.run_ui(input, |ui| {
let _ = map.view(ui);
}).textures_delta.clear();
}
for events in frames {
let input = RawInput {
screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))),
events,
..Default::default()
};
let mut msgs = Vec::new();
ctx.run_ui(input, |ui| {
msgs = map.view(ui);
}).textures_delta.clear();
last = msgs;
}
last
}
/// Somewhere inside the pane. The widget is laid out under a header row, so this sits
/// well down the available space.
const OVER_PANE: Pos2 = pos2(400.0, 400.0);
#[test]
fn an_empty_pane_still_pans_and_zooms() {
// No points, no ways — the `empty` arm, which paints "no geo data" and used to
// return before reading any gesture.
let map = MapView::new("empty", Vec::new());
// ── DRAG: press, then move. ───────────────────────────────────────────────
let drag = drive(
&map,
vec![
vec![
egui::Event::PointerMoved(OVER_PANE),
egui::Event::PointerButton {
pos: OVER_PANE,
button: egui::PointerButton::Primary,
pressed: true,
modifiers: Modifiers::default(),
},
],
vec![egui::Event::PointerMoved(OVER_PANE + Vec2::new(40.0, -25.0))],
vec![egui::Event::PointerMoved(OVER_PANE + Vec2::new(70.0, -40.0))],
],
);
let panned: Vec<[f32; 2]> = drag
.iter()
.filter_map(|m| if let Msg::PanBy(d) = m { Some(*d) } else { None })
.collect();
assert!(
!panned.is_empty(),
"a drag over an EMPTY pane must return a PanBy — it returned {drag:?}. The pane senses \
click_and_drag and then dropped the gesture, so the only way out of an empty view was a \
control outside the map."
);
assert!(
panned.iter().any(|d| d[0] != 0.0 || d[1] != 0.0),
"and the delta must be the real one, not a zero: {panned:?}"
);
// ── WHEEL ────────────────────────────────────────────────────────────────
let wheel = drive(
&map,
vec![
vec![egui::Event::PointerMoved(OVER_PANE)],
vec![
egui::Event::PointerMoved(OVER_PANE),
egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: vec2(0.0, 50.0),
modifiers: Modifiers::default(),
phase: egui::TouchPhase::Move,
},
],
],
);
let zoomed: Vec<f32> = wheel
.iter()
.filter_map(|m| if let Msg::ZoomBy(d) = m { Some(*d) } else { None })
.collect();
assert!(
!zoomed.is_empty(),
"a wheel notch over an EMPTY pane must return a ZoomBy — it returned {wheel:?}"
);
assert!(zoomed.iter().any(|d| *d != 0.0), "and the delta must be real: {zoomed:?}");
eprintln!("[seam-17] empty pane: PanBy {panned:?}, ZoomBy {zoomed:?}");
nornir_testmatrix::functional_status(
"facett-map",
"empty_pane_still_pans_and_zooms",
!panned.is_empty() && !zoomed.is_empty(),
&format!("PanBy {panned:?}, ZoomBy {zoomed:?}"),
);
}
/// **NON-VACUOUS CONTROL.** The same gestures on a POPULATED pane have always worked —
/// so if this one ever fails the harness is broken, not the fix, and the assertion above
/// is not proving anything about `seam-17` at all.
#[test]
fn a_populated_pane_pans_too_so_the_empty_case_is_a_real_comparison() {
let map = MapView::new("full", vec![(18.06, 59.33), (17.64, 59.85), (12.0, 57.7)]);
let drag = drive(
&map,
vec![
vec![
egui::Event::PointerMoved(OVER_PANE),
egui::Event::PointerButton {
pos: OVER_PANE,
button: egui::PointerButton::Primary,
pressed: true,
modifiers: Modifiers::default(),
},
],
vec![egui::Event::PointerMoved(OVER_PANE + Vec2::new(40.0, -25.0))],
vec![egui::Event::PointerMoved(OVER_PANE + Vec2::new(70.0, -40.0))],
],
);
assert!(
drag.iter().any(|m| matches!(m, Msg::PanBy(_))),
"the control case must pan: {drag:?}"
);
}