From b1bdc19afb9bb1abde92254fc15c080c767641eb Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 2 Dec 2024 11:30:34 -0700 Subject: [PATCH] feat(handler): Support for deleting a series in Sonarr --- .../library/delete_series_handler.rs | 118 ++++++ .../library/delete_series_handler_tests.rs | 339 ++++++++++++++++++ .../library/library_handler_tests.rs | 17 +- src/handlers/sonarr_handlers/library/mod.rs | 14 +- src/models/servarr_data/sonarr/sonarr_data.rs | 7 + .../servarr_data/sonarr/sonarr_data_tests.rs | 13 +- 6 files changed, 495 insertions(+), 13 deletions(-) create mode 100644 src/handlers/sonarr_handlers/library/delete_series_handler.rs create mode 100644 src/handlers/sonarr_handlers/library/delete_series_handler_tests.rs diff --git a/src/handlers/sonarr_handlers/library/delete_series_handler.rs b/src/handlers/sonarr_handlers/library/delete_series_handler.rs new file mode 100644 index 0000000..a9fbeff --- /dev/null +++ b/src/handlers/sonarr_handlers/library/delete_series_handler.rs @@ -0,0 +1,118 @@ +use crate::{ + app::{key_binding::DEFAULT_KEYBINDINGS, App}, + event::Key, + handlers::{handle_prompt_toggle, KeyEventHandler}, + models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS}, + network::sonarr_network::SonarrEvent, +}; + +#[cfg(test)] +#[path = "delete_series_handler_tests.rs"] +mod delete_series_handler_tests; + +pub(super) struct DeleteSeriesHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for DeleteSeriesHandler<'a, 'b> { + fn accepts(active_block: ActiveSonarrBlock) -> bool { + DELETE_SERIES_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + _context: Option, + ) -> Self { + DeleteSeriesHandler { + key, + app, + active_sonarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading + } + + fn handle_scroll_up(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt { + self.app.data.sonarr_data.selected_block.up(); + } + } + + fn handle_scroll_down(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt { + self.app.data.sonarr_data.selected_block.down(); + } + } + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt { + handle_prompt_toggle(self.app, self.key); + } + } + + fn handle_submit(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt { + match self.app.data.sonarr_data.selected_block.get_active_block() { + ActiveSonarrBlock::DeleteSeriesConfirmPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteSeries(None)); + self.app.should_refresh = true; + } else { + self.app.data.sonarr_data.reset_delete_series_preferences(); + } + + self.app.pop_navigation_stack(); + } + ActiveSonarrBlock::DeleteSeriesToggleDeleteFile => { + self.app.data.sonarr_data.delete_series_files = + !self.app.data.sonarr_data.delete_series_files; + } + ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion => { + self.app.data.sonarr_data.add_list_exclusion = + !self.app.data.sonarr_data.add_list_exclusion; + } + _ => (), + } + } + } + + fn handle_esc(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.reset_delete_series_preferences(); + self.app.data.sonarr_data.prompt_confirm = false; + } + } + + fn handle_char_key_event(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::DeleteSeriesPrompt + && self.app.data.sonarr_data.selected_block.get_active_block() + == ActiveSonarrBlock::DeleteSeriesConfirmPrompt + && self.key == DEFAULT_KEYBINDINGS.confirm.key + { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::DeleteSeries(None)); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + } +} diff --git a/src/handlers/sonarr_handlers/library/delete_series_handler_tests.rs b/src/handlers/sonarr_handlers/library/delete_series_handler_tests.rs new file mode 100644 index 0000000..13469da --- /dev/null +++ b/src/handlers/sonarr_handlers/library/delete_series_handler_tests.rs @@ -0,0 +1,339 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::library::delete_series_handler::DeleteSeriesHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS}; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::models::servarr_data::sonarr::sonarr_data::DELETE_SERIES_SELECTION_BLOCKS; + use crate::models::BlockSelectionState; + + use super::*; + + #[rstest] + fn test_delete_series_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::default(); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + DeleteSeriesHandler::with(key, &mut app, ActiveSonarrBlock::DeleteSeriesPrompt, None) + .handle(); + + if key == Key::Up { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::DeleteSeriesToggleDeleteFile + ); + } else { + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::DeleteSeriesConfirmPrompt + ); + } + } + + #[rstest] + fn test_delete_series_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::default(); + app.is_loading = true; + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app.data.sonarr_data.selected_block.down(); + + DeleteSeriesHandler::with(key, &mut app, ActiveSonarrBlock::DeleteSeriesPrompt, None) + .handle(); + + assert_eq!( + app.data.sonarr_data.selected_block.get_active_block(), + ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion + ); + } + } + + mod test_handle_left_right_action { + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + + DeleteSeriesHandler::with(key, &mut app, ActiveSonarrBlock::DeleteSeriesPrompt, None) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + DeleteSeriesHandler::with(key, &mut app, ActiveSonarrBlock::DeleteSeriesPrompt, None) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::models::servarr_data::sonarr::sonarr_data::DELETE_SERIES_SELECTION_BLOCKS; + use crate::models::BlockSelectionState; + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_delete_series_prompt_prompt_decline_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, DELETE_SERIES_SELECTION_BLOCKS.len() - 1); + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + + DeleteSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(!app.data.sonarr_data.delete_series_files); + assert!(!app.data.sonarr_data.add_list_exclusion); + } + + #[test] + fn test_delete_series_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, DELETE_SERIES_SELECTION_BLOCKS.len() - 1); + + DeleteSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DeleteSeries(None)) + ); + assert!(app.should_refresh); + assert!(app.data.sonarr_data.prompt_confirm); + assert!(app.data.sonarr_data.delete_series_files); + assert!(app.data.sonarr_data.add_list_exclusion); + } + + #[test] + fn test_delete_series_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + + DeleteSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::DeleteSeriesPrompt.into() + ); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert!(!app.should_refresh); + assert!(app.data.sonarr_data.prompt_confirm); + assert!(app.data.sonarr_data.delete_series_files); + assert!(app.data.sonarr_data.add_list_exclusion); + } + + #[test] + fn test_delete_series_toggle_delete_files_submit() { + let current_route = ActiveSonarrBlock::DeleteSeriesPrompt.into(); + let mut app = App::default(); + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + + DeleteSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!(app.data.sonarr_data.delete_series_files, true); + + DeleteSeriesHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), current_route); + assert_eq!(app.data.sonarr_data.delete_series_files, false); + } + } + + mod test_handle_esc { + use super::*; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_delete_series_prompt_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + + DeleteSeriesHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(!app.data.sonarr_data.delete_series_files); + assert!(!app.data.sonarr_data.add_list_exclusion); + } + } + + mod test_handle_key_char { + use crate::{ + models::{ + servarr_data::sonarr::sonarr_data::DELETE_SERIES_SELECTION_BLOCKS, BlockSelectionState, + }, + network::sonarr_network::SonarrEvent, + }; + + use super::*; + + #[test] + fn test_delete_series_confirm_prompt_prompt_confirm() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::Series.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteSeriesPrompt.into()); + app.data.sonarr_data.delete_series_files = true; + app.data.sonarr_data.add_list_exclusion = true; + app.data.sonarr_data.selected_block = + BlockSelectionState::new(DELETE_SERIES_SELECTION_BLOCKS); + app + .data + .sonarr_data + .selected_block + .set_index(0, DELETE_SERIES_SELECTION_BLOCKS.len() - 1); + + DeleteSeriesHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Series.into()); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DeleteSeries(None)) + ); + assert!(app.should_refresh); + assert!(app.data.sonarr_data.prompt_confirm); + assert!(app.data.sonarr_data.delete_series_files); + assert!(app.data.sonarr_data.add_list_exclusion); + } + } + + #[test] + fn test_delete_series_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if DELETE_SERIES_BLOCKS.contains(&active_sonarr_block) { + assert!(DeleteSeriesHandler::accepts(active_sonarr_block)); + } else { + assert!(!DeleteSeriesHandler::accepts(active_sonarr_block)); + } + }); + } + + #[test] + fn test_delete_series_handler_not_ready_when_loading() { + let mut app = App::default(); + app.is_loading = true; + + let handler = DeleteSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_delete_series_handler_ready_when_not_loading() { + let mut app = App::default(); + app.is_loading = false; + + let handler = DeleteSeriesHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::DeleteSeriesPrompt, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/library/library_handler_tests.rs b/src/handlers/sonarr_handlers/library/library_handler_tests.rs index 9d5c93b..752be77 100644 --- a/src/handlers/sonarr_handlers/library/library_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/library_handler_tests.rs @@ -15,6 +15,7 @@ mod tests { use crate::models::sonarr_models::{Series, SeriesStatus, SeriesType}; use crate::models::stateful_table::SortOption; use crate::models::HorizontallyScrollableText; + use crate::test_handler_delegation; mod test_handle_scroll_up_and_down { use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; @@ -1500,14 +1501,14 @@ mod tests { // ); // } - // #[test] - // fn test_delegates_delete_series_blocks_to_delete_series_handler() { - // test_handler_delegation!( - // LibraryHandler, - // ActiveSonarrBlock::Series, - // ActiveSonarrBlock::DeleteSeriesPrompt - // ); - // } + #[test] + fn test_delegates_delete_series_blocks_to_delete_series_handler() { + test_handler_delegation!( + LibraryHandler, + ActiveSonarrBlock::Series, + ActiveSonarrBlock::DeleteSeriesPrompt + ); + } #[test] fn test_series_sorting_options_title() { diff --git a/src/handlers/sonarr_handlers/library/mod.rs b/src/handlers/sonarr_handlers/library/mod.rs index 55dc92e..ee61a54 100644 --- a/src/handlers/sonarr_handlers/library/mod.rs +++ b/src/handlers/sonarr_handlers/library/mod.rs @@ -1,3 +1,5 @@ +use delete_series_handler::DeleteSeriesHandler; + use crate::{ app::App, event::Key, @@ -18,6 +20,8 @@ use crate::{ use super::handle_change_tab_left_right_keys; use crate::app::key_binding::DEFAULT_KEYBINDINGS; +mod delete_series_handler; + #[cfg(test)] #[path = "library_handler_tests.rs"] mod library_handler_tests; @@ -26,12 +30,16 @@ pub(super) struct LibraryHandler<'a, 'b> { key: Key, app: &'a mut App<'b>, active_sonarr_block: ActiveSonarrBlock, - _context: Option, + context: Option, } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, 'b> { fn handle(&mut self) { match self.active_sonarr_block { + _ if DeleteSeriesHandler::accepts(self.active_sonarr_block) => { + DeleteSeriesHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle(); + } _ => self.handle_key_event(), } } @@ -44,13 +52,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, ' key: Key, app: &'a mut App<'b>, active_block: ActiveSonarrBlock, - _context: Option, + context: Option, ) -> LibraryHandler<'a, 'b> { LibraryHandler { key, app, active_sonarr_block: active_block, - _context, + context, } } diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index 6e77c89..f40807a 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -324,6 +324,13 @@ pub static DOWNLOADS_BLOCKS: [ActiveSonarrBlock; 3] = [ ActiveSonarrBlock::UpdateDownloadsPrompt, ]; +pub static DELETE_SERIES_BLOCKS: [ActiveSonarrBlock; 4] = [ + ActiveSonarrBlock::DeleteSeriesPrompt, + ActiveSonarrBlock::DeleteSeriesConfirmPrompt, + ActiveSonarrBlock::DeleteSeriesToggleDeleteFile, + ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion, +]; + pub const DELETE_SERIES_SELECTION_BLOCKS: &[&[ActiveSonarrBlock]] = &[ &[ActiveSonarrBlock::DeleteSeriesToggleDeleteFile], &[ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion], diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index aef207e..bf07cda 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -202,8 +202,8 @@ mod tests { mod active_sonarr_block_tests { use crate::models::servarr_data::sonarr::sonarr_data::{ - ActiveSonarrBlock, DELETE_SERIES_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_SERIES_BLOCKS, - EDIT_SERIES_SELECTION_BLOCKS, SERIES_BLOCKS, + ActiveSonarrBlock, DELETE_SERIES_BLOCKS, DELETE_SERIES_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, + EDIT_SERIES_BLOCKS, EDIT_SERIES_SELECTION_BLOCKS, SERIES_BLOCKS, }; #[test] @@ -279,6 +279,15 @@ mod tests { assert!(DOWNLOADS_BLOCKS.contains(&ActiveSonarrBlock::UpdateDownloadsPrompt)); } + #[test] + fn test_delete_series_blocks_contents() { + assert_eq!(DELETE_SERIES_BLOCKS.len(), 4); + assert!(DELETE_SERIES_BLOCKS.contains(&ActiveSonarrBlock::DeleteSeriesPrompt)); + assert!(DELETE_SERIES_BLOCKS.contains(&ActiveSonarrBlock::DeleteSeriesConfirmPrompt)); + assert!(DELETE_SERIES_BLOCKS.contains(&ActiveSonarrBlock::DeleteSeriesToggleDeleteFile)); + assert!(DELETE_SERIES_BLOCKS.contains(&ActiveSonarrBlock::DeleteSeriesToggleAddListExclusion)); + } + #[test] fn test_delete_series_selection_blocks_ordering() { let mut delete_series_block_iter = DELETE_SERIES_SELECTION_BLOCKS.iter();