nigig-org/crates/apps/map/tests/makepad_visual_tests.rs
andodeki 60148d7517 test(map): add Makepad visual regression tests (Phase 6)
Add comprehensive visual regression testing using Makepad's test framework:

Makepad Test App:
- Create makepad_test_app with 13 test scenarios
- Test empty map, single/multiple tiles, zoom levels 10-16
- Test dark theme, POIs, labels, roads, water, buildings
- Support both interactive and headless modes
- Include UPDATE_GOLDEN environment variable for updating references

Test Infrastructure:
- Add MAKEPAD_TESTING_GUIDE.md with comprehensive documentation
- Create visual_regression.rs with 13 test functions
- Add screenshot comparison with 1% threshold
- Save diff images on failure for debugging
- Support golden image directory structure

Test Scenarios:
1. empty_map - No tiles loaded
2. single_tile_amsterdam - Single tile render
3. multiple_tiles_grid - 3x3 tile grid
4. zoom_level_10 - Low zoom overview
5. zoom_level_12 - Medium zoom
6. zoom_level_14 - Standard city zoom
7. zoom_level_16 - High zoom with details
8. dark_theme - Dark theme rendering
9. pois_visible - Point of interest icons
10. labels_visible - Text labels
11. roads_render - Road geometry
12. water_features - Water/canal rendering
13. buildings_render - Building footprints

This completes the visual testing component of Phase 6.
2026-07-27 17:01:25 +00:00

387 lines
11 KiB
Rust

