diff --git a/src/handlers/sonarr_handlers/mod.rs b/src/handlers/sonarr_handlers/mod.rs index 83d277b..edf2cb8 100644 --- a/src/handlers/sonarr_handlers/mod.rs +++ b/src/handlers/sonarr_handlers/mod.rs @@ -2,6 +2,7 @@ use blocklist::BlocklistHandler; use downloads::DownloadsHandler; use history::HistoryHandler; use library::LibraryHandler; +use root_folders::RootFoldersHandler; use crate::{ app::{key_binding::DEFAULT_KEYBINDINGS, App}, @@ -15,6 +16,7 @@ mod blocklist; mod downloads; mod history; mod library; +mod root_folders; #[cfg(test)] #[path = "sonarr_handler_tests.rs"] @@ -46,6 +48,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SonarrHandler<'a, 'b _ if HistoryHandler::accepts(self.active_sonarr_block) => { HistoryHandler::with(self.key, self.app, self.active_sonarr_block, self.context).handle() } + _ if RootFoldersHandler::accepts(self.active_sonarr_block) => { + RootFoldersHandler::with(self.key, self.app, self.active_sonarr_block, self.context) + .handle() + } _ => self.handle_key_event(), } } diff --git a/src/handlers/sonarr_handlers/root_folders/mod.rs b/src/handlers/sonarr_handlers/root_folders/mod.rs new file mode 100644 index 0000000..cc18830 --- /dev/null +++ b/src/handlers/sonarr_handlers/root_folders/mod.rs @@ -0,0 +1,195 @@ +use crate::app::key_binding::DEFAULT_KEYBINDINGS; +use crate::app::App; +use crate::event::Key; +use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; +use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; +use crate::models::{HorizontallyScrollableText, Scrollable}; +use crate::network::sonarr_network::SonarrEvent; +use crate::{handle_text_box_keys, handle_text_box_left_right_keys}; + +#[cfg(test)] +#[path = "root_folders_handler_tests.rs"] +mod root_folders_handler_tests; + +pub(super) struct RootFoldersHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_sonarr_block: ActiveSonarrBlock, + _context: Option, +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<'a, 'b> { + fn accepts(active_block: ActiveSonarrBlock) -> bool { + ROOT_FOLDERS_BLOCKS.contains(&active_block) + } + + fn with( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveSonarrBlock, + _context: Option, + ) -> RootFoldersHandler<'a, 'b> { + RootFoldersHandler { + key, + app, + active_sonarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.sonarr_data.root_folders.is_empty() + } + + fn handle_scroll_up(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::RootFolders { + self.app.data.sonarr_data.root_folders.scroll_up() + } + } + + fn handle_scroll_down(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::RootFolders { + self.app.data.sonarr_data.root_folders.scroll_down() + } + } + + fn handle_home(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::RootFolders => self.app.data.sonarr_data.root_folders.scroll_to_top(), + ActiveSonarrBlock::AddRootFolderPrompt => self + .app + .data + .sonarr_data + .edit_root_folder + .as_mut() + .unwrap() + .scroll_home(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::RootFolders => self.app.data.sonarr_data.root_folders.scroll_to_bottom(), + ActiveSonarrBlock::AddRootFolderPrompt => self + .app + .data + .sonarr_data + .edit_root_folder + .as_mut() + .unwrap() + .reset_offset(), + _ => (), + } + } + + fn handle_delete(&mut self) { + if self.active_sonarr_block == ActiveSonarrBlock::RootFolders { + self + .app + .push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()) + } + } + + fn handle_left_right_action(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::RootFolders => handle_change_tab_left_right_keys(self.app, self.key), + ActiveSonarrBlock::DeleteRootFolderPrompt => handle_prompt_toggle(self.app, self.key), + ActiveSonarrBlock::AddRootFolderPrompt => { + handle_text_box_left_right_keys!( + self, + self.key, + self.app.data.sonarr_data.edit_root_folder.as_mut().unwrap() + ) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::DeleteRootFolderPrompt => { + if self.app.data.sonarr_data.prompt_confirm { + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteRootFolder(None)); + } + + self.app.pop_navigation_stack(); + } + _ if self.active_sonarr_block == ActiveSonarrBlock::AddRootFolderPrompt + && !self + .app + .data + .sonarr_data + .edit_root_folder + .as_ref() + .unwrap() + .text + .is_empty() => + { + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::AddRootFolder(None)); + self.app.data.sonarr_data.prompt_confirm = true; + self.app.should_ignore_quit_key = false; + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_sonarr_block { + ActiveSonarrBlock::AddRootFolderPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.edit_root_folder = None; + self.app.data.sonarr_data.prompt_confirm = false; + self.app.should_ignore_quit_key = false; + } + ActiveSonarrBlock::DeleteRootFolderPrompt => { + self.app.pop_navigation_stack(); + self.app.data.sonarr_data.prompt_confirm = false; + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_sonarr_block { + ActiveSonarrBlock::RootFolders => match self.key { + _ if key == DEFAULT_KEYBINDINGS.refresh.key => { + self.app.should_refresh = true; + } + _ if key == DEFAULT_KEYBINDINGS.add.key => { + self + .app + .push_navigation_stack(ActiveSonarrBlock::AddRootFolderPrompt.into()); + self.app.data.sonarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); + self.app.should_ignore_quit_key = true; + } + _ => (), + }, + ActiveSonarrBlock::AddRootFolderPrompt => { + handle_text_box_keys!( + self, + key, + self.app.data.sonarr_data.edit_root_folder.as_mut().unwrap() + ) + } + ActiveSonarrBlock::DeleteRootFolderPrompt => { + if key == DEFAULT_KEYBINDINGS.confirm.key { + self.app.data.sonarr_data.prompt_confirm = true; + self.app.data.sonarr_data.prompt_confirm_action = + Some(SonarrEvent::DeleteRootFolder(None)); + + self.app.pop_navigation_stack(); + } + } + _ => (), + } + } +} diff --git a/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs new file mode 100644 index 0000000..8c8947c --- /dev/null +++ b/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs @@ -0,0 +1,804 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_str_eq; + use strum::IntoEnumIterator; + + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::app::App; + use crate::event::Key; + use crate::handlers::sonarr_handlers::root_folders::RootFoldersHandler; + use crate::handlers::KeyEventHandler; + use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; + use crate::models::servarr_models::RootFolder; + use crate::models::HorizontallyScrollableText; + + mod test_handle_scroll_up_and_down { + use rstest::rstest; + + use crate::models::servarr_models::RootFolder; + use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; + + use super::*; + + test_iterable_scroll!( + test_root_folders_scroll, + RootFoldersHandler, + sonarr_data, + root_folders, + simple_stateful_iterable_vec!(RootFolder, String, path), + ActiveSonarrBlock::RootFolders, + None, + path + ); + + #[rstest] + fn test_root_folders_scroll_no_op_when_not_ready( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = true; + app + .data + .sonarr_data + .root_folders + .set_items(simple_stateful_iterable_vec!(RootFolder, String, path)); + + RootFoldersHandler::with(key, &mut app, ActiveSonarrBlock::RootFolders, None).handle(); + + assert_str_eq!( + app.data.sonarr_data.root_folders.current_selection().path, + "Test 1" + ); + + RootFoldersHandler::with(key, &mut app, ActiveSonarrBlock::RootFolders, None).handle(); + + assert_str_eq!( + app.data.sonarr_data.root_folders.current_selection().path, + "Test 1" + ); + } + } + + mod test_handle_home_end { + use std::sync::atomic::Ordering; + + use pretty_assertions::assert_eq; + + use crate::models::servarr_models::RootFolder; + use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; + + use super::*; + + test_iterable_home_and_end!( + test_root_folders_home_end, + RootFoldersHandler, + sonarr_data, + root_folders, + extended_stateful_iterable_vec!(RootFolder, String, path), + ActiveSonarrBlock::RootFolders, + None, + path + ); + + #[test] + fn test_root_folders_home_end_no_op_when_not_ready() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = true; + app + .data + .sonarr_data + .root_folders + .set_items(extended_stateful_iterable_vec!(RootFolder, String, path)); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.root_folders.current_selection().path, + "Test 1" + ); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.root_folders.current_selection().path, + "Test 1" + ); + } + + #[test] + fn test_add_root_folder_prompt_home_end_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.sonarr_data.edit_root_folder = Some("Test".into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 4 + ); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_root_folder_prompt() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::RootFolders, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::DeleteRootFolderPrompt.into() + ); + } + + #[test] + fn test_delete_root_folder_prompt_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::RootFolders, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + } + + mod test_handle_left_right_action { + use std::sync::atomic::Ordering; + + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_root_folders_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(4); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::History.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::History.into()); + } + + #[rstest] + fn test_root_folders_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = is_ready; + app.data.sonarr_data.main_tabs.set_index(4); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.data.sonarr_data.main_tabs.get_active_route(), + ActiveSonarrBlock::Indexers.into() + ); + assert_eq!(app.get_current_route(), ActiveSonarrBlock::Indexers.into()); + } + + #[rstest] + fn test_left_right_delete_root_folder_prompt_toggle( + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + + RootFoldersHandler::with( + key, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + + RootFoldersHandler::with( + key, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[test] + fn test_add_root_folder_prompt_left_right_keys() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.data.sonarr_data.edit_root_folder = Some("Test".into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 1 + ); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .sonarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_add_root_folder_prompt_confirm_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.sonarr_data.edit_root_folder = Some("Test".into()); + app.data.sonarr_data.prompt_confirm = true; + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddRootFolderPrompt.into()); + + RootFoldersHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert!(!app.should_ignore_quit_key); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::AddRootFolder(None)) + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + + #[test] + fn test_add_root_folder_prompt_confirm_submit_noop_on_empty_folder() { + let mut app = App::default(); + app.data.sonarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); + app.data.sonarr_data.prompt_confirm = false; + app.should_ignore_quit_key = true; + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddRootFolderPrompt.into()); + + RootFoldersHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(app.should_ignore_quit_key); + assert!(app.data.sonarr_data.prompt_confirm_action.is_none()); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddRootFolderPrompt.into() + ); + } + + #[test] + fn test_delete_root_folder_prompt_confirm_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.sonarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DeleteRootFolder(None)) + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + + #[test] + fn test_delete_root_folder_prompt_decline_submit() { + let mut app = App::default(); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::with( + SUBMIT_KEY, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(!app.data.sonarr_data.prompt_confirm); + assert_eq!(app.data.sonarr_data.prompt_confirm_action, None); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + } + + mod test_handle_esc { + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_delete_root_folder_prompt_block_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()); + app.data.sonarr_data.prompt_confirm = true; + + RootFoldersHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert!(!app.data.sonarr_data.prompt_confirm); + } + + #[test] + fn test_add_root_folder_prompt_esc() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::AddRootFolderPrompt.into()); + app.data.sonarr_data.edit_root_folder = Some("/nfs/test".into()); + app.should_ignore_quit_key = true; + + RootFoldersHandler::with( + ESC_KEY, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + + assert!(app.data.sonarr_data.edit_root_folder.is_none()); + assert!(!app.data.sonarr_data.prompt_confirm); + assert!(!app.should_ignore_quit_key); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + + RootFoldersHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::RootFolders, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert!(app.error.text.is_empty()); + } + } + + mod test_handle_key_char { + use pretty_assertions::{assert_eq, assert_str_eq}; + + use crate::network::sonarr_network::SonarrEvent; + + use super::*; + + #[test] + fn test_root_folder_add() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.add.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::AddRootFolderPrompt.into() + ); + assert!(app.should_ignore_quit_key); + assert!(app.data.sonarr_data.edit_root_folder.is_some()); + } + + #[test] + fn test_root_folder_add_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.add.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert!(!app.should_ignore_quit_key); + assert!(app.data.sonarr_data.edit_root_folder.is_none()); + } + + #[test] + fn test_refresh_root_folders_key() { + let mut app = App::default(); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_root_folders_key_no_op_when_not_ready() { + let mut app = App::default(); + app.is_loading = true; + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + assert!(!app.should_refresh); + } + + #[test] + fn test_add_root_folder_prompt_backspace_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.sonarr_data.edit_root_folder = Some("/nfs/test".into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.edit_root_folder.as_ref().unwrap().text, + "/nfs/tes" + ); + } + + #[test] + fn test_add_root_folder_prompt_char_key() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.sonarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); + + RootFoldersHandler::with( + Key::Char('h'), + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_str_eq!( + app.data.sonarr_data.edit_root_folder.as_ref().unwrap().text, + "h" + ); + } + + #[test] + fn test_delete_root_folder_prompt_confirm() { + let mut app = App::default(); + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveSonarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveSonarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.sonarr_data.prompt_confirm); + assert_eq!( + app.data.sonarr_data.prompt_confirm_action, + Some(SonarrEvent::DeleteRootFolder(None)) + ); + assert_eq!( + app.get_current_route(), + ActiveSonarrBlock::RootFolders.into() + ); + } + } + + #[test] + fn test_root_folders_handler_accepts() { + ActiveSonarrBlock::iter().for_each(|active_sonarr_block| { + if ROOT_FOLDERS_BLOCKS.contains(&active_sonarr_block) { + assert!(RootFoldersHandler::accepts(active_sonarr_block)); + } else { + assert!(!RootFoldersHandler::accepts(active_sonarr_block)); + } + }) + } + + #[test] + fn test_root_folders_handler_not_ready_when_loading() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = true; + + let handler = RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_root_folders_handler_not_ready_when_root_folders_is_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = false; + + let handler = RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_root_folders_handler_ready_when_not_loading_and_root_folders_is_not_empty() { + let mut app = App::default(); + app.push_navigation_stack(ActiveSonarrBlock::RootFolders.into()); + app.is_loading = false; + + app + .data + .sonarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + let handler = RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::RootFolders, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/sonarr_handlers/sonarr_handler_tests.rs b/src/handlers/sonarr_handlers/sonarr_handler_tests.rs index 95de5ce..00fd74d 100644 --- a/src/handlers/sonarr_handlers/sonarr_handler_tests.rs +++ b/src/handlers/sonarr_handlers/sonarr_handler_tests.rs @@ -165,4 +165,20 @@ mod tests { active_sonarr_block ); } + + #[rstest] + fn test_delegates_root_folders_blocks_to_root_folders_handler( + #[values( + ActiveSonarrBlock::RootFolders, + ActiveSonarrBlock::AddRootFolderPrompt, + ActiveSonarrBlock::DeleteRootFolderPrompt + )] + active_sonarr_block: ActiveSonarrBlock, + ) { + test_handler_delegation!( + SonarrHandler, + ActiveSonarrBlock::RootFolders, + active_sonarr_block + ); + } } diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index d7f7880..104e8d8 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -380,6 +380,12 @@ pub static HISTORY_BLOCKS: [ActiveSonarrBlock; 7] = [ ActiveSonarrBlock::SearchHistoryError, ]; +pub static ROOT_FOLDERS_BLOCKS: [ActiveSonarrBlock; 3] = [ + ActiveSonarrBlock::RootFolders, + ActiveSonarrBlock::AddRootFolderPrompt, + ActiveSonarrBlock::DeleteRootFolderPrompt, +]; + impl From for Route { fn from(active_sonarr_block: ActiveSonarrBlock) -> Route { Route::Sonarr(active_sonarr_block, None) diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index 57ec225..a5ed5a9 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -204,7 +204,7 @@ mod tests { use crate::models::servarr_data::sonarr::sonarr_data::{ ActiveSonarrBlock, ADD_SERIES_BLOCKS, ADD_SERIES_SELECTION_BLOCKS, BLOCKLIST_BLOCKS, DELETE_SERIES_BLOCKS, DELETE_SERIES_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_SERIES_BLOCKS, - EDIT_SERIES_SELECTION_BLOCKS, HISTORY_BLOCKS, LIBRARY_BLOCKS, + EDIT_SERIES_SELECTION_BLOCKS, HISTORY_BLOCKS, LIBRARY_BLOCKS, ROOT_FOLDERS_BLOCKS, }; #[test] @@ -386,5 +386,13 @@ mod tests { assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::SearchHistory)); assert!(HISTORY_BLOCKS.contains(&ActiveSonarrBlock::SearchHistoryError)); } + + #[test] + fn test_root_folders_blocks_contents() { + assert_eq!(ROOT_FOLDERS_BLOCKS.len(), 3); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveSonarrBlock::RootFolders)); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveSonarrBlock::AddRootFolderPrompt)); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveSonarrBlock::DeleteRootFolderPrompt)); + } } }