1277 lines
41 KiB
Rust
1277 lines
41 KiB
Rust
//! Integration tests for the cost estimator module.
|
||
//!
|
||
//! Tests the full workflow: initialize data → load rooms → manipulate store → compute totals.
|
||
//! Also tests screen_actions logic paths, floor multiplier, export summary, and cross-module workflows.
|
||
|
||
use nigig_build::construction_frame::pages::workspace::cost_estimator::data_raw::{
|
||
calculate_property_cost, format_cost, get_building_data, get_default_rooms, BuildingCategory,
|
||
BuildingCostData, PropertyDefaultRooms, Room, RoomSizeBand,
|
||
};
|
||
use nigig_build::construction_frame::pages::workspace::cost_estimator::room_controller::{
|
||
build_size_bands_lookup, get_rooms_by_category, lookup_rate, update_rates_in_place,
|
||
};
|
||
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),
|
||
..Room::default()
|
||
}
|
||
}
|
||
|
||
fn make_room_with_count(name: &str, length: f64, width: f64, rate: f64, count: u32) -> Room {
|
||
Room {
|
||
room_type: name.into(),
|
||
length,
|
||
width,
|
||
rate,
|
||
count: Some(count),
|
||
..Room::default()
|
||
}
|
||
}
|
||
|
||
// ── Full workflow tests ───────────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn full_workflow_initialize_load_store_totals() {
|
||
let data = get_building_data();
|
||
|
||
let rooms = get_rooms_by_category(
|
||
&data,
|
||
&BuildingCategory::Residential,
|
||
Some("Nairobi"),
|
||
true,
|
||
Some("Standard Bungalow"),
|
||
);
|
||
assert!(
|
||
!rooms.is_empty(),
|
||
"Should load rooms for residential Nairobi bungalow"
|
||
);
|
||
|
||
let mut store = RoomStore::new();
|
||
for room in rooms {
|
||
store.add(room);
|
||
}
|
||
assert!(store.len() > 0);
|
||
|
||
for (_, room) in store.iter() {
|
||
assert!(
|
||
room.rate > 0.0,
|
||
"Room '{}' should have a positive rate",
|
||
room.room_type
|
||
);
|
||
}
|
||
|
||
assert!(store.total_area() > 0.0, "Total area should be positive");
|
||
assert!(store.total_cost() > 0.0, "Total cost should be positive");
|
||
}
|
||
|
||
#[test]
|
||
fn workflow_add_remove_rooms() {
|
||
let mut store = RoomStore::new();
|
||
|
||
let idx1 = store.add(make_room("Bedroom", 4.0, 3.0, 20_000.0));
|
||
let idx2 = store.add(make_room("Kitchen", 3.0, 3.0, 25_000.0));
|
||
let idx3 = store.add(make_room("Bathroom", 2.0, 2.0, 30_000.0));
|
||
|
||
assert_eq!(store.len(), 3);
|
||
|
||
assert!(store.remove(idx2));
|
||
assert_eq!(store.len(), 2);
|
||
|
||
assert_eq!(store.get(0).unwrap().room_type, "Bedroom");
|
||
assert_eq!(store.get(1).unwrap().room_type, "Bathroom");
|
||
|
||
assert_eq!(store.total_cost(), 360_000.0);
|
||
}
|
||
|
||
#[test]
|
||
fn workflow_update_all_rates() {
|
||
let mut store = RoomStore::new();
|
||
store.add(make_room("A", 3.0, 3.0, 0.0));
|
||
store.add(make_room("B", 4.0, 4.0, 0.0));
|
||
|
||
assert_eq!(store.total_cost(), 0.0);
|
||
|
||
update_rates_in_place(&mut store, 10_000.0);
|
||
|
||
assert_eq!(store.total_cost(), 250_000.0);
|
||
}
|
||
|
||
#[test]
|
||
fn workflow_format_cost() {
|
||
assert_eq!(format_cost(1_500_000.0), "1,500,000.00");
|
||
assert_eq!(format_cost(123_456.78), "123,456.78");
|
||
assert_eq!(format_cost(0.0), "0.00");
|
||
}
|
||
|
||
#[test]
|
||
fn workflow_lookup_rate() {
|
||
let data = get_building_data();
|
||
|
||
let rate = lookup_rate(
|
||
&data,
|
||
&BuildingCategory::Residential,
|
||
"Nairobi",
|
||
"Standard Bungalow",
|
||
);
|
||
let rate = rate.expect("Should find rate for residential Nairobi bungalow");
|
||
assert!(rate > 0.0);
|
||
|
||
let area = 150.0;
|
||
let expected_cost = rate * area;
|
||
assert!(expected_cost > 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn workflow_default_rooms() {
|
||
let data = get_building_data();
|
||
let default = get_default_rooms(&data, "Standard Bungalow");
|
||
assert!(
|
||
default.is_some(),
|
||
"Should have default rooms for Standard Bungalow"
|
||
);
|
||
let rooms = &default.unwrap().rooms;
|
||
assert!(!rooms.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn workflow_store_display_filtering() {
|
||
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("Bedroom", 3.5, 3.5, 22_000.0));
|
||
store.add(make_room("Bathroom", 2.0, 2.0, 30_000.0));
|
||
|
||
store.rebuild_display_indices();
|
||
assert_eq!(store.displayed_indices().len(), 4);
|
||
|
||
store.remove(1);
|
||
store.rebuild_display_indices();
|
||
assert_eq!(store.displayed_indices().len(), 3);
|
||
|
||
store.clear();
|
||
assert!(store.is_empty());
|
||
assert_eq!(store.total_cost(), 0.0);
|
||
}
|
||
|
||
// ── Floor multiplier logic tests ──────────────────────────────────────
|
||
|
||
fn apply_floor_multiplier(
|
||
area: Option<f64>,
|
||
cost: Option<f64>,
|
||
storey_type: &StoreyType,
|
||
num_floors: u32,
|
||
) -> (Option<f64>, Option<f64>) {
|
||
if *storey_type == StoreyType::NonStorey || num_floors <= 1 {
|
||
return (area, cost);
|
||
}
|
||
let multiplier = num_floors as f64;
|
||
(area.map(|a| a * multiplier), cost.map(|c| c * multiplier))
|
||
}
|
||
|
||
#[test]
|
||
fn floor_multiplier_non_storey_no_change() {
|
||
let area = Some(100.0);
|
||
let cost = Some(2_000_000.0);
|
||
let (a, c) = apply_floor_multiplier(area, cost, &StoreyType::NonStorey, 1);
|
||
assert_eq!(a, Some(100.0));
|
||
assert_eq!(c, Some(2_000_000.0));
|
||
}
|
||
|
||
#[test]
|
||
fn floor_multiplier_single_floor_no_change() {
|
||
let area = Some(100.0);
|
||
let cost = Some(2_000_000.0);
|
||
let (a, c) = apply_floor_multiplier(area, cost, &StoreyType::LowRise, 1);
|
||
assert_eq!(a, Some(100.0));
|
||
assert_eq!(c, Some(2_000_000.0));
|
||
}
|
||
|
||
#[test]
|
||
fn floor_multiplier_low_rise_3_floors() {
|
||
let area = Some(100.0);
|
||
let cost = Some(2_000_000.0);
|
||
let (a, c) = apply_floor_multiplier(area, cost, &StoreyType::LowRise, 3);
|
||
assert!((a.unwrap() - 300.0).abs() < f64::EPSILON);
|
||
assert!((c.unwrap() - 6_000_000.0).abs() < f64::EPSILON);
|
||
}
|
||
|
||
#[test]
|
||
fn floor_multiplier_high_rise_10_floors() {
|
||
let area = Some(50.0);
|
||
let cost = Some(1_000_000.0);
|
||
let (a, c) = apply_floor_multiplier(area, cost, &StoreyType::HighRise, 10);
|
||
assert!((a.unwrap() - 500.0).abs() < f64::EPSILON);
|
||
assert!((c.unwrap() - 10_000_000.0).abs() < f64::EPSILON);
|
||
}
|
||
|
||
#[test]
|
||
fn floor_multiplier_none_values_stay_none() {
|
||
let (a, c) = apply_floor_multiplier(None, None, &StoreyType::HighRise, 5);
|
||
assert!(a.is_none());
|
||
assert!(c.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn floor_multiplier_mixed_some_none() {
|
||
let (a, c) = apply_floor_multiplier(Some(100.0), None, &StoreyType::LowRise, 2);
|
||
assert!((a.unwrap() - 200.0).abs() < f64::EPSILON);
|
||
assert!(c.is_none());
|
||
}
|
||
|
||
// ── All building categories ───────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn all_categories_have_regions_and_rooms() {
|
||
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 mut store = RoomStore::new();
|
||
for room in rooms {
|
||
store.add(room);
|
||
}
|
||
assert!(store.total_area() >= 0.0);
|
||
assert!(store.total_cost() >= 0.0);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn all_categories_lookup_rates() {
|
||
let data = get_building_data();
|
||
let categories = [
|
||
BuildingCategory::Residential,
|
||
BuildingCategory::Commercial,
|
||
BuildingCategory::Retail,
|
||
BuildingCategory::Industrial,
|
||
];
|
||
for cat in &categories {
|
||
let residential_data = data.get(cat);
|
||
if let Some(entries) = residential_data {
|
||
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,
|
||
"Rate for {} should be positive",
|
||
prop.property_type
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Export summary format ─────────────────────────────────────────────
|
||
|
||
fn export_summary(
|
||
category: &BuildingCategory,
|
||
property_type: &str,
|
||
region: &str,
|
||
storey_type: &StoreyType,
|
||
num_floors: u32,
|
||
area: Option<f64>,
|
||
cost: Option<f64>,
|
||
) -> String {
|
||
let floors_info = if storey_type.shows_floor_input() {
|
||
format!(
|
||
"\nStorey Type: {}\nFloors: {}",
|
||
storey_type.as_label(),
|
||
num_floors
|
||
)
|
||
} else {
|
||
format!("\nStorey Type: {}", storey_type.as_label())
|
||
};
|
||
|
||
format!(
|
||
"Cost Estimate\nCategory: {}\nProperty: {}\nRegion: {}{}\nArea: {}\nCost: {}",
|
||
category.as_label(),
|
||
property_type,
|
||
region,
|
||
floors_info,
|
||
area.map(|v| format!("{} m² / {} ft²", format_cost(v), format_cost(v * 10.763_91)))
|
||
.unwrap_or_else(|| "--".to_string()),
|
||
cost.map(|v| format!("Ksh {}", format_cost(v)))
|
||
.unwrap_or_else(|| "Ksh --".to_string()),
|
||
)
|
||
}
|
||
|
||
#[test]
|
||
fn export_summary_non_storey() {
|
||
let summary = export_summary(
|
||
&BuildingCategory::Residential,
|
||
"Standard Bungalow",
|
||
"Nairobi",
|
||
&StoreyType::NonStorey,
|
||
1,
|
||
Some(150.0),
|
||
Some(3_000_000.0),
|
||
);
|
||
assert!(summary.contains("Category: Residential"));
|
||
assert!(summary.contains("Property: Standard Bungalow"));
|
||
assert!(summary.contains("Region: Nairobi"));
|
||
assert!(summary.contains("Storey Type: Non-Storey"));
|
||
assert!(!summary.contains("Floors:"));
|
||
assert!(summary.contains("150.00"));
|
||
assert!(summary.contains("Ksh"));
|
||
}
|
||
|
||
#[test]
|
||
fn export_summary_low_rise_with_floors() {
|
||
let summary = export_summary(
|
||
&BuildingCategory::Residential,
|
||
"Maisonette",
|
||
"Mombasa",
|
||
&StoreyType::LowRise,
|
||
3,
|
||
Some(200.0),
|
||
Some(6_000_000.0),
|
||
);
|
||
assert!(summary.contains("Storey Type: Low Rise"));
|
||
assert!(summary.contains("Floors: 3"));
|
||
assert!(summary.contains("200.00"));
|
||
assert!(summary.contains("6,000,000.00"));
|
||
}
|
||
|
||
#[test]
|
||
fn export_summary_no_data() {
|
||
let summary = export_summary(
|
||
&BuildingCategory::Industrial,
|
||
"Warehouse",
|
||
"Kisumu",
|
||
&StoreyType::NonStorey,
|
||
1,
|
||
None,
|
||
None,
|
||
);
|
||
assert!(summary.contains("Category: Industrial"));
|
||
assert!(summary.contains("--"));
|
||
assert!(summary.contains("Ksh --"));
|
||
}
|
||
|
||
// ── Room with count multiplier ────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn room_count_multiplies_area_and_cost() {
|
||
let mut store = RoomStore::new();
|
||
store.add(make_room_with_count("Bedroom", 4.0, 3.0, 20_000.0, 3));
|
||
|
||
// area = 4*3*3 = 36
|
||
assert!((store.total_area() - 36.0).abs() < f64::EPSILON);
|
||
// cost = 36 * 20000 = 720000
|
||
assert_eq!(store.total_cost(), 720_000.0);
|
||
}
|
||
|
||
#[test]
|
||
fn mixed_count_rooms_totals() {
|
||
let mut store = RoomStore::new();
|
||
store.add(make_room_with_count("Bedroom", 4.0, 3.0, 20_000.0, 2)); // 24 m² → 480000
|
||
store.add(make_room("Kitchen", 3.0, 3.0, 25_000.0)); // 9 m² → 225000
|
||
store.add(make_room_with_count("Bathroom", 2.0, 2.0, 30_000.0, 2)); // 8 m² → 480000
|
||
|
||
assert!((store.total_area() - 65.0).abs() < f64::EPSILON);
|
||
assert_eq!(store.total_cost(), 1_185_000.0);
|
||
}
|
||
|
||
// ── Size bands and classification ─────────────────────────────────────
|
||
|
||
#[test]
|
||
fn size_bands_lookup_for_multiple_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);
|
||
assert!(
|
||
!lookup.is_empty(),
|
||
"Should have size bands for residential rooms"
|
||
);
|
||
|
||
for (room_type, bands) in &lookup {
|
||
assert!(
|
||
!bands.is_empty(),
|
||
"Room type '{}' should have bands",
|
||
room_type
|
||
);
|
||
for band in bands {
|
||
assert!(!band.size_category.is_empty());
|
||
assert!(band.length > 0.0);
|
||
assert!(band.width > 0.0);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── StoreyType classification integration ─────────────────────────────
|
||
|
||
#[test]
|
||
fn storey_classification_for_all_property_types() {
|
||
let data = get_building_data();
|
||
let categories = [
|
||
BuildingCategory::Residential,
|
||
BuildingCategory::Commercial,
|
||
BuildingCategory::Retail,
|
||
BuildingCategory::Industrial,
|
||
];
|
||
|
||
let mut all_property_types = Vec::new();
|
||
for cat in &categories {
|
||
if let Some(entries) = data.get(cat) {
|
||
for entry in entries {
|
||
let rooms = get_rooms_by_category(&data, cat, None, false, None);
|
||
for room in &rooms {
|
||
let st = StoreyType::classify_property(&room.room_type);
|
||
assert!(matches!(
|
||
st,
|
||
StoreyType::NonStorey | StoreyType::LowRise | StoreyType::HighRise
|
||
));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn storey_type_from_all_indices() {
|
||
assert_eq!(StoreyType::from_index(0), StoreyType::NonStorey);
|
||
assert_eq!(StoreyType::from_index(1), StoreyType::LowRise);
|
||
assert_eq!(StoreyType::from_index(2), StoreyType::HighRise);
|
||
assert_eq!(StoreyType::from_index(3), StoreyType::NonStorey);
|
||
assert_eq!(StoreyType::from_index(100), StoreyType::NonStorey);
|
||
assert_eq!(StoreyType::from_index(usize::MAX), StoreyType::NonStorey);
|
||
}
|
||
|
||
// ── Edge cases ────────────────────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn empty_store_totals_are_zero() {
|
||
let store = RoomStore::new();
|
||
assert_eq!(store.total_area(), 0.0);
|
||
assert_eq!(store.total_cost(), 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn store_remove_all_rooms_one_by_one() {
|
||
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));
|
||
|
||
assert!(store.remove(0));
|
||
assert!(store.remove(0));
|
||
assert!(store.remove(0));
|
||
assert!(store.is_empty());
|
||
assert_eq!(store.total_cost(), 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn zero_rate_rooms_contribute_zero_cost() {
|
||
let mut store = RoomStore::new();
|
||
store.add(make_room("A", 10.0, 10.0, 0.0));
|
||
store.add(make_room("B", 5.0, 5.0, 0.0));
|
||
assert_eq!(store.total_cost(), 0.0);
|
||
assert_eq!(store.total_area(), 125.0);
|
||
}
|
||
|
||
#[test]
|
||
fn format_cost_various_values() {
|
||
assert_eq!(format_cost(0.0), "0.00");
|
||
assert_eq!(format_cost(1.0), "1.00");
|
||
assert_eq!(format_cost(999.0), "999.00");
|
||
assert_eq!(format_cost(1000.0), "1,000.00");
|
||
assert_eq!(format_cost(1234567.89), "1,234,567.89");
|
||
assert_eq!(format_cost(-1000.0), "-1,000.00");
|
||
}
|
||
|
||
// ── Cross-module: data → controller → store ──────────────────────────
|
||
|
||
#[test]
|
||
fn cross_module_data_controller_store_workflow() {
|
||
let data = get_building_data();
|
||
|
||
let categories = [BuildingCategory::Residential, BuildingCategory::Commercial];
|
||
|
||
for cat in &categories {
|
||
let rooms = get_rooms_by_category(&data, cat, None, false, None);
|
||
if rooms.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
let mut store = RoomStore::new();
|
||
for room in &rooms {
|
||
store.add(room.clone());
|
||
}
|
||
|
||
assert!(store.len() > 0);
|
||
store.rebuild_display_indices();
|
||
assert_eq!(store.displayed_indices().len(), store.len());
|
||
|
||
if let Some(first) = store.get(0) {
|
||
if first.rate > 0.0 {
|
||
let cost =
|
||
first.length * first.width * first.rate * first.count.unwrap_or(1) as f64;
|
||
assert!(cost > 0.0);
|
||
}
|
||
}
|
||
|
||
let total_area = store.total_area();
|
||
let total_cost = store.total_cost();
|
||
assert!(total_area >= 0.0);
|
||
assert!(total_cost >= 0.0);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn calculate_property_cost_for_all_residential_types() {
|
||
let data = get_building_data();
|
||
let residential = data.get(&BuildingCategory::Residential).unwrap();
|
||
|
||
for entry in residential {
|
||
for region in &entry.regions {
|
||
for bt in ®ion.building_types {
|
||
for prop in &bt.properties {
|
||
let (area, cost) = calculate_property_cost(
|
||
entry,
|
||
&prop.property_type,
|
||
®ion.name,
|
||
&bt.category,
|
||
);
|
||
if let Some(a) = area {
|
||
assert!(
|
||
a > 0.0,
|
||
"Area for {} should be positive",
|
||
prop.property_type
|
||
);
|
||
}
|
||
if let Some(c) = cost {
|
||
assert!(
|
||
c > 0.0,
|
||
"Cost for {} should be positive",
|
||
prop.property_type
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Room rate update propagation ──────────────────────────────────────
|
||
|
||
#[test]
|
||
fn rate_update_changes_total_proportionally() {
|
||
let mut store = RoomStore::new();
|
||
store.add(make_room("A", 5.0, 5.0, 10_000.0)); // 25 * 10000 = 250000
|
||
store.add(make_room("B", 3.0, 4.0, 20_000.0)); // 12 * 20000 = 240000
|
||
|
||
let cost_before = store.total_cost();
|
||
|
||
update_rates_in_place(&mut store, 50_000.0);
|
||
|
||
let cost_after = store.total_cost();
|
||
// A: 25*50000=1,250,000 B: 12*50000=600,000 → total=1,850,000
|
||
assert_eq!(cost_after, 1_850_000.0);
|
||
assert!(cost_after > cost_before);
|
||
}
|
||
|
||
#[test]
|
||
fn rate_update_to_zero_resets_cost() {
|
||
let mut store = RoomStore::new();
|
||
store.add(make_room("A", 5.0, 5.0, 10_000.0));
|
||
store.add(make_room("B", 3.0, 4.0, 20_000.0));
|
||
|
||
assert!(store.total_cost() > 0.0);
|
||
update_rates_in_place(&mut store, 0.0);
|
||
assert_eq!(store.total_cost(), 0.0);
|
||
}
|
||
|
||
// ── Estimate + Command pipeline integration tests ────────────────────
|
||
|
||
use nigig_build::construction_frame::pages::workspace::cost_estimator::estimate::{
|
||
Command, DomainEvent, Estimate,
|
||
};
|
||
use nigig_build::construction_frame::pages::workspace::cost_estimator::estimate_store::EstimateStore;
|
||
use nigig_build::construction_frame::pages::workspace::cost_estimator::lookup::LookupIndex;
|
||
use nigig_build::construction_frame::pages::workspace::cost_estimator::services;
|
||
use nigig_build::construction_frame::pages::workspace::cost_estimator::types::{
|
||
PropertyType, RegionName,
|
||
};
|
||
|
||
#[test]
|
||
fn estimate_dispatch_initialize_events() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut e = Estimate::new();
|
||
let events = e.execute_command(Command::Initialize, data, &idx);
|
||
assert!(events.contains(&DomainEvent::SelectionChanged));
|
||
assert!(events.contains(&DomainEvent::FloorsChanged));
|
||
assert!(events.contains(&DomainEvent::TotalsChanged));
|
||
assert!(events.contains(&DomainEvent::RegionListChanged));
|
||
assert!(events.contains(&DomainEvent::PropertyListChanged));
|
||
assert!(events.contains(&DomainEvent::RoomTypesChanged));
|
||
}
|
||
|
||
#[test]
|
||
fn estimate_dispatch_add_remove_room_roundtrip() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut e = Estimate::new();
|
||
e.execute_command(Command::Initialize, data, &idx);
|
||
|
||
let room = Room {
|
||
room_type: "Bedroom".into(),
|
||
length: 4.0,
|
||
width: 3.0,
|
||
rate: 20_000.0,
|
||
count: Some(1),
|
||
..Room::default()
|
||
};
|
||
|
||
let events = e.execute_command(Command::AddRoom(room), data, &idx);
|
||
assert!(events
|
||
.iter()
|
||
.any(|ev| matches!(ev, DomainEvent::RoomAdded(0))));
|
||
assert_eq!(e.rooms.len(), 1);
|
||
|
||
let events = e.execute_command(Command::RemoveRoom(0), data, &idx);
|
||
assert!(events
|
||
.iter()
|
||
.any(|ev| matches!(ev, DomainEvent::RoomRemoved(0))));
|
||
assert_eq!(e.rooms.len(), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn estimate_selected_data_available_after_init() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut e = Estimate::new();
|
||
e.execute_command(Command::Initialize, data, &idx);
|
||
|
||
let regions = e.region_labels(&idx);
|
||
if let Some(r) = regions.first() {
|
||
e.execute_command(Command::SetRegion(r.clone().into()), data, &idx);
|
||
let bd = e.selected_data(data);
|
||
assert!(bd.is_some());
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn estimate_compute_totals_after_full_selection() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut e = Estimate::new();
|
||
e.execute_command(Command::Initialize, data, &idx);
|
||
|
||
let regions = e.region_labels(&idx);
|
||
if let Some(r) = regions.first() {
|
||
e.execute_command(Command::SetRegion(r.clone().into()), data, &idx);
|
||
}
|
||
let props = e.filtered_property_types(&idx);
|
||
if let Some(p) = props.first() {
|
||
e.execute_command(Command::SetProperty(p.clone().into()), data, &idx);
|
||
}
|
||
|
||
let totals = e.compute_totals(data);
|
||
assert!(totals.property_area_m2.is_some());
|
||
assert!(totals.property_cost.is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn estimate_export_summary_after_selection() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut e = Estimate::new();
|
||
e.execute_command(Command::Initialize, data, &idx);
|
||
|
||
let regions = e.region_labels(&idx);
|
||
if let Some(r) = regions.first() {
|
||
e.execute_command(Command::SetRegion(r.clone().into()), data, &idx);
|
||
}
|
||
|
||
let totals = e.compute_totals(data);
|
||
let summary = e.export_summary(data, &totals);
|
||
assert!(summary.contains("Cost Estimate"));
|
||
assert!(summary.contains("Ksh"));
|
||
}
|
||
|
||
#[test]
|
||
fn estimate_store_dispatch_matches_direct_execute() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut est = Estimate::new();
|
||
let mut store = EstimateStore::new();
|
||
|
||
est.execute_command(Command::Initialize, data, &idx);
|
||
let store_events = store.dispatch(Command::Initialize);
|
||
|
||
assert_eq!(est.selected_category, store.estimate.selected_category);
|
||
assert_eq!(est.num_floors, store.estimate.num_floors);
|
||
assert_eq!(est.rooms.len(), store.estimate.rooms.len());
|
||
assert_eq!(store_events.len(), 6);
|
||
}
|
||
|
||
// ── Index-backed vs data-backed service cross-validation ─────────────
|
||
|
||
#[test]
|
||
fn index_vs_data_list_regions_match() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let categories = [
|
||
BuildingCategory::Residential,
|
||
BuildingCategory::Commercial,
|
||
BuildingCategory::Retail,
|
||
BuildingCategory::Industrial,
|
||
];
|
||
for cat in &categories {
|
||
let data_regions = services::list_regions(data, cat);
|
||
let index_regions = services::list_regions_via_index(&idx, cat);
|
||
assert_eq!(
|
||
data_regions.len(),
|
||
index_regions.len(),
|
||
"Region count mismatch for {:?}",
|
||
cat
|
||
);
|
||
for r in &data_regions {
|
||
assert!(
|
||
index_regions.contains(r),
|
||
"Index missing region '{}' for {:?}",
|
||
r,
|
||
cat
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn index_vs_data_property_types_match() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let categories = [
|
||
BuildingCategory::Residential,
|
||
BuildingCategory::Commercial,
|
||
BuildingCategory::Retail,
|
||
BuildingCategory::Industrial,
|
||
];
|
||
for cat in &categories {
|
||
let data_regions = services::list_regions(data, cat);
|
||
for region in &data_regions {
|
||
let data_props = services::property_types_for_region(data, cat, region);
|
||
let index_props = services::property_types_via_index(&idx, cat, region);
|
||
assert_eq!(
|
||
data_props.len(),
|
||
index_props.len(),
|
||
"Property count mismatch for {:?}/{}",
|
||
cat,
|
||
region
|
||
);
|
||
for p in &data_props {
|
||
assert!(
|
||
index_props.contains(p),
|
||
"Index missing property '{}' for {:?}/{}",
|
||
p,
|
||
cat,
|
||
region
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn index_vs_data_compute_rate_matches() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let categories = [BuildingCategory::Residential, BuildingCategory::Commercial];
|
||
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 data_rate = services::compute_rate(
|
||
entry,
|
||
®ion.name,
|
||
&bt.category,
|
||
&prop.property_type,
|
||
);
|
||
let index_rate = services::compute_rate_via_index(
|
||
&idx,
|
||
&bt.category,
|
||
®ion.name,
|
||
&prop.property_type,
|
||
);
|
||
assert!(
|
||
(data_rate - index_rate).abs() < f64::EPSILON,
|
||
"Rate mismatch for {} in {}/{}: data={}, index={}",
|
||
prop.property_type,
|
||
bt.category,
|
||
region.name,
|
||
data_rate,
|
||
index_rate
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn index_vs_data_ensure_valid_region_matches() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let categories = [BuildingCategory::Residential, BuildingCategory::Commercial];
|
||
for cat in &categories {
|
||
let data_result = services::ensure_valid_region(data, cat, "nonexistent_region");
|
||
let index_result = services::ensure_valid_region_via_index(&idx, cat, "nonexistent_region");
|
||
assert_eq!(
|
||
data_result, index_result,
|
||
"ensure_valid_region mismatch for {:?}",
|
||
cat
|
||
);
|
||
|
||
let data_result = services::ensure_valid_region(data, cat, &data_result);
|
||
let index_result = services::ensure_valid_region_via_index(&idx, cat, &index_result);
|
||
assert_eq!(
|
||
data_result, index_result,
|
||
"ensure_valid_region (valid) mismatch for {:?}",
|
||
cat
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Newtype integration in Command flow ──────────────────────────────
|
||
|
||
#[test]
|
||
fn region_name_newtype_in_command_roundtrip() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut e = Estimate::new();
|
||
e.execute_command(Command::Initialize, data, &idx);
|
||
|
||
let r: RegionName = "Nairobi".into();
|
||
e.execute_command(Command::SetRegion(r), data, &idx);
|
||
assert_eq!(e.selected_region.as_str(), "Nairobi");
|
||
}
|
||
|
||
#[test]
|
||
fn property_type_newtype_in_command_roundtrip() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut e = Estimate::new();
|
||
e.execute_command(Command::Initialize, data, &idx);
|
||
|
||
let regions = e.region_labels(&idx);
|
||
if let Some(r) = regions.first() {
|
||
e.execute_command(Command::SetRegion(r.clone().into()), data, &idx);
|
||
}
|
||
let props = e.filtered_property_types(&idx);
|
||
if let Some(p) = props.first() {
|
||
e.execute_command(Command::SetProperty(p.clone().into()), data, &idx);
|
||
assert_eq!(e.selected_property.as_str(), p.as_str());
|
||
}
|
||
}
|
||
|
||
// ── Export service integration tests ────────────────────────────────
|
||
|
||
use nigig_build::construction_frame::pages::workspace::cost_estimator::data_raw::Room;
|
||
|
||
#[test]
|
||
fn export_csv_full_workflow() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
|
||
let regions = store.estimate.region_labels(store.index());
|
||
if let Some(r) = regions.first() {
|
||
store.dispatch(Command::SetRegion(r.clone().into()));
|
||
}
|
||
let props = store.estimate.filtered_property_types(store.index());
|
||
if let Some(p) = props.first() {
|
||
store.dispatch(Command::SetProperty(p.clone().into()));
|
||
}
|
||
|
||
store.dispatch(Command::AddRoom(Room {
|
||
room_type: "Bedroom".into(),
|
||
size_category: "Medium".into(),
|
||
length: 4.0,
|
||
width: 3.0,
|
||
count: Some(2),
|
||
rate: 20_000.0,
|
||
..Room::default()
|
||
}));
|
||
|
||
let totals = store.compute_totals();
|
||
let csv = store.export_csv(&totals);
|
||
|
||
assert!(csv.contains("Header,Category,"));
|
||
assert!(csv.contains("Room,Bedroom,Medium,"));
|
||
assert!(csv.contains("Room,Type,Size,Length,Width,Count,Rate,Cost"));
|
||
}
|
||
|
||
#[test]
|
||
fn export_json_full_workflow() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
|
||
let regions = store.estimate.region_labels(store.index());
|
||
if let Some(r) = regions.first() {
|
||
store.dispatch(Command::SetRegion(r.clone().into()));
|
||
}
|
||
let props = store.estimate.filtered_property_types(store.index());
|
||
if let Some(p) = props.first() {
|
||
store.dispatch(Command::SetProperty(p.clone().into()));
|
||
}
|
||
|
||
store.dispatch(Command::AddRoom(Room {
|
||
room_type: "Kitchen".into(),
|
||
size_category: "Small".into(),
|
||
length: 3.0,
|
||
width: 3.0,
|
||
count: Some(1),
|
||
rate: 25_000.0,
|
||
..Room::default()
|
||
}));
|
||
|
||
let totals = store.compute_totals();
|
||
let json = store.export_json(&totals);
|
||
|
||
assert!(json.contains("\"category\":"));
|
||
assert!(json.contains("\"rooms\":"));
|
||
assert!(json.contains("\"type\": \"Kitchen\""));
|
||
assert!(json.contains("\"cost\":"));
|
||
}
|
||
|
||
#[test]
|
||
fn export_formats_consistent_totals() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
|
||
let regions = store.estimate.region_labels(store.index());
|
||
if let Some(r) = regions.first() {
|
||
store.dispatch(Command::SetRegion(r.clone().into()));
|
||
}
|
||
let props = store.estimate.filtered_property_types(store.index());
|
||
if let Some(p) = props.first() {
|
||
store.dispatch(Command::SetProperty(p.clone().into()));
|
||
}
|
||
|
||
store.dispatch(Command::AddRoom(Room {
|
||
room_type: "Bedroom".into(),
|
||
size_category: "Medium".into(),
|
||
length: 4.0,
|
||
width: 3.0,
|
||
count: Some(2),
|
||
rate: 20_000.0,
|
||
..Room::default()
|
||
}));
|
||
|
||
let totals = store.compute_totals();
|
||
let summary = store.export_summary(&totals);
|
||
let csv = store.export_csv(&totals);
|
||
let json = store.export_json(&totals);
|
||
|
||
// All three formats should contain the same cost value
|
||
if let Some(cost) = totals.property_cost {
|
||
let cost_str = format_cost(cost);
|
||
assert!(summary.contains(&cost_str), "Summary should contain cost");
|
||
assert!(csv.contains(&cost_str), "CSV should contain cost");
|
||
assert!(
|
||
json.contains(&format!("{:.2}", cost)),
|
||
"JSON should contain cost"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn bench_lookup_index_build() {
|
||
let data = get_building_data();
|
||
let start = std::time::Instant::now();
|
||
let idx = LookupIndex::build(data);
|
||
let elapsed = start.elapsed();
|
||
eprintln!("LookupIndex::build: {:?}", elapsed);
|
||
// Sanity: index should contain data
|
||
let residential_regions = idx.regions_for_category(&BuildingCategory::Residential);
|
||
assert!(!residential_regions.is_empty());
|
||
// Should complete in < 50ms.
|
||
assert!(
|
||
elapsed.as_millis() < 200,
|
||
"LookupIndex::build took {:?} — expected < 200ms",
|
||
elapsed
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn bench_estimate_store_dispatch_initialize() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
let mut e = Estimate::new();
|
||
|
||
let start = std::time::Instant::now();
|
||
for _ in 0..100 {
|
||
e.execute_command(Command::Initialize, data, &idx);
|
||
}
|
||
let elapsed = start.elapsed();
|
||
eprintln!("100× Estimate::execute_command(Initialize): {:?}", elapsed);
|
||
}
|
||
|
||
#[test]
|
||
fn bench_full_workflow_100_iterations() {
|
||
let data = get_building_data();
|
||
let idx = LookupIndex::build(data);
|
||
|
||
let start = std::time::Instant::now();
|
||
for _ in 0..100 {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
|
||
let regions = store.estimate.region_labels(store.index());
|
||
if let Some(r) = regions.first() {
|
||
store.dispatch(Command::SetRegion(r.clone().into()));
|
||
}
|
||
let props = store.estimate.filtered_property_types(store.index());
|
||
if let Some(p) = props.first() {
|
||
store.dispatch(Command::SetProperty(p.clone().into()));
|
||
}
|
||
store.dispatch(Command::SetStorey(
|
||
nigig_build::construction_frame::pages::workspace::cost_estimator::screen_state::StoreyType::LowRise,
|
||
));
|
||
store.dispatch(Command::SetFloorCount(3));
|
||
|
||
store.dispatch(Command::AddRoom(make_room("Bedroom", 4.0, 3.0, 20_000.0)));
|
||
store.dispatch(Command::AddRoom(make_room("Kitchen", 3.0, 3.0, 25_000.0)));
|
||
store.dispatch(Command::AddRoom(make_room("Bathroom", 2.0, 2.0, 30_000.0)));
|
||
|
||
let totals = store.compute_totals();
|
||
let _ = store.export_summary(&totals);
|
||
}
|
||
let elapsed = start.elapsed();
|
||
eprintln!(
|
||
"100× full workflow (init→select→add rooms→totals→export): {:?}",
|
||
elapsed
|
||
);
|
||
}
|
||
|
||
// ── UpdateRoomDimensions integration tests ──────────────────────────────
|
||
|
||
#[test]
|
||
fn update_room_dimensions_roundtrip() {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
store.dispatch(Command::AddRoom(make_room("Bedroom", 4.0, 3.0, 20_000.0)));
|
||
|
||
let events = store.dispatch(Command::UpdateRoomDimensions {
|
||
index: 0,
|
||
length: 6.0,
|
||
width: 5.0,
|
||
});
|
||
assert!(events
|
||
.iter()
|
||
.any(|e| matches!(e, DomainEvent::RoomUpdated(0))));
|
||
assert!(events
|
||
.iter()
|
||
.any(|e| matches!(e, DomainEvent::TotalsChanged)));
|
||
|
||
let room = store.estimate.rooms.get(0).unwrap();
|
||
assert_eq!(room.length, 6.0);
|
||
assert_eq!(room.width, 5.0);
|
||
}
|
||
|
||
#[test]
|
||
fn update_room_dimensions_out_of_bounds_returns_no_events() {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
|
||
let events = store.dispatch(Command::UpdateRoomDimensions {
|
||
index: 0,
|
||
length: 5.0,
|
||
width: 4.0,
|
||
});
|
||
assert!(events.is_empty());
|
||
|
||
let events = store.dispatch(Command::UpdateRoomDimensions {
|
||
index: 999,
|
||
length: 5.0,
|
||
width: 4.0,
|
||
});
|
||
assert!(events.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn update_room_dimensions_zero_is_valid() {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
store.dispatch(Command::AddRoom(make_room("Bedroom", 4.0, 3.0, 20_000.0)));
|
||
|
||
store.dispatch(Command::UpdateRoomDimensions {
|
||
index: 0,
|
||
length: 0.0,
|
||
width: 0.0,
|
||
});
|
||
let room = store.estimate.rooms.get(0).unwrap();
|
||
assert_eq!(room.length, 0.0);
|
||
assert_eq!(room.width, 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn update_room_dimensions_negative_values_accepted() {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
store.dispatch(Command::AddRoom(make_room("Bedroom", 4.0, 3.0, 20_000.0)));
|
||
|
||
store.dispatch(Command::UpdateRoomDimensions {
|
||
index: 0,
|
||
length: -5.0,
|
||
width: -3.0,
|
||
});
|
||
let room = store.estimate.rooms.get(0).unwrap();
|
||
assert_eq!(room.length, -5.0);
|
||
assert_eq!(room.width, -3.0);
|
||
}
|
||
|
||
#[test]
|
||
fn update_room_dimensions_extreme_values() {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
store.dispatch(Command::AddRoom(make_room("Bedroom", 4.0, 3.0, 20_000.0)));
|
||
|
||
store.dispatch(Command::UpdateRoomDimensions {
|
||
index: 0,
|
||
length: f64::MAX,
|
||
width: f64::MAX,
|
||
});
|
||
let room = store.estimate.rooms.get(0).unwrap();
|
||
assert_eq!(room.length, f64::MAX);
|
||
assert_eq!(room.width, f64::MAX);
|
||
}
|
||
|
||
#[test]
|
||
fn update_room_dimensions_multiple_rooms_independently() {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
store.dispatch(Command::AddRoom(make_room("Bedroom", 4.0, 3.0, 20_000.0)));
|
||
store.dispatch(Command::AddRoom(make_room("Kitchen", 3.0, 3.0, 25_000.0)));
|
||
store.dispatch(Command::AddRoom(make_room("Bathroom", 2.0, 2.0, 30_000.0)));
|
||
|
||
store.dispatch(Command::UpdateRoomDimensions {
|
||
index: 1,
|
||
length: 5.0,
|
||
width: 4.0,
|
||
});
|
||
|
||
let bedroom = store.estimate.rooms.get(0).unwrap();
|
||
let kitchen = store.estimate.rooms.get(1).unwrap();
|
||
let bathroom = store.estimate.rooms.get(2).unwrap();
|
||
|
||
assert_eq!(bedroom.length, 4.0);
|
||
assert_eq!(bedroom.width, 3.0);
|
||
assert_eq!(kitchen.length, 5.0);
|
||
assert_eq!(kitchen.width, 4.0);
|
||
assert_eq!(bathroom.length, 2.0);
|
||
assert_eq!(bathroom.width, 2.0);
|
||
}
|
||
|
||
// ── Undo/Redo integration tests ─────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn undo_redo_update_room_dimensions() {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
store.dispatch(Command::AddRoom(make_room("Bedroom", 4.0, 3.0, 20_000.0)));
|
||
|
||
store.dispatch(Command::UpdateRoomDimensions {
|
||
index: 0,
|
||
length: 6.0,
|
||
width: 5.0,
|
||
});
|
||
assert_eq!(store.estimate.rooms.get(0).unwrap().length, 6.0);
|
||
|
||
store.dispatch(Command::Undo);
|
||
assert_eq!(store.estimate.rooms.get(0).unwrap().length, 4.0);
|
||
assert_eq!(store.estimate.rooms.get(0).unwrap().width, 3.0);
|
||
|
||
store.dispatch(Command::Redo);
|
||
assert_eq!(store.estimate.rooms.get(0).unwrap().length, 6.0);
|
||
assert_eq!(store.estimate.rooms.get(0).unwrap().width, 5.0);
|
||
}
|
||
|
||
#[test]
|
||
fn undo_add_room_then_undo_update_dimensions() {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
store.dispatch(Command::AddRoom(make_room("Bedroom", 4.0, 3.0, 20_000.0)));
|
||
store.dispatch(Command::UpdateRoomDimensions {
|
||
index: 0,
|
||
length: 6.0,
|
||
width: 5.0,
|
||
});
|
||
|
||
store.dispatch(Command::Undo);
|
||
assert_eq!(store.estimate.rooms.get(0).unwrap().length, 4.0);
|
||
|
||
store.dispatch(Command::Undo);
|
||
assert_eq!(store.estimate.rooms.len(), 0);
|
||
|
||
store.dispatch(Command::Redo);
|
||
assert_eq!(store.estimate.rooms.len(), 1);
|
||
assert_eq!(store.estimate.rooms.get(0).unwrap().length, 4.0);
|
||
|
||
store.dispatch(Command::Redo);
|
||
assert_eq!(store.estimate.rooms.get(0).unwrap().length, 6.0);
|
||
}
|
||
|
||
#[test]
|
||
fn new_mutation_after_undo_clears_redo() {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
store.dispatch(Command::SetFloorCount(5));
|
||
store.dispatch(Command::Undo);
|
||
assert!(store.can_redo());
|
||
|
||
store.dispatch(Command::SetFloorCount(10));
|
||
assert!(!store.can_redo());
|
||
assert_eq!(store.estimate.num_floors, 10);
|
||
|
||
store.dispatch(Command::Undo);
|
||
assert_eq!(store.estimate.num_floors, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn undo_empty_stack_returns_empty_events() {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
let events = store.dispatch(Command::Undo);
|
||
assert!(events.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn redo_empty_stack_returns_empty_events() {
|
||
let mut store = EstimateStore::new();
|
||
store.dispatch(Command::Initialize);
|
||
let events = store.dispatch(Command::Redo);
|
||
assert!(events.is_empty());
|
||
}
|