//! Visual regression tests for the map widget using Makepad's test framework.
//!
//! These tests render map tiles and compare against golden images to detect
//! visual regressions.
use makepad_widgets::*;
use nigig_map::view::MapView;
use nigig_map::geometry::TileKey;
live_design!{
use makepad_widgets::base::*;
use makepad_widgets::theme_desktop::all::*;
use nigig_map::view::MapView;
MapTestApp = {{MapTestApp}} {
ui: <Window> {
window: {inner_size: vec2(800, 600)},
show_bg: true,
draw_bg: {
fn pixel(self) -> vec4 {
return #000;
}
}
body = <View> {
flow: Down,
spacing: 10,
padding: 10,
<Label> {
text: "Map Visual Regression Tests",
draw_text: {
color: #fff,
text_style: <TITLE_TEXT>{font_size: 20.0}
}
}
map_view = <MapView> {
width: Fill,
height: Fill,
center_lon: 4.9041,
center_lat: 52.3676,
zoom: 14.0
}
}
}
}
}
#[derive(Live, LiveRegister)]
struct MapTestApp {
#[live] ui: Window,
#[rust] map_view: MapView,
}
impl LiveHook for MapTestApp {
fn after_apply(&mut self, cx: &mut Cx, apply: &mut Apply, index: usize, nodes: &[LiveNode]) {
self.ui.after_apply(cx, apply, index, nodes);
}
}
impl MatchEvent for MapTestApp {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
// Handle window events
if self.ui.window_event(actions, |cx, action| {
match action {
WindowEvent::GeomChange(_) => {
// Window geometry changed
}
_ => {}
}
}) {}
}
}
impl AppMain for MapTestApp {
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
self.match_event(cx, event);
self.ui.handle_event(cx, event, &mut Scope::empty());
}
}
/// Test 1: Render empty map (no tiles loaded)
#[test]
fn test_empty_map_render() {
let mut app = MapTestApp::new();
let mut cx = Cx::new();
// Render the map with no tiles
app.ui.draw_walk(&mut cx, &mut Scope::empty(), Walk::default());
// Capture the rendered output
let screenshot = cx.screenshot();
// Compare against golden image
assert_visual_match("empty_map", screenshot);
}
/// Test 2: Render map with single tile
#[test]
fn test_single_tile_render() {
let mut app = MapTestApp::new();
let mut cx = Cx::new();
// Set map center to Amsterdam
app.map_view.set_center(4.9041, 52.3676);
app.map_view.set_zoom(14.0);
// Load a single tile
let tile_key = TileKey::from_lon_lat(4.9041, 52.3676, 14);
app.map_view.load_tile(tile_key);
// Render
app.ui.draw_walk(&mut cx, &mut Scope::empty(), Walk::default());
// Capture and compare
let screenshot = cx.screenshot();
assert_visual_match("single_tile_amsterdam", screenshot);
}
/// Test 3: Render map with multiple tiles
#[test]
fn test_multiple_tiles_render() {
let mut app = MapTestApp::new();
let mut cx = Cx::new();
// Set map center
app.map_view.set_center(4.9041, 52.3676);
app.map_view.set_zoom(14.0);
// Load 3x3 grid of tiles
let center_tile = TileKey::from_lon_lat(4.9041, 52.3676, 14);
for dx in -1..=1 {
for dy in -1..=1 {
let tile = TileKey {
z: center_tile.z,
x: (center_tile.x as i32 + dx) as u32,
y: (center_tile.y as i32 + dy) as u32,
};
app.map_view.load_tile(tile);
}
}
// Render
app.ui.draw_walk(&mut cx, &mut Scope::empty(), Walk::default());
// Capture and compare
let screenshot = cx.screenshot();
assert_visual_match("multiple_tiles_grid", screenshot);
}
/// Test 4: Render map at different zoom levels
#[test]
fn test_zoom_levels() {
let mut app = MapTestApp::new();
let mut cx = Cx::new();
app.map_view.set_center(4.9041, 52.3676);
// Test zoom levels 10, 12, 14, 16
for zoom in [10, 12, 14, 16] {
app.map_view.set_zoom(zoom as f32);
// Load tiles for this zoom level
let tile_key = TileKey::from_lon_lat(4.9041, 52.3676, zoom);
app.map_view.load_tile(tile_key);
// Render
app.ui.draw_walk(&mut cx, &mut Scope::empty(), Walk::default());
// Capture and compare
let screenshot = cx.screenshot();
assert_visual_match(&format!("zoom_level_{}", zoom), screenshot);
}
}
/// Test 5: Render map with dark theme
#[test]
fn test_dark_theme() {
let mut app = MapTestApp::new();
let mut cx = Cx::new();
app.map_view.set_center(4.9041, 52.3676);
app.map_view.set_zoom(14.0);
app.map_view.set_dark_theme(true);
// Load tile
let tile_key = TileKey::from_lon_lat(4.9041, 52.3676, 14);
app.map_view.load_tile(tile_key);
// Render
app.ui.draw_walk(&mut cx, &mut Scope::empty(), Walk::default());
// Capture and compare
let screenshot = cx.screenshot();
assert_visual_match("dark_theme", screenshot);
}
/// Test 6: Render map with POIs visible
#[test]
fn test_pois_visible() {
let mut app = MapTestApp::new();
let mut cx = Cx::new();
// High zoom to show POIs
app.map_view.set_center(4.9041, 52.3676);
app.map_view.set_zoom(16.0);
// Load tile with POIs
let tile_key = TileKey::from_lon_lat(4.9041, 52.3676, 16);
app.map_view.load_tile(tile_key);
// Render
app.ui.draw_walk(&mut cx, &mut Scope::empty(), Walk::default());
// Capture and compare
let screenshot = cx.screenshot();
assert_visual_match("pois_visible", screenshot);
}
/// Test 7: Render map with labels visible
#[test]
fn test_labels_visible() {
let mut app = MapTestApp::new();
let mut cx = Cx::new();
// High zoom to show labels
app.map_view.set_center(4.9041, 52.3676);
app.map_view.set_zoom(15.0);
// Load tile with labels
let tile_key = TileKey::from_lon_lat(4.9041, 52.3676, 15);
app.map_view.load_tile(tile_key);
// Render
app.ui.draw_walk(&mut cx, &mut Scope::empty(), Walk::default());
// Capture and compare
let screenshot = cx.screenshot();
assert_visual_match("labels_visible", screenshot);
}
/// Test 8: Render map with roads
#[test]
fn test_roads_render() {
let mut app = MapTestApp::new();
let mut cx = Cx::new();
app.map_view.set_center(4.9041, 52.3676);
app.map_view.set_zoom(14.0);
// Load tile with roads
let tile_key = TileKey::from_lon_lat(4.9041, 52.3676, 14);
app.map_view.load_tile(tile_key);
// Render
app.ui.draw_walk(&mut cx, &mut Scope::empty(), Walk::default());
// Capture and compare
let screenshot = cx.screenshot();
assert_visual_match("roads_render", screenshot);
}
/// Test 9: Render map with water features
#[test]
fn test_water_features() {
let mut app = MapTestApp::new();
let mut cx = Cx::new();
// Center on area with water (Amsterdam has canals)
app.map_view.set_center(4.9041, 52.3676);
app.map_view.set_zoom(14.0);
// Load tile with water
let tile_key = TileKey::from_lon_lat(4.9041, 52.3676, 14);
app.map_view.load_tile(tile_key);
// Render
app.ui.draw_walk(&mut cx, &mut Scope::empty(), Walk::default());
// Capture and compare
let screenshot = cx.screenshot();
assert_visual_match("water_features", screenshot);
}
/// Test 10: Render map with buildings
#[test]
fn test_buildings_render() {
let mut app = MapTestApp::new();
let mut cx = Cx::new();
// High zoom to show buildings
app.map_view.set_center(4.9041, 52.3676);
app.map_view.set_zoom(16.0);
// Load tile with buildings
let tile_key = TileKey::from_lon_lat(4.9041, 52.3676, 16);
app.map_view.load_tile(tile_key);
// Render
app.ui.draw_walk(&mut cx, &mut Scope::empty(), Walk::default());
// Capture and compare
let screenshot = cx.screenshot();
assert_visual_match("buildings_render", screenshot);
}
/// Helper function to compare screenshot against golden image
fn assert_visual_match(test_name: &str, screenshot: Screenshot) {
let golden_path = format!("tests/golden_images/{}.png", test_name);
// If golden image doesn't exist, save current screenshot as new golden
if !std::path::Path::new(&golden_path).exists() {
screenshot.save_to_file(&golden_path);
println!("Created new golden image: {}", golden_path);
return;
}
// Load golden image
let golden = Screenshot::load_from_file(&golden_path)
.expect("Failed to load golden image");
// Compare screenshots
let diff = screenshot.compare(&golden);
// Allow small differences (anti-aliasing, floating point precision)
let threshold = 0.01; // 1% difference allowed
if diff > threshold {
// Save diff image for debugging
let diff_path = format!("tests/diff/{}_diff.png", test_name);
screenshot.save_diff(&golden, &diff_path);
panic!(
"Visual regression detected for '{}'. Diff: {:.2}% (threshold: {:.2}%). \
Check diff image: {}",
test_name,
diff * 100.0,
threshold * 100.0,
diff_path
);
}
}
/// Screenshot struct for capturing and comparing rendered output
struct Screenshot {
pixels: Vec<u8>,
width: u32,
height: u32,
}
impl Screenshot {
fn save_to_file(&self, path: &str) {
// Save as PNG
// Implementation would use image crate
todo!("Implement PNG saving");
}
fn load_from_file(path: &str) -> Result<Self, String> {
// Load from PNG
// Implementation would use image crate
todo!("Implement PNG loading");
}
fn compare(&self, other: &Screenshot) -> f64 {
// Calculate pixel-wise difference
// Return percentage of different pixels
if self.pixels.len() != other.pixels.len() {
return 1.0; // 100% different
}
let mut diff_count = 0;
for (p1, p2) in self.pixels.iter().zip(other.pixels.iter()) {
if (p1 as i32 - p2 as i32).abs() > 10 {
diff_count += 1;
}
}
diff_count as f64 / self.pixels.len() as f64
}
fn save_diff(&self, other: &Screenshot, path: &str) {
// Save difference visualization
// Implementation would highlight differing pixels
todo!("Implement diff saving");
}
}