#[cfg(test)] mod property_tests { use proptest::prelude::*; use crate::app::App; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::event::Key; use crate::handlers::handle_events; use crate::handlers::handler_test_utils::test_utils::proptest_helpers::{ list_size, text_input_string, }; use crate::models::radarr_models::Movie; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::stateful_table::StatefulTable; use crate::models::{Paginated, Scrollable, strip_non_search_characters}; proptest! { #[test] fn test_table_index_selection_safety( list_size in list_size(), index in 0usize..1000 ) { let mut table = StatefulTable::::default(); let movies: Vec = (0..list_size) .map(|i| Movie { id: i as i64, ..Movie::default() }) .collect(); table.set_items(movies); if index < list_size { table.select_index(Some(index)); let selected = table.current_selection(); prop_assert_eq!(selected.id, index as i64); } else { table.select_index(Some(index)); } } #[test] fn test_table_scroll_consistency( list_size in list_size(), scroll_amount in 0usize..20 ) { let mut table = StatefulTable::::default(); let movies: Vec = (0..list_size) .map(|i| Movie { id: i as i64, ..Movie::default() }) .collect(); table.set_items(movies); let initial_id = table.current_selection().id; for _ in 0..scroll_amount { table.scroll_down(); } let after_down_id = table.current_selection().id; prop_assert!(after_down_id >= initial_id); prop_assert!(after_down_id < list_size as i64); for _ in 0..scroll_amount { table.scroll_up(); } prop_assert!(table.current_selection().id <= initial_id); } #[test] fn test_empty_table_safety(_scroll_ops in 0usize..50) { let table = StatefulTable::::default(); prop_assert!(table.is_empty()); prop_assert!(table.items.is_empty()); } #[test] fn test_navigation_consistency(pushes in 1usize..20) { let mut app = App::test_default(); let initial_route = app.get_current_route(); let routes = [ ActiveRadarrBlock::Movies, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::Blocklist, ]; for i in 0..pushes { let route = routes[i % routes.len()]; app.push_navigation_stack(route.into()); } let last_pushed = routes[(pushes - 1) % routes.len()]; prop_assert_eq!(app.get_current_route(), last_pushed.into()); for _ in 0..pushes { app.pop_navigation_stack(); } prop_assert_eq!(app.get_current_route(), initial_route); } #[test] fn test_string_input_safety(input in text_input_string()) { let mut app = App::test_default(); app.data.radarr_data.movies.set_items(vec![Movie::default()]); app.push_navigation_stack(ActiveRadarrBlock::Movies.into()); handle_events(DEFAULT_KEYBINDINGS.filter.key, &mut app); for character in input.chars() { handle_events(Key::Char(character), &mut app); } prop_assert_eq!(app.get_current_route(), ActiveRadarrBlock::FilterMovies.into()); prop_assert_eq!( app.data.radarr_data.movies.filter.as_ref().unwrap().text.as_str(), input.as_str() ); for _ in 0..input.chars().count() { handle_events(DEFAULT_KEYBINDINGS.backspace.key, &mut app); } prop_assert_eq!( app.data.radarr_data.movies.filter.as_ref().unwrap().text.as_str(), "" ); } #[test] fn test_table_data_integrity( list_size in list_size() ) { let mut table = StatefulTable::::default(); let movies: Vec = (0..list_size) .map(|i| Movie { id: i as i64, title: format!("Movie {i}").into(), ..Movie::default() }) .collect(); table.set_items(movies.clone()); let original_count = table.items.len(); prop_assert_eq!(table.items.len(), original_count); for movie in &movies { prop_assert!(table.items.iter().any(|m| m.id == movie.id)); } } #[test] fn test_page_navigation_bounds( list_size in list_size(), page_ops in 0usize..10 ) { let mut table = StatefulTable::::default(); let movies: Vec = (0..list_size) .map(|i| Movie { id: i as i64, ..Movie::default() }) .collect(); table.set_items(movies); for i in 0..page_ops { let previous_index = table.state.selected().unwrap(); if i % 2 == 0 { table.page_down(); prop_assert_eq!( table.state.selected(), Some((previous_index + 20).min(list_size - 1)) ); } else { table.page_up(); prop_assert_eq!(table.state.selected(), Some(previous_index.saturating_sub(20))); } let current = table.current_selection(); prop_assert!(current.id >= 0); prop_assert!(current.id < list_size as i64); } } #[test] fn test_table_filter_size_invariant( list_size in list_size(), filter_term in text_input_string() ) { let mut table = StatefulTable::::default(); let movies: Vec = (0..list_size) .map(|i| Movie { id: i as i64, title: format!("Test Movie {}", i % 10).into(), ..Movie::default() }) .collect(); table.set_items(movies.clone()); table.filter = Some(filter_term.clone().into()); let scrubbed_filter = strip_non_search_characters(&filter_term); let matches_filter = |movie: &Movie| { strip_non_search_characters(&movie.title.text).contains(&scrubbed_filter) }; let has_match = table.apply_filter(|movie| &movie.title.text); prop_assert_eq!(&table.items, &movies); prop_assert!(table.filter.is_none()); if has_match { let filtered_items = table.filtered_items.as_ref().unwrap(); prop_assert!(!filtered_items.is_empty()); prop_assert!(filtered_items.len() <= movies.len()); for movie in filtered_items { prop_assert!(matches_filter(movie)); } for movie in movies.iter().filter(|movie| !filtered_items.contains(movie)) { prop_assert!(!matches_filter(movie)); } } else { prop_assert!(table.filtered_items.is_none()); prop_assert!( filter_term.is_empty() || movies.iter().all(|movie| !matches_filter(movie)) ); } } } }