//! UI integration tests for the cost estimator widget. //! //! Full widget rendering tests require the makepad test harness (window + event loop). //! See the placeholder comment block in cad_ui.rs for framework details. //! //! These tests exercise data logic that backs the widget without requiring //! a makepad App context. They validate StoreyType classification, room list //! population, dropdown option generation, modal data, and cost formatting. use nigig_build::construction_frame::pages::workspace::cost_estimator::data_raw::{ format_cost, get_building_data, BuildingCategory, Room, }; use nigig_build::construction_frame::pages::workspace::cost_estimator::room_controller::{ build_size_bands_lookup, get_rooms_by_category, lookup_rate, }; use nigig_build::construction_frame::pages::workspace::cost_estimator::room_store::RoomStore; use nigig_build::construction_frame::pages::workspace::cost_estimator::screen_state::StoreyType; fn make_room(name: &str, length: f64, width: f64, rate: f64) -> Room { Room { room_type: name.into(), length, width, rate, count: Some(1), ..Default::default() } } // ── Dropdown option generation ──────────────────────────────────────── #[test] fn dropdown_options_for_residential() { let data = get_building_data(); let regions: Vec = data .values() .flat_map(|list| list.iter()) .flat_map(|bd| bd.regions.iter()) .map(|r| r.name.clone()) .collect::>() .into_iter() .collect(); assert!(!regions.is_empty(), "Should have at least one region"); let categories = [ BuildingCategory::Residential, BuildingCategory::Commercial, BuildingCategory::Industrial, ]; for cat in &categories { let rooms = get_rooms_by_category(&data, cat, regions.first().map(|s| s.as_str()), false, None); let _ = rooms; } } #[test] fn all_categories_have_at_least_one_region() { let data = get_building_data(); let categories = [ BuildingCategory::Residential, BuildingCategory::Commercial, BuildingCategory::Retail, BuildingCategory::Industrial, ]; for cat in &categories { if let Some(entries) = data.get(cat) { let regions: Vec = entries .iter() .flat_map(|e| e.regions.iter()) .map(|r| r.name.clone()) .collect::>() .into_iter() .collect(); if !regions.is_empty() { assert!( regions.len() >= 1, "Category {:?} should have at least 1 region", cat ); } } } } #[test] fn dropdown_options_for_all_categories_with_regions() { let data = get_building_data(); let categories = [ BuildingCategory::Residential, BuildingCategory::Commercial, BuildingCategory::Retail, BuildingCategory::Industrial, ]; for cat in &categories { let rooms = get_rooms_by_category(&data, cat, None, false, None); if !rooms.is_empty() { let property_types: Vec = rooms .iter() .map(|r| r.room_type.clone()) .collect::>() .into_iter() .collect(); assert!( property_types.len() >= 1, "Category {:?} should have at least 1 room type", cat ); } } } // ── StoreyType UI labels match enum ─────────────────────────────────── #[test] fn storey_type_ui_labels_match_enum() { let options = ["Non-Storey", "Low Rise", "High Rise"]; for (i, expected) in options.iter().enumerate() { assert_eq!(StoreyType::from_index(i).as_label(), *expected); } } #[test] fn storey_type_all_variants_have_labels() { let variants = [ StoreyType::NonStorey, StoreyType::LowRise, StoreyType::HighRise, ]; for v in &variants { let label = v.as_label(); assert!(!label.is_empty(), "StoreyType label should not be empty"); } } #[test] fn storey_type_default_floors_consistent() { assert!(StoreyType::NonStorey.default_floors() >= 1); assert!(StoreyType::LowRise.default_floors() >= 2); assert!(StoreyType::HighRise.default_floors() >= 5); } #[test] fn storey_type_shows_floor_input_consistent() { assert!(!StoreyType::NonStorey.shows_floor_input()); assert!(StoreyType::LowRise.shows_floor_input()); assert!(StoreyType::HighRise.shows_floor_input()); } // ── Room list population ────────────────────────────────────────────── #[test] fn room_list_populated_from_data() { let data = get_building_data(); let rooms = get_rooms_by_category( &data, &BuildingCategory::Residential, Some("Nairobi"), true, Some("Standard Bungalow"), ); let mut store = RoomStore::new(); for room in rooms { store.add(room); } assert!(store.len() > 0); store.rebuild_display_indices(); assert_eq!(store.displayed_indices().len(), store.len()); } #[test] fn room_list_rooms_have_positive_dimensions() { let data = get_building_data(); let rooms = get_rooms_by_category( &data, &BuildingCategory::Residential, Some("Nairobi"), false, Some("Standard Bungalow"), ); for room in &rooms { assert!( room.length >= 0.0, "Room '{}' length should be non-negative", room.room_type ); assert!( room.width >= 0.0, "Room '{}' width should be non-negative", room.room_type ); } } #[test] fn room_list_for_commercial_category() { let data = get_building_data(); let rooms = get_rooms_by_category( &data, &BuildingCategory::Commercial, None, false, None, ); if !rooms.is_empty() { let mut store = RoomStore::new(); for room in rooms { store.add(room); } assert!(store.len() > 0); assert!(store.total_area() >= 0.0); } } // ── Cost preview formatting ─────────────────────────────────────────── #[test] fn cost_preview_formats_correctly() { let preview = format_cost(2_500_000.0); assert_eq!(preview, "2,500,000.00"); let preview = format_cost(99.9); assert_eq!(preview, "99.90"); } #[test] fn cost_preview_format_zero() { assert_eq!(format_cost(0.0), "0.00"); } #[test] fn cost_preview_format_small() { assert_eq!(format_cost(42.5), "42.50"); } #[test] fn cost_preview_format_large() { assert_eq!(format_cost(99_999_999.99), "99,999,999.99"); } #[test] fn cost_preview_format_negative() { assert_eq!(format_cost(-1234.56), "-1,234.56"); } #[test] fn cost_preview_format_with_commas() { assert_eq!(format_cost(1_234_567.89), "1,234,567.89"); } // ── Modal room type list ────────────────────────────────────────────── #[test] fn modal_room_type_list_matches_data() { use std::collections::HashSet; let data = get_building_data(); let room_types: HashSet = data .values() .flat_map(|list| list.iter()) .flat_map(|bd| bd.default_rooms.iter()) .flat_map(|dr| dr.rooms.iter()) .map(|r| r.room_type.clone()) .collect(); assert!(room_types.contains("Bedroom")); assert!( room_types.len() >= 3, "Should have several distinct room types" ); } #[test] fn modal_room_types_have_default_dimensions() { let data = get_building_data(); let residential = data.get(&BuildingCategory::Residential).unwrap(); for entry in residential { for pdr in &entry.default_rooms { for room in &pdr.rooms { if room.length > 0.0 && room.width > 0.0 { assert!( room.length > 0.0 && room.width > 0.0, "Room '{}' should have positive dimensions if populated", room.room_type ); } } } } } #[test] fn modal_has_size_bands_for_room_types() { let data = get_building_data(); let residential = data.get(&BuildingCategory::Residential).unwrap(); let first = &residential[0]; let lookup = build_size_bands_lookup(&first.room_sizes); let mut room_types_with_bands = 0; for (room_type, bands) in &lookup { if !bands.is_empty() { room_types_with_bands += 1; assert!( bands.len() >= 2, "Room '{}' should have at least 2 size bands", room_type ); } } assert!( room_types_with_bands >= 1, "At least 1 room type should have size bands" ); } // ── Storey classification for property types ────────────────────────── #[test] fn classify_property_standard_variants() { let high_rise_inputs = [ "Apartment Block", "Office Tower", "High Rise", "Skyscraper", "Multi-Storey", ]; for input in &high_rise_inputs { assert_eq!( StoreyType::classify_property(input), StoreyType::HighRise, "'{}' should classify as HighRise", input ); } let low_rise_inputs = [ "Maisonette", "Townhouse", "Duplex", "Two Storey", "Terrace House", ]; for input in &low_rise_inputs { assert_eq!( StoreyType::classify_property(input), StoreyType::LowRise, "'{}' should classify as LowRise", input ); } let non_storey_inputs = ["Standard Bungalow", "Cottage", "Villa", "Studio"]; for input in &non_storey_inputs { assert_eq!( StoreyType::classify_property(input), StoreyType::NonStorey, "'{}' should classify as NonStorey", input ); } } #[test] fn classify_property_case_insensitive() { assert_eq!( StoreyType::classify_property("APARTMENT BLOCK"), StoreyType::HighRise ); assert_eq!( StoreyType::classify_property("maisonette"), StoreyType::LowRise ); assert_eq!( StoreyType::classify_property("BOULANGERIE"), StoreyType::NonStorey ); } #[test] fn classify_property_empty_string() { assert_eq!( StoreyType::classify_property(""), StoreyType::NonStorey ); } // ── Region + property type filtering ────────────────────────────────── #[test] fn property_types_by_region_consistency() { let data = get_building_data(); let categories = [ BuildingCategory::Residential, BuildingCategory::Commercial, BuildingCategory::Retail, BuildingCategory::Industrial, ]; for cat in &categories { if let Some(entries) = data.get(cat) { for entry in entries { for region in &entry.regions { let rooms = get_rooms_by_category( &data, cat, Some(®ion.name), false, None, ); if !rooms.is_empty() { let mut store = RoomStore::new(); for room in rooms { store.add(room); } assert!(store.total_area() >= 0.0); } } } } } } // ── Rate lookup edge cases ──────────────────────────────────────────── #[test] fn lookup_rate_nonexistent_region() { let data = get_building_data(); let rate = lookup_rate( &data, &BuildingCategory::Residential, "NonexistentRegion", "Standard Bungalow", ); assert!(rate.is_none()); } #[test] fn lookup_rate_nonexistent_property() { let data = get_building_data(); let rate = lookup_rate( &data, &BuildingCategory::Residential, "Nairobi", "NonexistentProperty", ); assert!(rate.is_none()); } #[test] fn lookup_rate_all_categories() { let data = get_building_data(); let categories = [ BuildingCategory::Residential, BuildingCategory::Commercial, BuildingCategory::Retail, BuildingCategory::Industrial, ]; for cat in &categories { if let Some(entries) = data.get(cat) { for entry in entries { for region in &entry.regions { for bt in ®ion.building_types { for prop in &bt.properties { let rate = lookup_rate( &data, cat, ®ion.name, &prop.property_type, ); if let Some(r) = rate { assert!(r > 0.0); } } } } } } } } // ── Room store + classification integration ─────────────────────────── #[test] fn room_store_with_classified_rooms() { let mut store = RoomStore::new(); store.add(make_room("Bedroom", 4.0, 3.0, 20_000.0)); store.add(make_room("Kitchen", 3.0, 3.0, 25_000.0)); store.add(make_room("Bathroom", 2.0, 2.0, 30_000.0)); let total_area = store.total_area(); let total_cost = store.total_cost(); assert!((total_area - 25.0).abs() < f64::EPSILON); assert_eq!(total_cost, 240_000.0 + 225_000.0 + 120_000.0); } #[test] fn store_rebuild_display_indices_after_multiple_operations() { let mut store = RoomStore::new(); store.add(make_room("A", 1.0, 1.0, 100.0)); store.add(make_room("B", 2.0, 2.0, 200.0)); store.add(make_room("C", 3.0, 3.0, 300.0)); store.add(make_room("D", 4.0, 4.0, 400.0)); store.remove(1); // remove B store.remove(1); // remove C (was at index 2, now at 1) store.rebuild_display_indices(); assert_eq!(store.displayed_indices().len(), 2); assert_eq!(store.get(0).unwrap().room_type, "A"); assert_eq!(store.get(1).unwrap().room_type, "D"); } // ── Data completeness checks ────────────────────────────────────────── #[test] fn building_data_json_completeness() { let data = get_building_data(); let mut total_entries = 0; let mut total_regions = 0; for (cat, entries) in &data { total_entries += entries.len(); for entry in entries { total_regions += entry.regions.len(); assert!( !entry.room_sizes.is_empty(), "Entry for {:?} should have room_sizes", cat ); assert!( !entry.default_rooms.is_empty(), "Entry for {:?} should have default_rooms", cat ); assert!( !entry.meta.currency.is_empty(), "Entry for {:?} should have currency", cat ); } } assert!(total_entries > 0, "Should have building data entries"); assert!(total_regions > 0, "Should have regions"); } #[test] fn all_regions_have_building_types() { let data = get_building_data(); for (_cat, entries) in &data { for entry in entries { for region in &entry.regions { assert!( !region.building_types.is_empty(), "Region '{}' should have building types", region.name ); for bt in ®ion.building_types { assert!( !bt.properties.is_empty(), "Building type '{}' in region '{}' should have properties", bt.category, region.name ); } } } } } #[test] fn property_costs_are_consistent() { let data = get_building_data(); for (_cat, entries) in &data { for entry in entries { for region in &entry.regions { for bt in ®ion.building_types { for prop in &bt.properties { if let Some(m2_cost) = prop.cost_per_square_metre { assert!(m2_cost > 0, "cost_per_square_metre for '{}' should be > 0", prop.property_type); } if let Some(ft2_cost) = prop.cost_per_square_foot { assert!(ft2_cost > 0, "cost_per_square_foot for '{}' should be > 0", prop.property_type); } } } } } } } // ── StoreyType default clone ────────────────────────────────────────── #[test] fn storey_type_clone_and_default() { let st = StoreyType::default(); assert_eq!(st, StoreyType::NonStorey); let cloned = StoreyType::HighRise; assert_eq!(cloned, StoreyType::HighRise); }