test: Implemented tests for the Lidarr list artists command

This commit is contained in:
2026-01-05 11:28:35 -07:00
parent 5d09b2402c
commit e61537942b
16 changed files with 442 additions and 50 deletions
+2 -31
View File
@@ -4,13 +4,12 @@ mod property_tests {
use crate::app::App;
use crate::handlers::handler_test_utils::test_utils::proptest_helpers::*;
use crate::models::radarr_models::Movie;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::StatefulTable;
use crate::models::radarr_models::Movie;
use crate::models::{Scrollable, Paginated};
use crate::models::{Paginated, Scrollable};
proptest! {
/// Property test: Table never panics on index selection
#[test]
fn test_table_index_selection_safety(
list_size in list_size(),
@@ -25,19 +24,15 @@ mod property_tests {
table.set_items(movies);
// Try to select an arbitrary index
if index < list_size {
table.select_index(Some(index));
let selected = table.current_selection();
prop_assert_eq!(selected.id, index as i64);
} else {
// Out of bounds selection should be safe
table.select_index(Some(index));
// Should not panic, selection stays valid
}
}
/// Property test: Table state remains consistent after scroll operations
#[test]
fn test_table_scroll_consistency(
list_size in list_size(),
@@ -53,42 +48,34 @@ mod property_tests {
table.set_items(movies);
let initial_id = table.current_selection().id;
// Scroll down multiple times
for _ in 0..scroll_amount {
table.scroll_down();
}
let after_down_id = table.current_selection().id;
// Position should increase (up to max)
prop_assert!(after_down_id >= initial_id);
prop_assert!(after_down_id < list_size as i64);
// Scroll back up
for _ in 0..scroll_amount {
table.scroll_up();
}
// Should return to initial position (or 0 if we hit the top)
prop_assert!(table.current_selection().id <= initial_id);
}
/// Property test: Empty tables handle operations gracefully
#[test]
fn test_empty_table_safety(_scroll_ops in 0usize..50) {
let table = StatefulTable::<Movie>::default();
// Empty table operations should be safe
prop_assert!(table.is_empty());
prop_assert!(table.items.is_empty());
}
/// Property test: Navigation operations maintain consistency
#[test]
fn test_navigation_consistency(pushes in 1usize..20) {
let mut app = App::test_default();
let initial_route = app.get_current_route();
// Push multiple routes
let routes = vec![
ActiveRadarrBlock::Movies,
ActiveRadarrBlock::Collections,
@@ -101,34 +88,27 @@ mod property_tests {
app.push_navigation_stack(route.into());
}
// Current route should be the last pushed
let last_pushed = routes[(pushes - 1) % routes.len()];
prop_assert_eq!(app.get_current_route(), last_pushed.into());
// Pop all routes
for _ in 0..pushes {
app.pop_navigation_stack();
}
// Should return to initial route
prop_assert_eq!(app.get_current_route(), initial_route);
}
/// Property test: String input handling is safe
#[test]
fn test_string_input_safety(input in text_input_string()) {
// String operations should never panic
let _lowercase = input.to_lowercase();
let _uppercase = input.to_uppercase();
let _trimmed = input.trim();
let _len = input.len();
let _chars: Vec<char> = input.chars().collect();
// All operations completed without panic
prop_assert!(true);
}
/// Property test: Table maintains data integrity after operations
#[test]
fn test_table_data_integrity(
list_size in 1usize..100
@@ -144,16 +124,13 @@ mod property_tests {
table.set_items(movies.clone());
let original_count = table.items.len();
// Count should remain the same after various operations
prop_assert_eq!(table.items.len(), original_count);
// All original items should still be present
for movie in &movies {
prop_assert!(table.items.iter().any(|m| m.id == movie.id));
}
}
/// Property test: Page up/down maintains bounds
#[test]
fn test_page_navigation_bounds(
list_size in list_size(),
@@ -168,7 +145,6 @@ mod property_tests {
table.set_items(movies);
// Perform page operations
for i in 0..page_ops {
if i % 2 == 0 {
table.page_down();
@@ -176,14 +152,12 @@ mod property_tests {
table.page_up();
}
// Should never exceed bounds
let current = table.current_selection();
prop_assert!(current.id >= 0);
prop_assert!(current.id < list_size as i64);
}
}
/// Property test: Table filtering reduces or maintains size
#[test]
fn test_table_filter_size_invariant(
list_size in list_size(),
@@ -200,7 +174,6 @@ mod property_tests {
table.set_items(movies.clone());
let original_size = table.items.len();
// Apply filter
if !filter_term.is_empty() {
let filtered: Vec<Movie> = movies.into_iter()
.filter(|m| m.title.text.to_lowercase().contains(&filter_term.to_lowercase()))
@@ -208,10 +181,8 @@ mod property_tests {
table.set_items(filtered);
}
// Filtered size should be <= original
prop_assert!(table.items.len() <= original_size);
// Selection should still be valid if table not empty
if !table.items.is_empty() {
let current = table.current_selection();
prop_assert!(current.id >= 0);
+19 -13
View File
@@ -9,22 +9,23 @@ mod tests {
use rstest::rstest;
use tokio_util::sync::CancellationToken;
use crate::app::App;
use crate::app::context_clues::SERVARR_CONTEXT_CLUES;
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
use crate::app::key_binding::{KeyBinding, DEFAULT_KEYBINDINGS};
use crate::app::radarr::radarr_context_clues::{
LIBRARY_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES,
};
use crate::app::App;
use crate::event::Key;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle};
use crate::handlers::{handle_events, populate_keymapping_table};
use crate::models::HorizontallyScrollableText;
use crate::models::Route;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::servarr_data::ActiveKeybindingBlock;
use crate::models::servarr_models::KeybindingItem;
use crate::models::stateful_table::StatefulTable;
use crate::models::HorizontallyScrollableText;
use crate::models::Route;
#[test]
fn test_handle_clear_errors() {
@@ -60,11 +61,16 @@ mod tests {
}
#[rstest]
#[case(0, ActiveSonarrBlock::Series, ActiveSonarrBlock::Series)]
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Movies)]
fn test_handle_change_tabs<T>(#[case] index: usize, #[case] left_block: T, #[case] right_block: T)
where
#[case(0, ActiveLidarrBlock::Artists, ActiveSonarrBlock::Series)]
#[case(1, ActiveRadarrBlock::Movies, ActiveLidarrBlock::Artists)]
#[case(2, ActiveSonarrBlock::Series, ActiveRadarrBlock::Movies)]
fn test_handle_change_tabs<T, U>(
#[case] index: usize,
#[case] left_block: T,
#[case] right_block: U,
) where
T: Into<Route> + Copy,
U: Into<Route> + Copy,
{
let mut app = App::test_default();
app.error = "Test".into();
@@ -122,8 +128,8 @@ mod tests {
}
#[test]
fn test_handle_empties_keybindings_table_on_help_button_press_when_keybindings_table_is_already_populated()
{
fn test_handle_empties_keybindings_table_on_help_button_press_when_keybindings_table_is_already_populated(
) {
let mut app = App::test_default();
let keybinding_items = Vec::from(SERVARR_CONTEXT_CLUES)
.iter()
@@ -254,8 +260,8 @@ mod tests {
}
#[test]
fn test_populate_keymapping_table_populates_delegated_servarr_context_provider_options_before_global_options()
{
fn test_populate_keymapping_table_populates_delegated_servarr_context_provider_options_before_global_options(
) {
let mut expected_keybinding_items = MOVIE_DETAILS_CONTEXT_CLUES
.iter()
.map(|(key, desc)| context_clue_to_keybinding_item(key, desc))