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.
387 lines
11 KiB
Rust
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");
|
|
}
|
|
}
|