From 23b1ca43717f84f3f41e1944a83b0c2f6ffd4918 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 6 Dec 2024 20:30:26 -0700 Subject: [PATCH] feat(ui): Sonarr support for the series details popup --- Cargo.lock | 20 + Cargo.toml | 2 + src/app/sonarr/mod.rs | 8 +- src/app/sonarr/sonarr_context_clues.rs | 18 + src/app/sonarr/sonarr_context_clues_tests.rs | 53 +- src/app/sonarr/sonarr_tests.rs | 16 +- src/handlers/handler_test_utils.rs | 5 + .../library/movie_details_handler.rs | 2 +- .../library/movie_details_handler_tests.rs | 3 +- src/handlers/sonarr_handlers/history/mod.rs | 2 +- .../library/library_handler_tests.rs | 59 +- src/handlers/sonarr_handlers/library/mod.rs | 7 + .../library/series_details_handler.rs | 610 +++++ .../library/series_details_handler_tests.rs | 1971 +++++++++++++++++ .../sonarr_handler_test_utils.rs | 6 +- src/models/servarr_data/sonarr/sonarr_data.rs | 12 +- .../servarr_data/sonarr/sonarr_data_tests.rs | 26 +- .../servarr_data/sonarr/sonarr_test_utils.rs | 3 + src/models/sonarr_models.rs | 1 + src/network/sonarr_network_tests.rs | 6 +- src/ui/radarr_ui/library/movie_details_ui.rs | 1 - src/ui/sonarr_ui/library/edit_series_ui.rs | 2 +- src/ui/sonarr_ui/library/library_ui_tests.rs | 214 +- src/ui/sonarr_ui/library/mod.rs | 65 +- src/ui/sonarr_ui/library/series_details_ui.rs | 54 +- src/ui/styles.rs | 5 + src/ui/styles_tests.rs | 8 + src/ui/widgets/button.rs | 36 +- src/ui/widgets/button_tests.rs | 41 - src/ui/widgets/checkbox.rs | 20 +- src/ui/widgets/checkbox_tests.rs | 32 - src/ui/widgets/confirmation_prompt.rs | 34 +- src/ui/widgets/confirmation_prompt_tests.rs | 76 - src/ui/widgets/input_box.rs | 42 +- src/ui/widgets/input_box_tests.rs | 98 - src/ui/widgets/managarr_table.rs | 65 +- src/ui/widgets/managarr_table_tests.rs | 353 --- src/ui/widgets/message.rs | 17 +- src/ui/widgets/message_tests.rs | 38 - 39 files changed, 3075 insertions(+), 956 deletions(-) create mode 100644 src/handlers/sonarr_handlers/library/series_details_handler.rs create mode 100644 src/handlers/sonarr_handlers/library/series_details_handler_tests.rs delete mode 100644 src/ui/widgets/button_tests.rs delete mode 100644 src/ui/widgets/checkbox_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 626e5e3..e673269 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -489,12 +489,30 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_setters" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8ef033054e131169b8f0f9a7af8f5533a9436fadf3c500ed547f730f07090d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "destructure_traitobject" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" +[[package]] +name = "deunicode" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" + [[package]] name = "diff" version = "0.1.13" @@ -1319,6 +1337,8 @@ dependencies = [ "crossterm", "ctrlc", "derivative", + "derive_setters", + "deunicode", "dirs-next", "human-panic", "indicatif", diff --git a/Cargo.toml b/Cargo.toml index 937f273..0006109 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,8 @@ async-trait = "0.1.83" dirs-next = "2.0.0" managarr-tree-widget = "0.24.0" indicatif = "0.17.9" +derive_setters = "0.1.6" +deunicode = "1.6.0" [dev-dependencies] assert_cmd = "2.0.16" diff --git a/src/app/sonarr/mod.rs b/src/app/sonarr/mod.rs index 71f969d..8457f40 100644 --- a/src/app/sonarr/mod.rs +++ b/src/app/sonarr/mod.rs @@ -196,7 +196,13 @@ impl<'a> App<'a> { .current_selection() .clone() .seasons - .unwrap_or_default(); + .unwrap_or_default() + .into_iter() + .map(|mut season| { + season.title = Some(format!("Season {}", season.season_number)); + season + }) + .collect(); self.data.sonarr_data.seasons.set_items(seasons); } } diff --git a/src/app/sonarr/sonarr_context_clues.rs b/src/app/sonarr/sonarr_context_clues.rs index 5dc757c..9f91b92 100644 --- a/src/app/sonarr/sonarr_context_clues.rs +++ b/src/app/sonarr/sonarr_context_clues.rs @@ -53,6 +53,24 @@ pub static HISTORY_CONTEXT_CLUES: [ContextClue; 6] = [ (DEFAULT_KEYBINDINGS.esc, "cancel filter"), ]; +pub static SERIES_HISTORY_CONTEXT_CLUES: [ContextClue; 9] = [ + ( + DEFAULT_KEYBINDINGS.refresh, + DEFAULT_KEYBINDINGS.refresh.desc, + ), + (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), + (DEFAULT_KEYBINDINGS.submit, "details"), + (DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc), + (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), + (DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc), + ( + DEFAULT_KEYBINDINGS.auto_search, + DEFAULT_KEYBINDINGS.auto_search.desc, + ), + (DEFAULT_KEYBINDINGS.update, DEFAULT_KEYBINDINGS.update.desc), + (DEFAULT_KEYBINDINGS.esc, "cancel filter/close"), +]; + pub static SEASON_DETAILS_CONTEXT_CLUES: [ContextClue; 5] = [ ( DEFAULT_KEYBINDINGS.refresh, diff --git a/src/app/sonarr/sonarr_context_clues_tests.rs b/src/app/sonarr/sonarr_context_clues_tests.rs index 9f36aa0..f5d0c1e 100644 --- a/src/app/sonarr/sonarr_context_clues_tests.rs +++ b/src/app/sonarr/sonarr_context_clues_tests.rs @@ -9,7 +9,7 @@ mod tests { HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, - SYSTEM_TASKS_CONTEXT_CLUES, + SERIES_HISTORY_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES, }, }; @@ -86,6 +86,57 @@ mod tests { assert_eq!(series_context_clues_iter.next(), None); } + #[test] + fn test_series_history_context_clues() { + let mut series_history_context_clues_iter = SERIES_HISTORY_CONTEXT_CLUES.iter(); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.edit); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.edit.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit); + assert_str_eq!(*description, "details"); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.search.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.filter); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.filter.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.auto_search); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.auto_search.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.update); + assert_str_eq!(*description, DEFAULT_KEYBINDINGS.update.desc); + + let (key_binding, description) = series_history_context_clues_iter.next().unwrap(); + + assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc); + assert_str_eq!(*description, "cancel filter/close"); + assert_eq!(series_history_context_clues_iter.next(), None); + } + #[test] fn test_history_context_clues() { let mut history_context_clues_iter = HISTORY_CONTEXT_CLUES.iter(); diff --git a/src/app/sonarr/sonarr_tests.rs b/src/app/sonarr/sonarr_tests.rs index 6821ae9..99bc61c 100644 --- a/src/app/sonarr/sonarr_tests.rs +++ b/src/app/sonarr/sonarr_tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { mod sonarr_tests { - use pretty_assertions::assert_eq; + use pretty_assertions::{assert_eq, assert_str_eq}; use tokio::sync::mpsc; use crate::{ @@ -572,6 +572,13 @@ mod tests { app.populate_seasons_table().await; assert!(!app.data.sonarr_data.seasons.items.is_empty()); + assert_str_eq!( + app.data.sonarr_data.seasons.items[0] + .title + .as_ref() + .unwrap(), + "Season 0" + ); } #[tokio::test] @@ -585,6 +592,13 @@ mod tests { app.populate_seasons_table().await; assert!(!app.data.sonarr_data.seasons.items.is_empty()); + assert_str_eq!( + app.data.sonarr_data.seasons.items[0] + .title + .as_ref() + .unwrap(), + "Season 0" + ); } fn construct_app_unit<'a>() -> (App<'a>, mpsc::Receiver) { diff --git a/src/handlers/handler_test_utils.rs b/src/handlers/handler_test_utils.rs index 36e5d5f..a4b7112 100644 --- a/src/handlers/handler_test_utils.rs +++ b/src/handlers/handler_test_utils.rs @@ -323,6 +323,11 @@ mod test_utils { macro_rules! test_handler_delegation { ($handler:ident, $base:expr, $active_block:expr) => { let mut app = App::default(); + let mut series_history = $crate::models::stateful_table::StatefulTable::default(); + series_history.set_items(vec![ + $crate::models::sonarr_models::SonarrHistoryItem::default(), + ]); + app.data.sonarr_data.series_history = Some(series_history); app.push_navigation_stack($base.into()); app.push_navigation_stack($active_block.into()); diff --git a/src/handlers/radarr_handlers/library/movie_details_handler.rs b/src/handlers/radarr_handlers/library/movie_details_handler.rs index ef9e890..71264df 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler.rs @@ -457,7 +457,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for MovieDetailsHandler< _ if key == DEFAULT_KEYBINDINGS.refresh.key => { self .app - .pop_and_push_navigation_stack((self.active_radarr_block).into()); + .pop_and_push_navigation_stack(self.active_radarr_block.into()); } _ if key == DEFAULT_KEYBINDINGS.sort.key => { self diff --git a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs index 290ef67..4a90954 100644 --- a/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs +++ b/src/handlers/radarr_handlers/library/movie_details_handler_tests.rs @@ -1785,6 +1785,7 @@ mod tests { let mut app = App::default(); app.is_loading = true; app.push_navigation_stack(active_radarr_block.into()); + app.is_routing = false; app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal { movie_details: ScrollableText::with_string("test".to_owned()), ..MovieDetailsModal::default() @@ -1799,7 +1800,7 @@ mod tests { .handle(); assert_eq!(app.get_current_route(), active_radarr_block.into()); - assert!(app.is_routing); + assert!(!app.is_routing); } #[rstest] diff --git a/src/handlers/sonarr_handlers/history/mod.rs b/src/handlers/sonarr_handlers/history/mod.rs index b100e29..575b60c 100644 --- a/src/handlers/sonarr_handlers/history/mod.rs +++ b/src/handlers/sonarr_handlers/history/mod.rs @@ -312,7 +312,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for HistoryHandler<'a, ' } } -fn history_sorting_options() -> Vec> { +pub(in crate::handlers::sonarr_handlers) fn history_sorting_options() -> Vec> { vec![ SortOption { name: "Source Title", diff --git a/src/handlers/sonarr_handlers/library/library_handler_tests.rs b/src/handlers/sonarr_handlers/library/library_handler_tests.rs index d3d1fae..2da1cc3 100644 --- a/src/handlers/sonarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/library_handler_tests.rs @@ -11,9 +11,7 @@ mod tests { use crate::event::Key; use crate::handlers::sonarr_handlers::library::{series_sorting_options, LibraryHandler}; use crate::handlers::KeyEventHandler; - use crate::models::servarr_data::sonarr::sonarr_data::{ - ActiveSonarrBlock, ADD_SERIES_BLOCKS, DELETE_SERIES_BLOCKS, EDIT_SERIES_BLOCKS, LIBRARY_BLOCKS, - }; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS, DELETE_SERIES_BLOCKS, EDIT_SERIES_BLOCKS, LIBRARY_BLOCKS, SERIES_DETAILS_BLOCKS}; use crate::models::sonarr_models::{Series, SeriesStatus, SeriesType}; use crate::models::stateful_table::SortOption; use crate::models::HorizontallyScrollableText; @@ -615,6 +613,7 @@ mod tests { #[test] fn test_search_series_submit() { let mut app = App::default(); + app.should_ignore_quit_key = true; app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::SearchSeries.into()); app @@ -629,6 +628,7 @@ mod tests { LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchSeries, None).handle(); + assert!(!app.should_ignore_quit_key); assert_str_eq!( app.data.sonarr_data.series.current_selection().title.text, "Test 2" @@ -639,6 +639,7 @@ mod tests { #[test] fn test_search_series_submit_error_on_no_search_hits() { let mut app = App::default(); + app.should_ignore_quit_key = true; app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::SearchSeries.into()); app @@ -653,6 +654,7 @@ mod tests { LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchSeries, None).handle(); + assert!(!app.should_ignore_quit_key); assert_str_eq!( app.data.sonarr_data.series.current_selection().title.text, "Test 1" @@ -666,6 +668,7 @@ mod tests { #[test] fn test_search_filtered_series_submit() { let mut app = App::default(); + app.should_ignore_quit_key = true; app .data .sonarr_data @@ -685,6 +688,7 @@ mod tests { LibraryHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchSeries, None).handle(); + assert!(!app.should_ignore_quit_key); assert_str_eq!( app.data.sonarr_data.series.current_selection().title.text, "Test 2" @@ -695,6 +699,7 @@ mod tests { #[test] fn test_filter_series_submit() { let mut app = App::default(); + app.should_ignore_quit_key = true; app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::FilterSeries.into()); app @@ -732,6 +737,7 @@ mod tests { #[test] fn test_filter_series_submit_error_on_no_filter_matches() { let mut app = App::default(); + app.should_ignore_quit_key = true; app.push_navigation_stack(ActiveSonarrBlock::Series.into()); app.push_navigation_stack(ActiveSonarrBlock::FilterSeries.into()); app @@ -946,7 +952,6 @@ mod tests { } mod test_handle_key_char { - use bimap::BiMap; use pretty_assertions::{assert_eq, assert_str_eq}; use serde_json::Number; use strum::IntoEnumIterator; @@ -1465,27 +1470,30 @@ mod tests { ); } - // #[rstest] - // fn test_delegates_series_details_blocks_to_series_details_handler( - // #[values( - // ActiveSonarrBlock::SeriesDetails, - // ActiveSonarrBlock::SeriesHistory, - // ActiveSonarrBlock::FileInfo, - // ActiveSonarrBlock::Cast, - // ActiveSonarrBlock::Crew, - // ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, - // ActiveSonarrBlock::UpdateAndScanPrompt, - // ActiveSonarrBlock::ManualSearch, - // ActiveSonarrBlock::ManualSearchConfirmPrompt - // )] - // active_sonarr_block: ActiveSonarrBlock, - // ) { - // test_handler_delegation!( - // LibraryHandler, - // ActiveSonarrBlock::Series, - // active_sonarr_block - // ); - // } + #[rstest] + fn test_delegates_series_details_blocks_to_series_details_handler( + #[values( + ActiveSonarrBlock::SeriesDetails, + ActiveSonarrBlock::SeriesHistory, + ActiveSonarrBlock::SearchSeason, + ActiveSonarrBlock::SearchSeasonError, + ActiveSonarrBlock::UpdateAndScanSeriesPrompt, + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + ActiveSonarrBlock::SearchSeriesHistory, + ActiveSonarrBlock::SearchSeriesHistoryError, + ActiveSonarrBlock::FilterSeriesHistory, + ActiveSonarrBlock::FilterSeriesHistoryError, + ActiveSonarrBlock::SeriesHistorySortPrompt, + ActiveSonarrBlock::SeriesHistoryDetails + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + LibraryHandler, + ActiveSonarrBlock::Series, + active_sonarr_block + ); + } #[rstest] fn test_delegates_edit_series_blocks_to_edit_series_handler( @@ -1711,6 +1719,7 @@ mod tests { library_handler_blocks.extend(ADD_SERIES_BLOCKS); library_handler_blocks.extend(DELETE_SERIES_BLOCKS); library_handler_blocks.extend(EDIT_SERIES_BLOCKS); + library_handler_blocks.extend(SERIES_DETAILS_BLOCKS); ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { if library_handler_blocks.contains(&active_sonarr_block) { diff --git a/src/handlers/sonarr_handlers/library/mod.rs b/src/handlers/sonarr_handlers/library/mod.rs index 47ff6ea..c91ffc1 100644 --- a/src/handlers/sonarr_handlers/library/mod.rs +++ b/src/handlers/sonarr_handlers/library/mod.rs @@ -22,6 +22,7 @@ use crate::{ use super::handle_change_tab_left_right_keys; use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler; mod add_series_handler; mod delete_series_handler; @@ -29,6 +30,7 @@ mod delete_series_handler; #[cfg(test)] #[path = "library_handler_tests.rs"] mod library_handler_tests; +mod series_details_handler; pub(super) struct LibraryHandler<'a, 'b> { key: Key, @@ -51,6 +53,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, ' EditSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context) .handle(); } + _ if SeriesDetailsHandler::accepts(self.active_sonarr_block) => { + SeriesDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } _ => self.handle_key_event(), } } @@ -59,6 +65,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, ' AddSeriesHandler::accepts(active_block) || DeleteSeriesHandler::accepts(active_block) || EditSeriesHandler::accepts(active_block) + || SeriesDetailsHandler::accepts(active_block) || LIBRARY_BLOCKS.contains(&active_block) } diff --git a/src/handlers/sonarr_handlers/library/series_details_handler.rs b/src/handlers/sonarr_handlers/library/series_details_handler.rs new file mode 100644 index 0000000..e47fa18 --- /dev/null +++ b/src/handlers/sonarr_handlers/library/series_details_handler.rs @@ -0,0 +1,610 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handle_text_box_keys; +use crate::handlers::sonarr_handlers::history::history_sorting_options; +use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, EDIT_SERIES_SELECTION_BLOCKS, SERIES_DETAILS_BLOCKS, +}; +use crate::models::{BlockSelectionState, HorizontallyScrollableText, Scrollable}; +use crate::network::sonarr_network::SonarrEvent; + +#[cfg(test)] +#[path = "series_details_handler_tests.rs"] +mod series_details_handler_tests; + +pub(super) struct SeriesDetailsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler<'a, 'b> { + fn accepts(active_block: ActiveSonarrBlock) -> bool { + SERIES_DETAILS_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + _context: Option, + ) -> SeriesDetailsHandler<'a, 'b> { + SeriesDetailsHandler { + key, + app, + active_sonarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + if self.active_sonarr_block == ActiveSonarrBlock::SeriesHistory { + !self.app.is_loading && self.app.data.sonarr_data.series_history.is_some() + } else { + !self.app.is_loading + } + } + + fn handle_scroll_up(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SeriesDetails => self.app.data.sonarr_data.seasons.scroll_up(), + ActiveSonarrBlock::SeriesHistory => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .scroll_up(), + ActiveSonarrBlock::SeriesHistorySortPrompt => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .sort + .as_mut() + .unwrap() + .scroll_up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SeriesDetails => self.app.data.sonarr_data.seasons.scroll_down(), + ActiveSonarrBlock::SeriesHistory => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .scroll_down(), + ActiveSonarrBlock::SeriesHistorySortPrompt => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .sort + .as_mut() + .unwrap() + .scroll_down(), + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SeriesDetails => self.app.data.sonarr_data.seasons.scroll_to_top(), + ActiveSonarrBlock::SearchSeason => self + .app + .data + .sonarr_data + .seasons + .search + .as_mut() + .unwrap() + .scroll_home(), + ActiveSonarrBlock::SearchSeriesHistory => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .search + .as_mut() + .unwrap() + .scroll_home(), + ActiveSonarrBlock::FilterSeriesHistory => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .filter + .as_mut() + .unwrap() + .scroll_home(), + ActiveSonarrBlock::SeriesHistory => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .scroll_to_top(), + ActiveSonarrBlock::SeriesHistorySortPrompt => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .sort + .as_mut() + .unwrap() + .scroll_to_top(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SeriesDetails => self.app.data.sonarr_data.seasons.scroll_to_bottom(), + ActiveSonarrBlock::SearchSeason => self + .app + .data + .sonarr_data + .seasons + .search + .as_mut() + .unwrap() + .reset_offset(), + ActiveSonarrBlock::SearchSeriesHistory => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .search + .as_mut() + .unwrap() + .reset_offset(), + ActiveSonarrBlock::FilterSeriesHistory => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .filter + .as_mut() + .unwrap() + .reset_offset(), + ActiveSonarrBlock::SeriesHistory => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .scroll_to_bottom(), + ActiveSonarrBlock::SeriesHistorySortPrompt => self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .sort + .as_mut() + .unwrap() + .scroll_to_bottom(), + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SeriesDetails | ActiveSonarrBlock::SeriesHistory => match self.key { + _ if self.key == DEFAULT_KEYBINDINGS.left.key => { + self.app.data.sonarr_data.series_info_tabs.previous(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .sonarr_data + .series_info_tabs + .get_active_route(), + ); + } + _ if self.key == DEFAULT_KEYBINDINGS.right.key => { + self.app.data.sonarr_data.series_info_tabs.next(); + self.app.pop_and_push_navigation_stack( + self + .app + .data + .sonarr_data + .series_info_tabs + .get_active_route(), + ); + } + _ => (), + }, + ActiveSonarrBlock::UpdateAndScanSeriesPrompt + | ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { + handle_prompt_toggle(self.app, self.key) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SeriesDetails if !self.app.data.sonarr_data.seasons.is_empty() => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into()); + } + ActiveSonarrBlock::SeriesHistory + if !self + .app + .data + .sonarr_data + .series_history + .as_ref() + .expect("Series history should be Some") + .is_empty() => + { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SeriesHistoryDetails.into()); + } + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::TriggerAutomaticSeriesSearch(None)); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::UpdateAndScanSeriesPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::UpdateAndScanSeries(None)); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::SearchSeason => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + + if self.app.data.sonarr_data.seasons.search.is_some() { + let has_match = self.app.data.sonarr_data.seasons.apply_search(|season| { + season + .title + .as_ref() + .expect("Season was not populated with title in handlers") + }); + + if !has_match { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SearchSeasonError.into()); + } + } + } + ActiveSonarrBlock::SearchSeriesHistory => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + + if self + .app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .search + .is_some() + { + let has_match = self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .apply_search(|history_item| &history_item.source_title.text); + + if !has_match { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SearchSeriesHistoryError.into()); + } + } + } + ActiveSonarrBlock::FilterSeriesHistory => { + self.app.pop_navigation_stack(); + self.app.should_ignore_quit_key = false; + + if self + .app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filter + .is_some() + { + let has_matches = self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .apply_filter(|history_item| &history_item.source_title.text); + + if !has_matches { + self + .app + .push_navigation_stack(ActiveSonarrBlock::FilterSeriesHistoryError.into()); + } + } + } + ActiveSonarrBlock::SeriesHistorySortPrompt => { + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .items + .sort_by(|a, b| a.id.cmp(&b.id)); + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .apply_sorting(); + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::SearchSeason | ActiveSonarrBlock::SearchSeasonError => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.seasons.reset_search(); + self.app.should_ignore_quit_key = false; + } + ActiveSonarrBlock::SearchSeriesHistory | ActiveSonarrBlock::SearchSeriesHistoryError => { + self.app.pop_navigation_stack(); + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .reset_search(); + self.app.should_ignore_quit_key = false; + } + ActiveSonarrBlock::FilterSeriesHistory | ActiveSonarrBlock::FilterSeriesHistoryError => { + self.app.pop_navigation_stack(); + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .reset_filter(); + self.app.should_ignore_quit_key = false; + } + ActiveSonarrBlock::UpdateAndScanSeriesPrompt + | ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + } + ActiveSonarrBlock::SeriesHistoryDetails | ActiveSonarrBlock::SeriesHistorySortPrompt => { + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::SeriesHistory => { + if self.app.data.sonarr_data.series_history.as_ref().expect("Series history is not populated").filtered_items.is_some() { + self.app.data.sonarr_data.series_history.as_mut().expect("Series history is not populated").reset_filter(); + } else { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.reset_series_info_tabs(); + } + } + ActiveSonarrBlock::SeriesDetails => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.reset_series_info_tabs(); + } + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::SeriesDetails => match self.key { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => self + .app + .pop_and_push_navigation_stack(self.active_sonarr_block.into()), + _ if key == DEFAULT_KEYBINDINGS.search.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SearchSeason.into()); + self.app.data.sonarr_data.seasons.search = Some(HorizontallyScrollableText::default()); + self.app.should_ignore_quit_key = true; + } + _ if key == DEFAULT_KEYBINDINGS.auto_search.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchSeriesPrompt.into()); + } + _ if key == DEFAULT_KEYBINDINGS.update.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::UpdateAndScanSeriesPrompt.into()); + } + _ if key == DEFAULT_KEYBINDINGS.edit.key => { + self.app.push_navigation_stack( + ( + ActiveSonarrBlock::EditSeriesPrompt, + Some(self.active_sonarr_block), + ) + .into(), + ); + self.app.data.sonarr_data.edit_series_modal = Some((&self.app.data.sonarr_data).into()); + self.app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + } + _ => (), + }, + ActiveSonarrBlock::SeriesHistory => match self.key { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => self + .app + .pop_and_push_navigation_stack(self.active_sonarr_block.into()), + _ if key == DEFAULT_KEYBINDINGS.auto_search.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchSeriesPrompt.into()); + } + _ if key == DEFAULT_KEYBINDINGS.search.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::SearchSeriesHistory.into()); + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .expect("Series history should be populated") + .search = Some(HorizontallyScrollableText::default()); + self.app.should_ignore_quit_key = true; + } + _ if key == DEFAULT_KEYBINDINGS.filter.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::FilterSeriesHistory.into()); + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .expect("Series history should be populated") + .reset_filter(); + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .expect("Series history should be populated") + .filter = Some(HorizontallyScrollableText::default()); + self.app.should_ignore_quit_key = true; + } + _ if key == DEFAULT_KEYBINDINGS.sort.key => { + self + .app + .data + .sonarr_data + .series_history + .as_mut() + .expect("Series history should be populated") + .sorting(history_sorting_options()); + self + .app + .push_navigation_stack(ActiveSonarrBlock::SeriesHistorySortPrompt.into()); + } + _ if key == DEFAULT_KEYBINDINGS.edit.key => { + self.app.push_navigation_stack( + ( + ActiveSonarrBlock::EditSeriesPrompt, + Some(self.active_sonarr_block), + ) + .into(), + ); + self.app.data.sonarr_data.edit_series_modal = Some((&self.app.data.sonarr_data).into()); + self.app.data.sonarr_data.selected_block = + BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS); + } + _ if key == DEFAULT_KEYBINDINGS.update.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::UpdateAndScanSeriesPrompt.into()); + } + _ => (), + }, + ActiveSonarrBlock::SearchSeason => { + handle_text_box_keys!( + self, + key, + self.app.data.sonarr_data.seasons.search.as_mut().unwrap() + ) + } + ActiveSonarrBlock::SearchSeriesHistory => { + handle_text_box_keys!( + self, + key, + self.app.data.sonarr_data.series_history.as_mut().expect("Series history should be populated").search.as_mut().unwrap() + ) + } + ActiveSonarrBlock::FilterSeriesHistory => { + handle_text_box_keys!( + self, + key, + self.app.data.sonarr_data.series_history.as_mut().expect("Series history should be populated").filter.as_mut().unwrap() + ) + } + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { + if key == DEFAULT_KEYBINDINGS.confirm.key { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::TriggerAutomaticSeriesSearch(None)); + + self.app.pop_navigation_stack(); + } + } + ActiveSonarrBlock::UpdateAndScanSeriesPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::UpdateAndScanSeries(None)); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } +} diff --git a/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs new file mode 100644 index 0000000..a1861dc --- /dev/null +++ b/src/handlers/sonarr_handlers/library/series_details_handler_tests.rs @@ -0,0 +1,1971 @@ +#[cfg(test)] +mod tests { + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ + ActiveSonarrBlock, SERIES_DETAILS_BLOCKS, + }; + use crate::models::sonarr_models::Season; + use crate::models::sonarr_models::SonarrHistoryItem; + use crate::models::stateful_table::{SortOption, StatefulTable}; + use core::sync::atomic::Ordering::SeqCst; + use pretty_assertions::assert_str_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + mod test_handle_scroll_up_and_down { + use super::*; + use pretty_assertions::assert_eq; + + #[rstest] + fn test_seasons_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.data.sonarr_data.seasons.set_items(vec![ + Season { + season_number: 1, + ..Season::default() + }, + Season { + season_number: 2, + ..Season::default() + }, + ]); + + SeriesDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SeriesDetails, None).handle(); + + assert_eq!( + app + .data + .sonarr_data + .seasons + .current_selection() + .season_number, + 2 + ); + + SeriesDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SeriesDetails, None).handle(); + + assert_eq!( + app + .data + .sonarr_data + .seasons + .current_selection() + .season_number, + 1 + ); + } + + #[rstest] + fn test_series_history_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![ + SonarrHistoryItem { + source_title: "Test 1".into(), + ..SonarrHistoryItem::default() + }, + SonarrHistoryItem { + source_title: "Test 2".into(), + ..SonarrHistoryItem::default() + }, + ]); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SeriesHistory, None).handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .current_selection() + .source_title + .text, + "Test 2" + ); + + SeriesDetailsHandler::with(key, &mut app, ActiveSonarrBlock::SeriesHistory, None).handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .current_selection() + .source_title + .text, + "Test 1" + ); + } + + #[rstest] + fn test_series_history_sort_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let series_history_item_field_vec = sort_options(); + let mut app = App::default(); + app.data.sonarr_data.series_history = Some(StatefulTable::default()); + app + .data + .sonarr_data + .series_history + .as_mut() + .unwrap() + .sorting(sort_options()); + + if key == Key::Up { + for i in (0..series_history_item_field_vec.len()).rev() { + SeriesDetailsHandler::with( + key, + &mut app, + ActiveSonarrBlock::SeriesHistorySortPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .sort + .as_ref() + .unwrap() + .current_selection(), + &series_history_item_field_vec[i] + ); + } + } else { + for i in 0..series_history_item_field_vec.len() { + SeriesDetailsHandler::with( + key, + &mut app, + ActiveSonarrBlock::SeriesHistorySortPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .sort + .as_ref() + .unwrap() + .current_selection(), + &series_history_item_field_vec[(i + 1) % series_history_item_field_vec.len()] + ); + } + } + } + } + + mod test_handle_home_end { + use super::*; + use pretty_assertions::{assert_eq, assert_str_eq}; + + #[test] + fn test_seasons_home_and_end() { + let mut app = App::default(); + app.data.sonarr_data.seasons.set_items(vec![ + Season { + season_number: 1, + ..Season::default() + }, + Season { + season_number: 2, + ..Season::default() + }, + Season { + season_number: 3, + ..Season::default() + }, + ]); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .seasons + .current_selection() + .season_number, + 3 + ); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .seasons + .current_selection() + .season_number, + 1 + ); + } + + #[test] + fn test_series_history_home_and_end() { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![ + SonarrHistoryItem { + source_title: "Test 1".into(), + ..SonarrHistoryItem::default() + }, + SonarrHistoryItem { + source_title: "Test 2".into(), + ..SonarrHistoryItem::default() + }, + SonarrHistoryItem { + source_title: "Test 3".into(), + ..SonarrHistoryItem::default() + }, + ]); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .current_selection() + .source_title + .text, + "Test 3" + ); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .current_selection() + .source_title + .text, + "Test 1" + ); + } + + #[test] + fn test_season_search_box_home_end_keys() { + let mut app = App::default(); + app + .data + .sonarr_data + .seasons + .set_items(vec![Season::default()]); + app.data.sonarr_data.seasons.search = Some("Test".into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SearchSeason, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .seasons + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 4 + ); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SearchSeason, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .seasons + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 0 + ); + } + + #[test] + fn test_series_history_search_box_home_end_keys() { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + series_history.search = Some("Test".into()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SearchSeriesHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 4 + ); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SearchSeriesHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .search + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 0 + ); + } + + #[test] + fn test_series_history_filter_box_home_end_keys() { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + series_history.filter = Some("Test".into()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::FilterSeriesHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filter + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 4 + ); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::FilterSeriesHistory, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filter + .as_ref() + .unwrap() + .offset + .load(SeqCst), + 0 + ); + } + + #[test] + fn test_series_history_sort_home_end() { + let series_history_item_field_vec = sort_options(); + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.sorting(sort_options()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::SeriesHistorySortPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .sort + .as_ref() + .unwrap() + .current_selection(), + &series_history_item_field_vec[series_history_item_field_vec.len() - 1] + ); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::SeriesHistorySortPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .sort + .as_ref() + .unwrap() + .current_selection(), + &series_history_item_field_vec[0] + ); + } + } + + mod test_handle_left_right_actions { + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + fn test_left_right_prompt_toggle( + #[values( + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + ActiveSonarrBlock::UpdateAndScanSeriesPrompt + )] + active_sonarr_block: ActiveSonarrBlock, + #[values(Key::Left, Key::Right)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + SeriesDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + SeriesDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[rstest] + #[case(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + #[case(ActiveSonarrBlock::SeriesHistory, ActiveSonarrBlock::SeriesDetails)] + fn test_series_details_tabs_left_right_action( + #[case] left_block: ActiveSonarrBlock, + #[case] right_block: ActiveSonarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.is_loading = is_ready; + app.push_navigation_stack(right_block.into()); + app.data.sonarr_data.series_info_tabs.index = app + .data + .sonarr_data + .series_info_tabs + .tabs + .iter() + .position(|tab_route| tab_route.route == right_block.into()) + .unwrap_or_default(); + + SeriesDetailsHandler::with(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None) + .handle(); + + assert_eq!( + app.get_current_route(), + app.data.sonarr_data.series_info_tabs.get_active_route() + ); + assert_eq!(app.get_current_route(), left_block.into()); + + SeriesDetailsHandler::with(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None) + .handle(); + + assert_eq!( + app.get_current_route(), + app.data.sonarr_data.series_info_tabs.get_active_route() + ); + assert_eq!(app.get_current_route(), right_block.into()); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::extended_stateful_iterable_vec; + use crate::models::HorizontallyScrollableText; + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_series_details_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .seasons + .set_items(extended_stateful_iterable_vec!(Season, Option)); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeasonDetails.into() + ); + } + + #[test] + fn test_series_details_submit_no_op_on_empty_seasons_table() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesDetails, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + } + + #[test] + fn test_series_details_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesDetails, None) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + + #[test] + fn test_series_history_submit() { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistoryDetails.into() + ); + } + + #[test] + fn test_series_history_submit_no_op_when_series_history_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.data.sonarr_data.series_history = Some(StatefulTable::default()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + } + + #[test] + fn test_series_history_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeriesHistory, None) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + } + + #[rstest] + #[case( + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + SonarrEvent::TriggerAutomaticSeriesSearch(None) + )] + #[case( + ActiveSonarrBlock::UpdateAndScanSeriesPrompt, + SonarrEvent::UpdateAndScanSeries(None) + )] + fn test_series_details_prompt_confirm_submit( + #[case] prompt_block: ActiveSonarrBlock, + #[case] expected_action: SonarrEvent, + ) { + let mut app = App::default(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(expected_action) + ); + } + + #[rstest] + fn test_series_details_prompt_decline_submit( + #[values( + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + ActiveSonarrBlock::UpdateAndScanSeriesPrompt + )] + prompt_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + } + + #[test] + fn test_search_seasons_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeason.into()); + app + .data + .sonarr_data + .seasons + .set_items(extended_stateful_iterable_vec!(Season, Option)); + app.data.sonarr_data.seasons.search = Some("Test 2".into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchSeason, None) + .handle(); + + assert_str_eq!( + app + .data + .sonarr_data + .seasons + .current_selection() + .title + .as_ref() + .unwrap(), + "Test 2" + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + } + + #[test] + fn test_search_seasons_submit_error_on_no_search_hits() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeason.into()); + app + .data + .sonarr_data + .seasons + .set_items(extended_stateful_iterable_vec!(Season, Option)); + app.data.sonarr_data.seasons.search = Some("Test 5".into()); + + SeriesDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SearchSeason, None) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert_str_eq!( + app + .data + .sonarr_data + .seasons + .current_selection() + .title + .as_ref() + .unwrap(), + "Test 1" + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SearchSeasonError.into() + ); + } + + #[test] + fn test_search_series_history_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(extended_stateful_iterable_vec!( + SonarrHistoryItem, + HorizontallyScrollableText, + source_title + )); + series_history.search = Some("Test 2".into()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::SearchSeriesHistory, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert_str_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .current_selection() + .source_title + .text, + "Test 2" + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + } + + #[test] + fn test_search_series_history_submit_error_on_no_search_hits() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(extended_stateful_iterable_vec!( + SonarrHistoryItem, + HorizontallyScrollableText, + source_title + )); + series_history.search = Some("Test 5".into()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::SearchSeriesHistory, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert_str_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .current_selection() + .source_title + .text, + "Test 1" + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SearchSeriesHistoryError.into() + ); + } + + #[test] + fn test_filter_series_history_submit() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.push_navigation_stack(ActiveSonarrBlock::FilterSeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(extended_stateful_iterable_vec!( + SonarrHistoryItem, + HorizontallyScrollableText, + source_title + )); + series_history.filter = Some("Test".into()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::FilterSeriesHistory, + None, + ) + .handle(); + + assert!(app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filtered_items + .is_some()); + assert!(!app.should_ignore_quit_key); + assert_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filtered_items + .as_ref() + .unwrap() + .len(), + 3 + ); + assert_str_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .current_selection() + .source_title + .text, + "Test 1" + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + } + + #[test] + fn test_filter_series_history_submit_error_on_no_filter_matches() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(extended_stateful_iterable_vec!( + SonarrHistoryItem, + HorizontallyScrollableText, + source_title + )); + series_history.filter = Some("Test 5".into()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::FilterSeriesHistory, + None, + ) + .handle(); + + assert!(!app.should_ignore_quit_key); + assert!(app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filtered_items + .is_none()); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::FilterSeriesHistoryError.into() + ); + } + + #[test] + fn test_series_history_sort_prompt_submit() { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + let series_history_vec = vec![ + SonarrHistoryItem { + id: 3, + source_title: "Test 1".into(), + ..SonarrHistoryItem::default() + }, + SonarrHistoryItem { + id: 2, + source_title: "Test 2".into(), + ..SonarrHistoryItem::default() + }, + SonarrHistoryItem { + id: 1, + source_title: "Test 3".into(), + ..SonarrHistoryItem::default() + }, + ]; + series_history.set_items(series_history_vec.clone()); + series_history.sorting(sort_options()); + series_history.sort_asc = true; + app.data.sonarr_data.series_history = Some(series_history); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistorySortPrompt.into()); + + let mut expected_vec = series_history_vec; + expected_vec.sort_by(|a, b| a.id.cmp(&b.id)); + expected_vec.reverse(); + + SeriesDetailsHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::SeriesHistorySortPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + assert_eq!( + app.data.sonarr_data.series_history.as_ref().unwrap().items, + expected_vec + ); + } + } + + mod test_handle_esc { + use super::*; + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + use crate::models::stateful_table::StatefulTable; + use pretty_assertions::assert_eq; + use ratatui::widgets::TableState; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_search_season_block_esc( + #[values(ActiveSonarrBlock::SearchSeason, ActiveSonarrBlock::SearchSeasonError)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(active_sonarr_block.into()); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.seasons.search = Some("Test".into()); + + SeriesDetailsHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.sonarr_data.seasons.search, None); + } + + #[rstest] + fn test_search_series_history_block_esc( + #[values( + ActiveSonarrBlock::SearchSeriesHistory, + ActiveSonarrBlock::SearchSeriesHistoryError + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.push_navigation_stack(active_sonarr_block.into()); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.series_history.as_mut().unwrap().search = Some("Test".into()); + + SeriesDetailsHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.data.sonarr_data.series_history.as_ref().unwrap().search, + None + ); + } + + #[rstest] + fn test_filter_series_history_block_esc( + #[values( + ActiveSonarrBlock::FilterSeriesHistory, + ActiveSonarrBlock::FilterSeriesHistoryError + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.push_navigation_stack(active_sonarr_block.into()); + app.data.sonarr_data = create_test_sonarr_data(); + app.data.sonarr_data.series_history = Some(StatefulTable { + filter: Some("Test".into()), + filtered_items: Some(Vec::new()), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }); + + SeriesDetailsHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.data.sonarr_data.series_history.as_ref().unwrap().filter, + None + ); + assert_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filtered_items, + None + ); + assert_eq!( + app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filtered_state, + None + ); + } + + #[test] + fn test_series_history_sort_prompt_block_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistorySortPrompt.into()); + + SeriesDetailsHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::SeriesHistorySortPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + } + + #[test] + fn test_series_history_details_block_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistoryDetails.into()); + + SeriesDetailsHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::SeriesHistoryDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + } + + #[rstest] + fn test_series_details_prompts_esc( + #[values( + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + ActiveSonarrBlock::UpdateAndScanSeriesPrompt + )] + prompt_block: ActiveSonarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::default(); + app.is_loading = is_ready; + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app.push_navigation_stack(prompt_block.into()); + + SeriesDetailsHandler::with(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + } + + #[test] + fn test_series_history_esc_resets_filter_if_one_is_set_instead_of_closing_the_window() { + let mut app = App::default(); + let series_history = StatefulTable { + filter: Some("Test".into()), + filtered_items: Some(vec![SonarrHistoryItem::default()]), + filtered_state: Some(TableState::default()), + ..StatefulTable::default() + }; + app.data.sonarr_data.series_history = Some(series_history); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + + SeriesDetailsHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::SeriesHistory, None) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + assert!(app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filter + .is_none()); + assert!(app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filtered_items + .is_none()); + assert!(app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filtered_state + .is_none()); + } + } + + mod test_handle_key_char { + use super::*; + use crate::handlers::sonarr_handlers::history::history_sorting_options; + use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data; + use crate::models::servarr_data::sonarr::sonarr_data::SonarrData; + use crate::models::sonarr_models::{Series, SeriesType}; + use crate::models::HorizontallyScrollableText; + use crate::test_edit_series_key; + use pretty_assertions::{assert_eq, assert_str_eq}; + use ratatui::widgets::TableState; + use serde_json::Number; + use strum::IntoEnumIterator; + use crate::network::sonarr_network::SonarrEvent; + + #[test] + fn test_search_season_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .seasons + .set_items(vec![Season::default()]); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.search.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SearchSeason.into() + ); + assert!(app.should_ignore_quit_key); + assert_eq!( + app.data.sonarr_data.seasons.search, + Some(HorizontallyScrollableText::default()) + ); + } + + #[test] + fn test_search_season_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into()); + app + .data + .sonarr_data + .seasons + .set_items(vec![Season::default()]); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.search.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesDetails.into() + ); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.sonarr_data.seasons.search, None); + } + + #[test] + fn test_search_series_history_key() { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.search.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SearchSeriesHistory.into() + ); + assert!(app.should_ignore_quit_key); + assert_eq!( + app.data.sonarr_data.series_history.as_ref().unwrap().search, + Some(HorizontallyScrollableText::default()) + ); + } + + #[test] + fn test_search_series_history_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.search.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.sonarr_data.seasons.search, None); + } + + #[test] + fn test_filter_series_history_key() { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.filter.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::FilterSeriesHistory.into() + ); + assert!(app.should_ignore_quit_key); + assert_eq!( + app.data.sonarr_data.series_history.as_ref().unwrap().filter, + Some(HorizontallyScrollableText::default()) + ); + } + + #[test] + fn test_filter_series_history_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.filter.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistory.into() + ); + assert!(!app.should_ignore_quit_key); + assert_eq!(app.data.sonarr_data.seasons.filter, None); + } + + #[test] + fn test_filter_series_history_key_resets_previous_filter() { + let mut app = App::default(); + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + series_history.filter = Some("Test".into()); + series_history.filtered_items = Some(vec![SonarrHistoryItem::default()]); + series_history.filtered_state = Some(TableState::default()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.filter.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::FilterSeriesHistory.into() + ); + assert!(app.should_ignore_quit_key); + assert_eq!( + app.data.sonarr_data.series_history.as_ref().unwrap().filter, + Some(HorizontallyScrollableText::default()) + ); + assert!(app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filtered_items + .is_none()); + assert!(app + .data + .sonarr_data + .series_history + .as_ref() + .unwrap() + .filtered_state + .is_none()); + } + + #[rstest] + fn test_series_details_edit_key( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_edit_series_key!( + SeriesDetailsHandler, + active_sonarr_block, + active_sonarr_block + ); + } + + #[rstest] + fn test_series_edit_key_no_op_when_not_ready( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.edit.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), active_sonarr_block.into()); + assert!(app.data.sonarr_data.edit_series_modal.is_none()); + } + + #[rstest] + fn test_auto_search_key( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + app.push_navigation_stack(active_sonarr_block.into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt.into() + ); + } + + #[rstest] + fn test_auto_search_key_no_op_when_not_ready( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.auto_search.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + active_sonarr_block.into() + ); + } + + #[rstest] + fn test_update_key( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + app.push_navigation_stack(active_sonarr_block.into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::UpdateAndScanSeriesPrompt.into() + ); + } + + #[rstest] + fn test_update_key_no_op_when_not_ready( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + active_sonarr_block.into() + ); + } + + #[rstest] + fn test_refresh_key( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + app.push_navigation_stack(active_sonarr_block.into()); + app.is_routing = false; + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + active_sonarr_block.into() + ); + assert!(app.is_routing); + } + + #[rstest] + fn test_refresh_key_no_op_when_not_ready( + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] + active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(active_sonarr_block.into()); + app.is_routing = false; + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + active_sonarr_block, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + active_sonarr_block.into() + ); + assert!(!app.is_routing); + } + + #[test] + fn test_sort_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.sort.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::SeriesHistorySortPrompt.into() + ); + assert_eq!( + app.data.sonarr_data.series_history.as_ref().unwrap().sort.as_ref().unwrap().items, + history_sorting_options() + ); + assert!(!app.data.sonarr_data.series_history.as_ref().unwrap().sort_asc); + } + + #[test] + fn test_sort_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::SeriesHistory.into()); + app.is_routing = false; + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.sort.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::SeriesHistory.into()); + assert!(app.data.sonarr_data.series_history.as_ref().unwrap().sort.is_none()); + assert!(!app.data.sonarr_data.series_history.as_ref().unwrap().sort_asc); + assert!(!app.is_routing); + } + + #[test] + fn test_search_season_box_backspace_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeason.into()); + app.data.sonarr_data.seasons.search = Some("Test".into()); + app + .data + .sonarr_data + .seasons + .set_items(vec![Season::default()]); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::SearchSeason, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.seasons.search.as_ref().unwrap().text, + "Tes" + ); + } + + #[test] + fn test_search_series_history_box_backspace_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + series_history.search = Some("Test".into()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::SearchSeriesHistory, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.series_history.as_ref().unwrap().search.as_ref().unwrap().text, + "Tes" + ); + } + + #[test] + fn test_filter_series_history_box_backspace_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::FilterSeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + series_history.filter = Some("Test".into()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::FilterSeriesHistory, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.series_history.as_ref().unwrap().filter.as_ref().unwrap().text, + "Tes" + ); + } + + #[test] + fn test_search_season_box_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeason.into()); + app + .data + .sonarr_data + .seasons + .set_items(vec![Season::default()]); + app.data.sonarr_data.seasons.search = Some(HorizontallyScrollableText::default()); + + SeriesDetailsHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::SearchSeason, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.seasons.search.as_ref().unwrap().text, + "h" + ); + } + + #[test] + fn test_search_series_history_box_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::SearchSeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + series_history.search = Some(HorizontallyScrollableText::default()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::SearchSeriesHistory, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.series_history.as_ref().unwrap().search.as_ref().unwrap().text, + "h" + ); + } + + #[test] + fn test_filter_series_history_box_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::FilterSeriesHistory.into()); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + series_history.filter = Some(HorizontallyScrollableText::default()); + app.data.sonarr_data.series_history = Some(series_history); + + SeriesDetailsHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::FilterSeriesHistory, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.series_history.as_ref().unwrap().filter.as_ref().unwrap().text, + "h" + ); + } + + #[rstest] + #[case( + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, + SonarrEvent::TriggerAutomaticSeriesSearch(None) + )] + #[case( + ActiveSonarrBlock::UpdateAndScanSeriesPrompt, + SonarrEvent::UpdateAndScanSeries(None) + )] + fn test_series_details_prompt_confirm_confirm_key( + #[case] prompt_block: ActiveSonarrBlock, + #[case] expected_action: SonarrEvent, + #[values(ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory)] active_sonarr_block: ActiveSonarrBlock, + ) { + let mut app = App::default(); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(active_sonarr_block.into()); + app.push_navigation_stack(prompt_block.into()); + + SeriesDetailsHandler::with(DEFAULT_KEYBINDINGS.confirm.key, &mut app, prompt_block, None).handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.get_current_route(), + active_sonarr_block.into() + ); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(expected_action) + ); + } + } + + #[test] + fn test_series_details_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if SERIES_DETAILS_BLOCKS.contains(&active_sonarr_block) { + assert!(SeriesDetailsHandler::accepts(active_sonarr_block)); + } else { + assert!(!SeriesDetailsHandler::accepts(active_sonarr_block)); + } + }); + } + + #[test] + fn test_series_details_handler_is_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.is_loading = true; + + let handler = SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_series_details_handler_is_not_ready_when_not_loading_and_series_history_is_none() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + let handler = SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_series_details_handler_ready_when_not_loading_and_series_history_is_some() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.data.sonarr_data.series_history = Some(StatefulTable::default()); + + let handler = SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeriesHistory, + None, + ); + + assert!(handler.is_ready()); + } + + #[test] + fn test_series_details_handler_ready_when_not_loading_for_series_details() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + + let handler = SeriesDetailsHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::SeriesDetails, + None, + ); + + assert!(handler.is_ready()); + } + + fn sort_options() -> Vec> { + vec![SortOption { + name: "Test 1", + cmp_fn: Some(|a, b| { + b.source_title + .text + .to_lowercase() + .cmp(&a.source_title.text.to_lowercase()) + }), + }] + } +} diff --git a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs index 759a2d3..226e2dd 100644 --- a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs +++ b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs @@ -7,15 +7,15 @@ mod utils { ($handler:ident, $block:expr, $context:expr) => { let mut app = App::default(); let mut sonarr_data = SonarrData { - quality_profile_map: BiMap::from_iter([ + quality_profile_map: bimap::BiMap::from_iter([ (2222, "HD - 1080p".to_owned()), (1111, "Any".to_owned()), ]), - language_profiles_map: BiMap::from_iter([ + language_profiles_map: bimap::BiMap::from_iter([ (2222, "English".to_owned()), (1111, "Any".to_owned()), ]), - tags_map: BiMap::from_iter([(1, "test".to_owned())]), + tags_map: bimap::BiMap::from_iter([(1, "test".to_owned())]), ..create_test_sonarr_data() }; sonarr_data.series.set_items(vec![Series { diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index 70fd9ca..38ee436 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -10,6 +10,7 @@ use crate::{ }, sonarr::sonarr_context_clues::{ HISTORY_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES, + SERIES_HISTORY_CONTEXT_CLUES, }, }, models::{ @@ -80,6 +81,12 @@ impl<'a> SonarrData<'a> { self.delete_series_files = false; self.add_list_exclusion = false; } + + pub fn reset_series_info_tabs(&mut self) { + self.series_history = None; + self.seasons = StatefulTable::default(); + self.series_info_tabs.index = 0; + } } impl<'a> Default for SonarrData<'a> { @@ -174,7 +181,7 @@ impl<'a> Default for SonarrData<'a> { title: "History", route: ActiveSonarrBlock::SeriesHistory.into(), help: String::new(), - contextual_help: Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)), + contextual_help: Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)), }, ]), } @@ -304,12 +311,13 @@ pub static LIBRARY_BLOCKS: [ActiveSonarrBlock; 7] = [ ActiveSonarrBlock::UpdateAllSeriesPrompt, ]; -pub static SERIES_DETAILS_BLOCKS: [ActiveSonarrBlock; 11] = [ +pub static SERIES_DETAILS_BLOCKS: [ActiveSonarrBlock; 12] = [ ActiveSonarrBlock::SeriesDetails, ActiveSonarrBlock::SeriesHistory, ActiveSonarrBlock::SearchSeason, ActiveSonarrBlock::SearchSeasonError, ActiveSonarrBlock::UpdateAndScanSeriesPrompt, + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt, ActiveSonarrBlock::SearchSeriesHistory, ActiveSonarrBlock::SearchSeriesHistoryError, ActiveSonarrBlock::FilterSeriesHistory, diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index 28f2d34..6e82dc3 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -4,6 +4,9 @@ mod tests { use chrono::{DateTime, Utc}; use pretty_assertions::{assert_eq, assert_str_eq}; + use crate::app::sonarr::sonarr_context_clues::SERIES_HISTORY_CONTEXT_CLUES; + use crate::models::sonarr_models::{Season, SonarrHistoryItem}; + use crate::models::stateful_table::StatefulTable; use crate::{ app::{ context_clues::{ @@ -56,6 +59,24 @@ mod tests { assert!(!sonarr_data.add_list_exclusion); } + #[test] + fn test_reset_series_info_tabs() { + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); + let mut sonarr_data = SonarrData { + series_history: Some(series_history), + ..SonarrData::default() + }; + sonarr_data.seasons.set_items(vec![Season::default()]); + sonarr_data.series_info_tabs.index = 1; + + sonarr_data.reset_series_info_tabs(); + + assert!(sonarr_data.series_history.is_none()); + assert!(sonarr_data.seasons.is_empty()); + assert_eq!(sonarr_data.series_info_tabs.index, 0); + } + #[test] fn test_sonarr_data_defaults() { let sonarr_data = SonarrData::default(); @@ -195,7 +216,7 @@ mod tests { assert!(sonarr_data.series_info_tabs.tabs[1].help.is_empty()); assert_eq!( sonarr_data.series_info_tabs.tabs[1].contextual_help, - Some(build_context_clue_string(&HISTORY_CONTEXT_CLUES)) + Some(build_context_clue_string(&SERIES_HISTORY_CONTEXT_CLUES)) ); } } @@ -570,12 +591,13 @@ mod tests { #[test] fn test_series_details_blocks_contents() { - assert_eq!(SERIES_DETAILS_BLOCKS.len(), 11); + assert_eq!(SERIES_DETAILS_BLOCKS.len(), 12); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeriesDetails)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SeriesHistory)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeason)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeasonError)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::UpdateAndScanSeriesPrompt)); + assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::AutomaticallySearchSeriesPrompt)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeriesHistory)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::SearchSeriesHistoryError)); assert!(SERIES_DETAILS_BLOCKS.contains(&ActiveSonarrBlock::FilterSeriesHistory)); diff --git a/src/models/servarr_data/sonarr/sonarr_test_utils.rs b/src/models/servarr_data/sonarr/sonarr_test_utils.rs index 4919c67..bb7b669 100644 --- a/src/models/servarr_data/sonarr/sonarr_test_utils.rs +++ b/src/models/servarr_data/sonarr/sonarr_test_utils.rs @@ -32,6 +32,8 @@ pub mod utils { let mut seasons = StatefulTable::default(); seasons.set_items(vec![Season::default()]); + let mut series_history = StatefulTable::default(); + series_history.set_items(vec![SonarrHistoryItem::default()]); let mut sonarr_data = SonarrData { delete_series_files: true, @@ -39,6 +41,7 @@ pub mod utils { add_series_search: Some("test search".into()), edit_root_folder: Some("test path".into()), seasons, + series_history: Some(series_history), season_details_modal: Some(season_details_modal), add_searched_series: Some(StatefulTable::default()), ..SonarrData::default() diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index f3c9e9b..b133d83 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -239,6 +239,7 @@ impl Eq for Rating {} #[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Season { + pub title: Option, #[serde(deserialize_with = "super::from_i64")] pub season_number: i64, pub monitored: bool, diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index ce27e68..684dbca 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -3,7 +3,7 @@ mod test { use std::sync::Arc; use bimap::BiMap; - use chrono::{DateTime, Utc}; + use chrono::DateTime; use indoc::formatdoc; use mockito::{Matcher, Server}; use pretty_assertions::{assert_eq, assert_str_eq}; @@ -5237,8 +5237,7 @@ mod test { ) .await; let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap()) - as DateTime; + let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap()); if let SonarrSerdeable::SystemStatus(status) = network .handle_sonarr_event(SonarrEvent::GetStatus) @@ -6874,6 +6873,7 @@ mod test { fn season() -> Season { Season { + title: None, season_number: 1, monitored: true, statistics: season_statistics(), diff --git a/src/ui/radarr_ui/library/movie_details_ui.rs b/src/ui/radarr_ui/library/movie_details_ui.rs index 2b2bd47..6ef4fc6 100644 --- a/src/ui/radarr_ui/library/movie_details_ui.rs +++ b/src/ui/radarr_ui/library/movie_details_ui.rs @@ -60,7 +60,6 @@ impl DrawUi for MovieDetailsUi { .prompt(&prompt) .yes_no_value(app.data.radarr_data.prompt_confirm); - draw_movie_info(f, app, content_area); f.render_widget( Popup::new(confirmation_prompt).size(Size::MediumPrompt), f.area(), diff --git a/src/ui/sonarr_ui/library/edit_series_ui.rs b/src/ui/sonarr_ui/library/edit_series_ui.rs index 3d25141..93a500e 100644 --- a/src/ui/sonarr_ui/library/edit_series_ui.rs +++ b/src/ui/sonarr_ui/library/edit_series_ui.rs @@ -82,7 +82,7 @@ impl DrawUi for EditSeriesUi { } _ if SERIES_DETAILS_BLOCKS.contains(&context) => { draw_popup_over_ui::(f, app, area, draw_library, Size::Large); - draw_popup(f, app, draw_edit_series_prompt, Size::Medium); + draw_popup(f, app, draw_edit_series_prompt, Size::Long); } _ => (), } diff --git a/src/ui/sonarr_ui/library/library_ui_tests.rs b/src/ui/sonarr_ui/library/library_ui_tests.rs index 7c0c075..88191ba 100644 --- a/src/ui/sonarr_ui/library/library_ui_tests.rs +++ b/src/ui/sonarr_ui/library/library_ui_tests.rs @@ -12,11 +12,11 @@ mod tests { use crate::ui::DrawUi; use pretty_assertions::assert_eq; use ratatui::widgets::{Cell, Row}; - use rstest::rstest; use strum::IntoEnumIterator; + use crate::models::sonarr_models::{Season, SeasonStatistics}; use crate::{ - models::sonarr_models::{Series, SeriesStatistics}, + models::sonarr_models::Series, ui::sonarr_ui::library::decorate_series_row_with_style, }; @@ -38,45 +38,193 @@ mod tests { }); } - #[rstest] - #[case(SeriesStatus::Ended, None, RowStyle::Missing)] - #[case(SeriesStatus::Ended, Some(59.0), RowStyle::Missing)] - #[case(SeriesStatus::Ended, Some(100.0), RowStyle::Downloaded)] - #[case(SeriesStatus::Continuing, None, RowStyle::Missing)] - #[case(SeriesStatus::Continuing, Some(59.0), RowStyle::Missing)] - #[case(SeriesStatus::Continuing, Some(100.0), RowStyle::Unreleased)] - #[case(SeriesStatus::Upcoming, None, RowStyle::Unreleased)] - #[case(SeriesStatus::Deleted, None, RowStyle::Missing)] - fn test_decorate_series_row_with_style( - #[case] series_status: SeriesStatus, - #[case] percent_of_episodes: Option, - #[case] expected_row_style: RowStyle, + #[test] + fn test_decorate_row_with_style_downloaded_when_ended_and_all_monitored_episodes_are_present( ) { - let mut series = Series { - status: series_status, + let seasons = vec![ + Season { + monitored: false, + statistics: SeasonStatistics { + episode_count: 1, + total_episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + Season { + monitored: true, + statistics: SeasonStatistics { + episode_count: 3, + total_episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + ]; + let series = Series { + status: SeriesStatus::Ended, + seasons: Some(seasons), ..Series::default() }; - if let Some(percentage) = percent_of_episodes { - series.statistics = Some(SeriesStatistics { - percent_of_episodes: percentage, - ..SeriesStatistics::default() - }); - } - let row = Row::new(vec![Cell::from("test".to_owned())]); let style = decorate_series_row_with_style(&series, row.clone()); - match expected_row_style { - RowStyle::Downloaded => assert_eq!(style, row.downloaded()), - RowStyle::Missing => assert_eq!(style, row.missing()), - RowStyle::Unreleased => assert_eq!(style, row.unreleased()), - } + assert_eq!(style, row.downloaded()); } - enum RowStyle { - Downloaded, - Missing, - Unreleased, + #[test] + fn test_decorate_row_with_style_missing_when_ended_and_episodes_are_missing() { + let seasons = vec![ + Season { + monitored: true, + statistics: SeasonStatistics { + episode_count: 1, + total_episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + Season { + monitored: true, + statistics: SeasonStatistics { + episode_count: 3, + total_episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + ]; + let series = Series { + status: SeriesStatus::Ended, + seasons: Some(seasons), + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_ended_and_seasons_is_empty() { + let series = Series { + status: SeriesStatus::Ended, + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + #[test] + fn test_decorate_row_with_style_unreleased_when_continuing_and_all_monitored_episodes_are_present( + ) { + let seasons = vec![ + Season { + monitored: false, + statistics: SeasonStatistics { + episode_count: 1, + total_episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + Season { + monitored: true, + statistics: SeasonStatistics { + episode_count: 3, + total_episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + ]; + let series = Series { + status: SeriesStatus::Continuing, + seasons: Some(seasons), + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.unreleased()); + } + + #[test] + fn test_decorate_row_with_style_missing_when_continuing_and_episodes_are_missing() { + let seasons = vec![ + Season { + monitored: true, + statistics: SeasonStatistics { + episode_count: 1, + total_episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + Season { + monitored: true, + statistics: SeasonStatistics { + episode_count: 3, + total_episode_count: 3, + ..SeasonStatistics::default() + }, + ..Season::default() + }, + ]; + let series = Series { + status: SeriesStatus::Continuing, + seasons: Some(seasons), + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.missing()); + } + + #[test] + fn test_decorate_row_with_style_indeterminate_when_continuing_and_seasons_is_empty() { + let series = Series { + status: SeriesStatus::Continuing, + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.indeterminate()); + } + + #[test] + fn test_decorate_row_with_style_unreleased_when_upcoming() { + let series = Series { + status: SeriesStatus::Upcoming, + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.unreleased()); + } + + #[test] + fn test_decorate_row_with_style_defaults_to_indeterminate() { + let series = Series { + status: SeriesStatus::Deleted, + ..Series::default() + }; + let row = Row::new(vec![Cell::from("test".to_owned())]); + + let style = decorate_series_row_with_style(&series, row.clone()); + + assert_eq!(style, row.indeterminate()); } } diff --git a/src/ui/sonarr_ui/library/mod.rs b/src/ui/sonarr_ui/library/mod.rs index 930ecca..e50a994 100644 --- a/src/ui/sonarr_ui/library/mod.rs +++ b/src/ui/sonarr_ui/library/mod.rs @@ -61,35 +61,6 @@ impl DrawUi for LibraryUi { | ActiveSonarrBlock::SearchSeriesError | ActiveSonarrBlock::FilterSeries | ActiveSonarrBlock::FilterSeriesError => draw_library(f, app, area), - // ActiveSonarrBlock::SearchSeries => draw_popup_over( - // f, - // app, - // area, - // draw_library, - // draw_library_search_box, - // Size::InputBox, - // ), - // ActiveSonarrBlock::SearchSeriesError => { - // let popup = Popup::new(Message::new("Series not found!")).size(Size::Message); - - // draw_library(f, app, area); - // f.render_widget(popup, f.area()); - // } - // ActiveSonarrBlock::FilterSeries => draw_popup_over( - // f, - // app, - // area, - // draw_library, - // draw_filter_series_box, - // Size::InputBox, - // ), - // ActiveSonarrBlock::FilterSeriesError => { - // let popup = Popup::new(Message::new("No series found matching the given filter!")) - // .size(Size::Message); - - // draw_library(f, app, area); - // f.render_widget(popup, f.area()); - // } ActiveSonarrBlock::UpdateAllSeriesPrompt => { let confirmation_prompt = ConfirmationPrompt::new() .title("Update All Series") @@ -234,24 +205,36 @@ pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn decorate_series_row_with_style<'a>(series: &Series, row: Row<'a>) -> Row<'a> { match series.status { SeriesStatus::Ended => { - if let Some(ref stats) = series.statistics { - if stats.percent_of_episodes == 100.0 { - return row.downloaded(); + if let Some(ref seasons) = series.seasons { + return if seasons + .iter() + .filter(|season| season.monitored) + .all(|season| season.statistics.episode_count == season.statistics.total_episode_count) + { + row.downloaded() + } else { + row.missing() } - } - - row.missing() + } + + row.indeterminate() } SeriesStatus::Continuing => { - if let Some(ref stats) = series.statistics { - if stats.percent_of_episodes == 100.0 { - return row.unreleased(); - } + if let Some(ref seasons) = series.seasons { + return if seasons + .iter() + .filter(|season| season.monitored) + .all(|season| season.statistics.episode_count == season.statistics.total_episode_count) + { + row.unreleased() + } else { + row.missing() + }; } - row.missing() + row.indeterminate() } SeriesStatus::Upcoming => row.unreleased(), - _ => row.missing(), + _ => row.indeterminate(), } } diff --git a/src/ui/sonarr_ui/library/series_details_ui.rs b/src/ui/sonarr_ui/library/series_details_ui.rs index d673881..63d6ca5 100644 --- a/src/ui/sonarr_ui/library/series_details_ui.rs +++ b/src/ui/sonarr_ui/library/series_details_ui.rs @@ -1,8 +1,10 @@ +use deunicode::deunicode; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::{Line, Text}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use ratatui::Frame; +use regex::Regex; use crate::app::App; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SERIES_DETAILS_BLOCKS}; @@ -67,6 +69,20 @@ impl DrawUi for SeriesDetailsUi { draw_series_details(f, app, content_area); match active_sonarr_block { + ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => { + let prompt = format!( + "Do you want to trigger an automatic search of your indexers for all monitored episode(s) for the series: {}", app.data.sonarr_data.series.current_selection().title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Automatic Series Search") + .prompt(&prompt) + .yes_no_value(app.data.sonarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } ActiveSonarrBlock::UpdateAndScanSeriesPrompt => { let prompt = format!( "Do you want to trigger an update and disk scan for the series: {}?", @@ -83,14 +99,7 @@ impl DrawUi for SeriesDetailsUi { ); } ActiveSonarrBlock::SeriesHistoryDetails => { - draw_popup_over( - f, - app, - popup_area, - draw_series_history_table, - draw_history_item_details_popup, - Size::Small, - ); + draw_history_item_details_popup(f, app, popup_area); } _ => (), }; @@ -129,19 +138,25 @@ pub fn draw_series_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) .get_by_left(¤t_selection.language_profile_id) .unwrap() .to_owned(); + let overview = Regex::new(r"[\r\n\t]") + .unwrap() + .replace_all( + &deunicode( + current_selection + .overview + .as_ref() + .unwrap_or(&String::new()), + ), + "", + ) + .to_string(); + let mut series_description = vec![ Line::from(vec![ "Title: ".primary().bold(), current_selection.title.text.clone().primary().bold(), ]), - Line::from(vec![ - "Overview: ".primary().bold(), - current_selection - .overview - .clone() - .unwrap_or_default() - .default(), - ]), + Line::from(vec!["Overview: ".primary().bold(), overview.default()]), Line::from(vec![ "Network: ".primary().bold(), current_selection @@ -194,7 +209,7 @@ pub fn draw_series_description(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) let description_paragraph = Paragraph::new(series_description) .block(borderless_block()) - .wrap(Wrap { trim: false }); + .wrap(Wrap { trim: true }); f.render_widget(description_paragraph, area); } @@ -220,9 +235,10 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .get_active_tab_contextual_help(); let season_row_mapping = |season: &Season| { let Season { - season_number, + title, monitored, statistics, + .. } = season; let SeasonStatistics { episode_count, @@ -235,7 +251,7 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let row = Row::new(vec![ Cell::from(season_monitored.to_owned()), - Cell::from(format!("Season {}", season_number)), + Cell::from(title.clone().unwrap()), Cell::from(format!("{}/{}", episode_count, total_episode_count)), Cell::from(format!("{size:.2} GB")), ]); diff --git a/src/ui/styles.rs b/src/ui/styles.rs index 744f633..6d5e055 100644 --- a/src/ui/styles.rs +++ b/src/ui/styles.rs @@ -14,6 +14,7 @@ where #[allow(clippy::new_ret_no_self)] fn new() -> T; fn awaiting_import(self) -> T; + fn indeterminate(self) -> T; fn default(self) -> T; fn downloaded(self) -> T; fn downloading(self) -> T; @@ -44,6 +45,10 @@ where self.fg(COLOR_ORANGE) } + fn indeterminate(self) -> T { + self.fg(COLOR_ORANGE) + } + fn default(self) -> T { self.white() } diff --git a/src/ui/styles_tests.rs b/src/ui/styles_tests.rs index 55169f7..e32bf2a 100644 --- a/src/ui/styles_tests.rs +++ b/src/ui/styles_tests.rs @@ -18,6 +18,14 @@ mod test { ); } + #[test] + fn test_style_indeterminate() { + assert_eq!( + Style::new().indeterminate(), + Style::new().fg(COLOR_ORANGE) + ); + } + #[test] fn test_style_default() { assert_eq!(Style::new().default(), Style::new().white()); diff --git a/src/ui/widgets/button.rs b/src/ui/widgets/button.rs index 26217b9..2c3524d 100644 --- a/src/ui/widgets/button.rs +++ b/src/ui/widgets/button.rs @@ -1,50 +1,26 @@ use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{layout_block, style_block_highlight}; +use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::prelude::{Style, Text, Widget}; use ratatui::style::Styled; use ratatui::widgets::Paragraph; -#[cfg(test)] -#[path = "button_tests.rs"] -mod button_tests; - -#[derive(Default)] +#[derive(Default, Setters)] pub struct Button<'a> { title: &'a str, + #[setters(strip_option)] label: Option<&'a str>, + #[setters(strip_option)] icon: Option<&'a str>, + #[setters(into)] style: Style, + #[setters(rename = "selected")] is_selected: bool, } impl<'a> Button<'a> { - pub fn title(mut self, title: &'a str) -> Button<'a> { - self.title = title; - self - } - - pub fn label(mut self, label: &'a str) -> Button<'a> { - self.label = Some(label); - self - } - - pub fn icon(mut self, icon: &'a str) -> Button<'a> { - self.icon = Some(icon); - self - } - - pub fn style>(mut self, style: S) -> Button<'a> { - self.style = style.into(); - self - } - - pub fn selected(mut self, is_selected: bool) -> Button<'a> { - self.is_selected = is_selected; - self - } - fn render_button_with_icon(self, area: Rect, buf: &mut Buffer) { let [title_area, icon_area] = Layout::horizontal([ Constraint::Length(self.title.len() as u16), diff --git a/src/ui/widgets/button_tests.rs b/src/ui/widgets/button_tests.rs deleted file mode 100644 index 7273a76..0000000 --- a/src/ui/widgets/button_tests.rs +++ /dev/null @@ -1,41 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::ui::widgets::button::Button; - use pretty_assertions::{assert_eq, assert_str_eq}; - use ratatui::style::{Style, Stylize}; - - #[test] - fn test_title() { - let button = Button::default().title("Title"); - - assert_str_eq!(button.title, "Title"); - } - - #[test] - fn test_label() { - let button = Button::default().label("Label"); - - assert_eq!(button.label, Some("Label")); - } - - #[test] - fn test_icon() { - let button = Button::default().icon("Icon"); - - assert_eq!(button.icon, Some("Icon")); - } - - #[test] - fn test_style() { - let button = Button::default().style(Style::new().bold()); - - assert_eq!(button.style, Style::new().bold()); - } - - #[test] - fn test_selected() { - let button = Button::default().selected(true); - - assert!(button.is_selected); - } -} diff --git a/src/ui/widgets/checkbox.rs b/src/ui/widgets/checkbox.rs index 08ee9d2..14dc882 100644 --- a/src/ui/widgets/checkbox.rs +++ b/src/ui/widgets/checkbox.rs @@ -1,3 +1,4 @@ +use derive_setters::Setters; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{borderless_block, layout_block, style_block_highlight}; use ratatui::buffer::Buffer; @@ -6,14 +7,13 @@ use ratatui::prelude::Text; use ratatui::style::Stylize; use ratatui::widgets::{Paragraph, Widget}; -#[cfg(test)] -#[path = "checkbox_tests.rs"] -mod checkbox_tests; - -#[derive(PartialEq, Debug, Copy, Clone)] +#[derive(PartialEq, Debug, Copy, Clone, Setters)] pub struct Checkbox<'a> { + #[setters(skip)] label: &'a str, + #[setters(rename = "checked")] is_checked: bool, + #[setters(rename = "highlighted")] is_highlighted: bool, } @@ -26,16 +26,6 @@ impl<'a> Checkbox<'a> { } } - pub fn checked(mut self, is_checked: bool) -> Checkbox<'a> { - self.is_checked = is_checked; - self - } - - pub fn highlighted(mut self, is_selected: bool) -> Checkbox<'a> { - self.is_highlighted = is_selected; - self - } - fn render_checkbox(self, area: Rect, buf: &mut Buffer) { let check = if self.is_checked { "✔" } else { "" }; let [label_area, checkbox_area] = diff --git a/src/ui/widgets/checkbox_tests.rs b/src/ui/widgets/checkbox_tests.rs deleted file mode 100644 index 7af1bba..0000000 --- a/src/ui/widgets/checkbox_tests.rs +++ /dev/null @@ -1,32 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::ui::widgets::checkbox::Checkbox; - use pretty_assertions::assert_str_eq; - - #[test] - fn test_checkbox_new() { - let checkbox = Checkbox::new("test"); - - assert_str_eq!(checkbox.label, "test"); - assert!(!checkbox.is_checked); - assert!(!checkbox.is_highlighted); - } - - #[test] - fn test_checkbox_checked() { - let checkbox = Checkbox::new("test").checked(true); - - assert_str_eq!(checkbox.label, "test"); - assert!(checkbox.is_checked); - assert!(!checkbox.is_highlighted); - } - - #[test] - fn test_checkbox_highlighted() { - let checkbox = Checkbox::new("test").highlighted(true); - - assert_str_eq!(checkbox.label, "test"); - assert!(!checkbox.is_checked); - assert!(checkbox.is_highlighted); - } -} diff --git a/src/ui/widgets/confirmation_prompt.rs b/src/ui/widgets/confirmation_prompt.rs index 911bcaa..8679204 100644 --- a/src/ui/widgets/confirmation_prompt.rs +++ b/src/ui/widgets/confirmation_prompt.rs @@ -8,15 +8,19 @@ use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::text::Text; use ratatui::widgets::{Paragraph, Widget}; use std::iter; +use derive_setters::Setters; #[cfg(test)] #[path = "confirmation_prompt_tests.rs"] mod confirmation_prompt_tests; +#[derive(Setters)] pub struct ConfirmationPrompt<'a> { title: &'a str, prompt: &'a str, + #[setters(strip_option)] content: Option>, + #[setters(strip_option)] checkboxes: Option>>, yes_no_value: bool, yes_no_highlighted: bool, @@ -34,36 +38,6 @@ impl<'a> ConfirmationPrompt<'a> { } } - pub fn title(mut self, title: &'a str) -> Self { - self.title = title; - self - } - - pub fn prompt(mut self, prompt: &'a str) -> Self { - self.prompt = prompt; - self - } - - pub fn content(mut self, content: Paragraph<'a>) -> Self { - self.content = Some(content); - self - } - - pub fn checkboxes(mut self, checkboxes: Vec>) -> Self { - self.checkboxes = Some(checkboxes); - self - } - - pub fn yes_no_value(mut self, yes_highlighted: bool) -> Self { - self.yes_no_value = yes_highlighted; - self - } - - pub fn yes_no_highlighted(mut self, yes_highlighted: bool) -> Self { - self.yes_no_highlighted = yes_highlighted; - self - } - fn render_confirmation_prompt_with_checkboxes(self, area: Rect, buf: &mut Buffer) { title_block_centered(self.title).render(area, buf); let help_text = diff --git a/src/ui/widgets/confirmation_prompt_tests.rs b/src/ui/widgets/confirmation_prompt_tests.rs index 6173ef7..61d98d2 100644 --- a/src/ui/widgets/confirmation_prompt_tests.rs +++ b/src/ui/widgets/confirmation_prompt_tests.rs @@ -1,9 +1,7 @@ #[cfg(test)] mod tests { - use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use pretty_assertions::{assert_eq, assert_str_eq}; - use ratatui::widgets::Paragraph; #[test] fn test_confirmation_prompt_new() { @@ -16,78 +14,4 @@ mod tests { assert!(!confirmation_prompt.yes_no_value); assert!(confirmation_prompt.yes_no_highlighted); } - - #[test] - fn test_confirmation_prompt_title() { - let confirmation_prompt = ConfirmationPrompt::new().title("title"); - - assert_str_eq!(confirmation_prompt.title, "title"); - assert_str_eq!(confirmation_prompt.prompt, ""); - assert_eq!(confirmation_prompt.content, None); - assert_eq!(confirmation_prompt.checkboxes, None); - assert!(!confirmation_prompt.yes_no_value); - assert!(confirmation_prompt.yes_no_highlighted); - } - - #[test] - fn test_confirmation_prompt_prompt() { - let confirmation_prompt = ConfirmationPrompt::new().prompt("prompt"); - - assert_str_eq!(confirmation_prompt.prompt, "prompt"); - assert_str_eq!(confirmation_prompt.title, ""); - assert_eq!(confirmation_prompt.content, None); - assert_eq!(confirmation_prompt.checkboxes, None); - assert!(!confirmation_prompt.yes_no_value); - assert!(confirmation_prompt.yes_no_highlighted); - } - - #[test] - fn test_confirmation_prompt_content() { - let content = Paragraph::new("content"); - let confirmation_prompt = ConfirmationPrompt::new().content(content.clone()); - - assert_eq!(confirmation_prompt.content, Some(content)); - assert_str_eq!(confirmation_prompt.title, ""); - assert_str_eq!(confirmation_prompt.prompt, ""); - assert_eq!(confirmation_prompt.checkboxes, None); - assert!(!confirmation_prompt.yes_no_value); - assert!(confirmation_prompt.yes_no_highlighted); - } - - #[test] - fn test_confirmation_prompt_checkboxes() { - let checkboxes = vec![Checkbox::new("test").highlighted(true).checked(false)]; - let confirmation_prompt = ConfirmationPrompt::new().checkboxes(checkboxes.clone()); - - assert_eq!(confirmation_prompt.checkboxes, Some(checkboxes)); - assert_str_eq!(confirmation_prompt.title, ""); - assert_str_eq!(confirmation_prompt.prompt, ""); - assert_eq!(confirmation_prompt.content, None); - assert!(!confirmation_prompt.yes_no_value); - assert!(confirmation_prompt.yes_no_highlighted); - } - - #[test] - fn test_confirmation_prompt_yes_no_value() { - let confirmation_prompt = ConfirmationPrompt::new().yes_no_value(true); - - assert!(confirmation_prompt.yes_no_value); - assert_str_eq!(confirmation_prompt.title, ""); - assert_str_eq!(confirmation_prompt.prompt, ""); - assert_eq!(confirmation_prompt.content, None); - assert_eq!(confirmation_prompt.checkboxes, None); - assert!(confirmation_prompt.yes_no_highlighted); - } - - #[test] - fn test_confirmation_prompt_yes_no_highlighted() { - let confirmation_prompt = ConfirmationPrompt::new().yes_no_highlighted(false); - - assert!(!confirmation_prompt.yes_no_highlighted); - assert_str_eq!(confirmation_prompt.title, ""); - assert_str_eq!(confirmation_prompt.prompt, ""); - assert_eq!(confirmation_prompt.content, None); - assert_eq!(confirmation_prompt.checkboxes, None); - assert!(!confirmation_prompt.yes_no_value); - } } diff --git a/src/ui/widgets/input_box.rs b/src/ui/widgets/input_box.rs index 96577b1..e0ec7e9 100644 --- a/src/ui/widgets/input_box.rs +++ b/src/ui/widgets/input_box.rs @@ -1,3 +1,4 @@ +use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Layout, Position, Rect}; use ratatui::prelude::Text; @@ -12,16 +13,20 @@ use crate::ui::utils::{borderless_block, layout_block}; #[path = "input_box_tests.rs"] mod input_box_tests; -#[derive(Default)] +#[derive(Default, Setters)] #[cfg_attr(test, derive(Debug, PartialEq))] pub struct InputBox<'a> { content: &'a str, offset: usize, + #[setters(into)] style: Style, block: Block<'a>, + #[setters(strip_option)] label: Option<&'a str>, cursor_after_string: bool, + #[setters(rename = "highlighted", strip_option)] is_highlighted: Option, + #[setters(rename = "selected", strip_option)] is_selected: Option, } @@ -39,41 +44,6 @@ impl<'a> InputBox<'a> { } } - pub fn style>(mut self, style: S) -> InputBox<'a> { - self.style = style.into(); - self - } - - pub fn block(mut self, block: Block<'a>) -> InputBox<'a> { - self.block = block; - self - } - - pub fn label(mut self, label: &'a str) -> InputBox<'a> { - self.label = Some(label); - self - } - - pub fn offset(mut self, offset: usize) -> InputBox<'a> { - self.offset = offset; - self - } - - pub fn cursor_after_string(mut self, cursor_after_string: bool) -> InputBox<'a> { - self.cursor_after_string = cursor_after_string; - self - } - - pub fn highlighted(mut self, is_highlighted: bool) -> InputBox<'a> { - self.is_highlighted = Some(is_highlighted); - self - } - - pub fn selected(mut self, is_selected: bool) -> InputBox<'a> { - self.is_selected = Some(is_selected); - self - } - pub fn is_selected(&self) -> bool { self.is_selected.unwrap_or_default() } diff --git a/src/ui/widgets/input_box_tests.rs b/src/ui/widgets/input_box_tests.rs index d4d87b8..a26a05e 100644 --- a/src/ui/widgets/input_box_tests.rs +++ b/src/ui/widgets/input_box_tests.rs @@ -20,104 +20,6 @@ mod tests { assert_eq!(input_box.is_selected, None); } - #[test] - fn test_input_box_style() { - let input_box = InputBox::new("test").style(Style::new().highlight()); - - assert_eq!(input_box.style, Style::new().highlight()); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.block, layout_block()); - assert_eq!(input_box.label, None); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_highlighted, None); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_block() { - let input_box = InputBox::new("test").block(layout_block().title("title")); - - assert_eq!(input_box.block, layout_block().title("title")); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.label, None); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_highlighted, None); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_label() { - let input_box = InputBox::new("test").label("label"); - - assert_str_eq!(input_box.label.unwrap(), "label"); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.block, layout_block()); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_highlighted, None); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_offset() { - let input_box = InputBox::new("test").offset(1); - - assert_eq!(input_box.offset, 1); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.block, layout_block()); - assert_eq!(input_box.label, None); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_highlighted, None); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_cursor_after_string() { - let input_box = InputBox::new("test").cursor_after_string(false); - - assert!(!input_box.cursor_after_string); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.block, layout_block()); - assert_eq!(input_box.label, None); - assert_eq!(input_box.is_highlighted, None); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_highlighted() { - let input_box = InputBox::new("test").highlighted(true); - - assert_eq!(input_box.is_highlighted, Some(true)); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.block, layout_block()); - assert_eq!(input_box.label, None); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_selected, None); - } - - #[test] - fn test_input_box_selected() { - let input_box = InputBox::new("test").selected(true); - - assert_eq!(input_box.is_selected, Some(true)); - assert_str_eq!(input_box.content, "test"); - assert_eq!(input_box.offset, 0); - assert_eq!(input_box.style, Style::new().default()); - assert_eq!(input_box.block, layout_block()); - assert_eq!(input_box.label, None); - assert!(input_box.cursor_after_string); - assert_eq!(input_box.is_highlighted, None); - } - #[test] fn test_input_box_is_selected() { let input_box = InputBox::new("test").selected(true); diff --git a/src/ui/widgets/managarr_table.rs b/src/ui/widgets/managarr_table.rs index b11df86..e8f94d2 100644 --- a/src/ui/widgets/managarr_table.rs +++ b/src/ui/widgets/managarr_table.rs @@ -12,7 +12,7 @@ use ratatui::widgets::{Block, ListItem, Paragraph, Row, StatefulWidget, Table, W use ratatui::Frame; use std::fmt::Debug; use std::sync::atomic::Ordering; - +use derive_setters::Setters; use super::input_box_popup::InputBoxPopup; use super::message::Message; use super::popup::Size; @@ -21,24 +21,32 @@ use super::popup::Size; #[path = "managarr_table_tests.rs"] mod managarr_table_tests; +#[derive(Setters)] pub struct ManagarrTable<'a, T, F> where F: Fn(&T) -> Row<'a>, T: Clone + PartialEq + Eq + Debug, { + #[setters(strip_option)] content: Option<&'a mut StatefulTable>, + #[setters(skip)] table_headers: Vec, + #[setters(skip)] constraints: Vec, row_mapper: F, footer: Option, footer_alignment: Alignment, block: Block<'a>, margin: u16, + #[setters(rename = "loading")] is_loading: bool, highlight_rows: bool, + #[setters(rename = "sorting")] is_sorting: bool, + #[setters(rename = "searching")] is_searching: bool, search_produced_empty_results: bool, + #[setters(rename = "filtering")] is_filtering: bool, filter_produced_empty_results: bool, search_box_content_length: usize, @@ -107,61 +115,6 @@ where self } - pub fn footer(mut self, footer: Option) -> Self { - self.footer = footer; - self - } - - pub fn footer_alignment(mut self, alignment: Alignment) -> Self { - self.footer_alignment = alignment; - self - } - - pub fn block(mut self, block: Block<'a>) -> Self { - self.block = block; - self - } - - pub fn margin(mut self, margin: u16) -> Self { - self.margin = margin; - self - } - - pub fn loading(mut self, is_loading: bool) -> Self { - self.is_loading = is_loading; - self - } - - pub fn highlight_rows(mut self, highlight_rows: bool) -> Self { - self.highlight_rows = highlight_rows; - self - } - - pub fn sorting(mut self, is_sorting: bool) -> Self { - self.is_sorting = is_sorting; - self - } - - pub fn searching(mut self, is_searching: bool) -> Self { - self.is_searching = is_searching; - self - } - - pub fn search_produced_empty_results(mut self, no_search_results: bool) -> Self { - self.search_produced_empty_results = no_search_results; - self - } - - pub fn filtering(mut self, is_filtering: bool) -> Self { - self.is_filtering = is_filtering; - self - } - - pub fn filter_produced_empty_results(mut self, no_filter_results: bool) -> Self { - self.filter_produced_empty_results = no_filter_results; - self - } - fn render_table(self, area: Rect, buf: &mut Buffer) { let table_headers = self.parse_headers(); let table_area = if let Some(ref footer) = self.footer { diff --git a/src/ui/widgets/managarr_table_tests.rs b/src/ui/widgets/managarr_table_tests.rs index 3e43edf..831cd57 100644 --- a/src/ui/widgets/managarr_table_tests.rs +++ b/src/ui/widgets/managarr_table_tests.rs @@ -3,7 +3,6 @@ mod tests { use crate::models::stateful_list::StatefulList; use crate::models::stateful_table::{SortOption, StatefulTable}; use crate::models::{HorizontallyScrollableText, Scrollable}; - use crate::ui::utils::layout_block; use crate::ui::widgets::managarr_table::ManagarrTable; use pretty_assertions::assert_eq; use ratatui::layout::{Alignment, Constraint}; @@ -180,358 +179,6 @@ mod tests { assert_eq!(managarr_table.filter_box_offset, 0); } - #[test] - fn test_managarr_table_footer() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - let footer = "footer".to_owned(); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .footer(Some(footer.clone())); - - let row_mapper = managarr_table.row_mapper; - assert_eq!(managarr_table.footer, Some(footer)); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - assert!(!managarr_table.is_searching); - assert!(!managarr_table.search_produced_empty_results); - assert!(!managarr_table.is_filtering); - assert!(!managarr_table.filter_produced_empty_results); - assert_eq!(managarr_table.search_box_content_length, 0); - assert_eq!(managarr_table.search_box_offset, 0); - assert_eq!(managarr_table.filter_box_content_length, 0); - assert_eq!(managarr_table.filter_box_offset, 0); - } - - #[test] - fn test_managarr_table_footer_alignment() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .footer_alignment(Alignment::Center); - - let row_mapper = managarr_table.row_mapper; - assert_eq!(managarr_table.footer_alignment, Alignment::Center); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - assert!(!managarr_table.is_searching); - assert!(!managarr_table.search_produced_empty_results); - assert!(!managarr_table.is_filtering); - assert!(!managarr_table.filter_produced_empty_results); - assert_eq!(managarr_table.search_box_content_length, 0); - assert_eq!(managarr_table.search_box_offset, 0); - assert_eq!(managarr_table.filter_box_content_length, 0); - assert_eq!(managarr_table.filter_box_offset, 0); - } - - #[test] - fn test_managarr_table_block() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .block(layout_block()); - - let row_mapper = managarr_table.row_mapper; - assert_eq!(managarr_table.block, layout_block()); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - assert!(!managarr_table.is_searching); - assert!(!managarr_table.search_produced_empty_results); - assert!(!managarr_table.is_filtering); - assert!(!managarr_table.filter_produced_empty_results); - assert_eq!(managarr_table.search_box_content_length, 0); - assert_eq!(managarr_table.search_box_offset, 0); - assert_eq!(managarr_table.filter_box_content_length, 0); - assert_eq!(managarr_table.filter_box_offset, 0); - } - - #[test] - fn test_managarr_table_margin() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])).margin(1); - - let row_mapper = managarr_table.row_mapper; - assert_eq!(managarr_table.margin, 1); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - assert!(!managarr_table.is_searching); - assert!(!managarr_table.search_produced_empty_results); - assert!(!managarr_table.is_filtering); - assert!(!managarr_table.filter_produced_empty_results); - assert_eq!(managarr_table.search_box_content_length, 0); - assert_eq!(managarr_table.search_box_offset, 0); - assert_eq!(managarr_table.filter_box_content_length, 0); - assert_eq!(managarr_table.filter_box_offset, 0); - } - - #[test] - fn test_managarr_table_loading() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .loading(true); - - let row_mapper = managarr_table.row_mapper; - assert!(managarr_table.is_loading); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - assert!(!managarr_table.is_searching); - assert!(!managarr_table.search_produced_empty_results); - assert!(!managarr_table.is_filtering); - assert!(!managarr_table.filter_produced_empty_results); - assert_eq!(managarr_table.search_box_content_length, 0); - assert_eq!(managarr_table.search_box_offset, 0); - assert_eq!(managarr_table.filter_box_content_length, 0); - assert_eq!(managarr_table.filter_box_offset, 0); - } - - #[test] - fn test_managarr_table_highlight_rows() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .highlight_rows(false); - - let row_mapper = managarr_table.row_mapper; - assert!(!managarr_table.highlight_rows); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(!managarr_table.is_sorting); - assert!(!managarr_table.is_searching); - assert!(!managarr_table.search_produced_empty_results); - assert!(!managarr_table.is_filtering); - assert!(!managarr_table.filter_produced_empty_results); - assert_eq!(managarr_table.search_box_content_length, 0); - assert_eq!(managarr_table.search_box_offset, 0); - assert_eq!(managarr_table.filter_box_content_length, 0); - assert_eq!(managarr_table.filter_box_offset, 0); - } - - #[test] - fn test_managarr_table_sorting() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .sorting(true); - - let row_mapper = managarr_table.row_mapper; - assert!(managarr_table.is_sorting); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_searching); - assert!(!managarr_table.search_produced_empty_results); - assert!(!managarr_table.is_filtering); - assert!(!managarr_table.filter_produced_empty_results); - assert_eq!(managarr_table.search_box_content_length, 0); - assert_eq!(managarr_table.search_box_offset, 0); - assert_eq!(managarr_table.filter_box_content_length, 0); - assert_eq!(managarr_table.filter_box_offset, 0); - } - - #[test] - fn test_managarr_table_is_searching() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .searching(true); - - let row_mapper = managarr_table.row_mapper; - assert!(managarr_table.is_searching); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - assert!(!managarr_table.search_produced_empty_results); - assert!(!managarr_table.is_filtering); - assert!(!managarr_table.filter_produced_empty_results); - assert_eq!(managarr_table.search_box_content_length, 0); - assert_eq!(managarr_table.search_box_offset, 0); - assert_eq!(managarr_table.filter_box_content_length, 0); - assert_eq!(managarr_table.filter_box_offset, 0); - } - - #[test] - fn test_managarr_table_search_produced_empty_results() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .search_produced_empty_results(true); - - let row_mapper = managarr_table.row_mapper; - assert!(managarr_table.search_produced_empty_results); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - assert!(!managarr_table.is_searching); - assert!(!managarr_table.is_filtering); - assert!(!managarr_table.filter_produced_empty_results); - assert_eq!(managarr_table.search_box_content_length, 0); - assert_eq!(managarr_table.search_box_offset, 0); - assert_eq!(managarr_table.filter_box_content_length, 0); - assert_eq!(managarr_table.filter_box_offset, 0); - } - - #[test] - fn test_managarr_table_is_filtering() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .filtering(true); - - let row_mapper = managarr_table.row_mapper; - assert!(managarr_table.is_filtering); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - assert!(!managarr_table.is_searching); - assert!(!managarr_table.search_produced_empty_results); - assert!(!managarr_table.filter_produced_empty_results); - assert_eq!(managarr_table.search_box_content_length, 0); - assert_eq!(managarr_table.search_box_offset, 0); - assert_eq!(managarr_table.filter_box_content_length, 0); - assert_eq!(managarr_table.filter_box_offset, 0); - } - - #[test] - fn test_managarr_table_filter_produced_empty_results() { - let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .filter_produced_empty_results(true); - - let row_mapper = managarr_table.row_mapper; - assert!(managarr_table.filter_produced_empty_results); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); - assert_eq!(managarr_table.table_headers, Vec::::new()); - assert_eq!(managarr_table.constraints, Vec::new()); - assert_eq!(managarr_table.footer, None); - assert_eq!(managarr_table.footer_alignment, Alignment::Left); - assert_eq!(managarr_table.block, Block::new()); - assert_eq!(managarr_table.margin, 0); - assert!(!managarr_table.is_loading); - assert!(managarr_table.highlight_rows); - assert!(!managarr_table.is_sorting); - assert!(!managarr_table.is_searching); - assert!(!managarr_table.search_produced_empty_results); - assert!(!managarr_table.is_filtering); - assert_eq!(managarr_table.search_box_content_length, 0); - assert_eq!(managarr_table.search_box_offset, 0); - assert_eq!(managarr_table.filter_box_content_length, 0); - assert_eq!(managarr_table.filter_box_offset, 0); - } - #[test] fn test_managarr_table_parse_headers() { let items = vec!["item1", "item2", "item3"]; diff --git a/src/ui/widgets/message.rs b/src/ui/widgets/message.rs index 7543430..3cfc8f6 100644 --- a/src/ui/widgets/message.rs +++ b/src/ui/widgets/message.rs @@ -1,5 +1,6 @@ use crate::ui::styles::ManagarrStyle; use crate::ui::utils::title_block_centered; +use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::{Alignment, Rect}; use ratatui::style::{Style, Stylize}; @@ -10,6 +11,7 @@ use ratatui::widgets::{Paragraph, Widget, Wrap}; #[path = "message_tests.rs"] mod message_tests; +#[derive(Setters)] pub struct Message<'a> { text: Text<'a>, title: &'a str, @@ -30,21 +32,6 @@ impl<'a> Message<'a> { } } - pub fn title(mut self, title: &'a str) -> Self { - self.title = title; - self - } - - pub fn style(mut self, style: Style) -> Self { - self.style = style; - self - } - - pub fn alignment(mut self, alignment: Alignment) -> Self { - self.alignment = alignment; - self - } - fn render_message(self, area: Rect, buf: &mut Buffer) { Paragraph::new(self.text) .style(self.style) diff --git a/src/ui/widgets/message_tests.rs b/src/ui/widgets/message_tests.rs index cf72796..f15db96 100644 --- a/src/ui/widgets/message_tests.rs +++ b/src/ui/widgets/message_tests.rs @@ -18,42 +18,4 @@ mod tests { assert_eq!(message.style, Style::new().failure().bold()); assert_eq!(message.alignment, Alignment::Center); } - - #[test] - fn test_message_title() { - let test_message = "This is a message"; - let title = "Success"; - - let message = Message::new(test_message).title(title); - - assert_str_eq!(message.title, title); - assert_eq!(message.text, Text::from(test_message)); - assert_eq!(message.style, Style::new().failure().bold()); - assert_eq!(message.alignment, Alignment::Center); - } - - #[test] - fn test_message_style() { - let test_message = "This is a message"; - let style = Style::new().success().bold(); - - let message = Message::new(test_message).style(style); - - assert_eq!(message.style, style); - assert_eq!(message.text, Text::from(test_message)); - assert_str_eq!(message.title, "Error"); - assert_eq!(message.alignment, Alignment::Center); - } - - #[test] - fn test_message_alignment() { - let test_message = "This is a message"; - - let message = Message::new(test_message).alignment(Alignment::Left); - - assert_eq!(message.alignment, Alignment::Left); - assert_eq!(message.text, Text::from(test_message)); - assert_str_eq!(message.title, "Error"); - assert_eq!(message.style, Style::new().failure().bold()); - } }