From 5e1f3bb7e38900e2f016eb6beec4d10747b01c3a Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 4 Sep 2026 12:22:35 -0600 Subject: [PATCH] feat(readarr): add the add root folder handler --- .../readarr_handlers/readarr_handler_tests.rs | 1 + .../root_folders/add_root_folder_handler.rs | 534 +++++ .../add_root_folder_handler_tests.rs | 1750 +++++++++++++++++ .../readarr_handlers/root_folders/mod.rs | 20 +- .../root_folders_handler_tests.rs | 30 + 5 files changed, 2332 insertions(+), 3 deletions(-) create mode 100644 src/handlers/readarr_handlers/root_folders/add_root_folder_handler.rs create mode 100644 src/handlers/readarr_handlers/root_folders/add_root_folder_handler_tests.rs diff --git a/src/handlers/readarr_handlers/readarr_handler_tests.rs b/src/handlers/readarr_handlers/readarr_handler_tests.rs index 063581e8..819b66f4 100644 --- a/src/handlers/readarr_handlers/readarr_handler_tests.rs +++ b/src/handlers/readarr_handlers/readarr_handler_tests.rs @@ -235,6 +235,7 @@ mod tests { fn test_delegates_root_folders_blocks_to_root_folders_handler( #[values( ActiveReadarrBlock::RootFolders, + ActiveReadarrBlock::AddRootFolderPrompt, ActiveReadarrBlock::DeleteRootFolderPrompt )] active_readarr_block: ActiveReadarrBlock, diff --git a/src/handlers/readarr_handlers/root_folders/add_root_folder_handler.rs b/src/handlers/readarr_handlers/root_folders/add_root_folder_handler.rs new file mode 100644 index 00000000..745d01aa --- /dev/null +++ b/src/handlers/readarr_handlers/root_folders/add_root_folder_handler.rs @@ -0,0 +1,534 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::{KeyEventHandler, handle_prompt_toggle}; +use crate::models::readarr_models::AddReadarrRootFolderBody; +use crate::models::servarr_data::readarr::modals::AddReadarrRootFolderModal; +use crate::models::servarr_data::readarr::readarr_data::{ + ADD_ROOT_FOLDER_BLOCKS, ActiveReadarrBlock, +}; +use crate::models::{Route, Scrollable}; +use crate::network::readarr_network::ReadarrEvent; +use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; + +#[cfg(test)] +#[path = "add_root_folder_handler_tests.rs"] +mod add_root_folder_handler_tests; + +pub(super) struct AddRootFolderHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_readarr_block: ActiveReadarrBlock, + context: Option, +} + +impl AddRootFolderHandler<'_, '_> { + fn build_add_root_folder_body(&mut self) -> AddReadarrRootFolderBody { + let add_root_folder_modal = self + .app + .data + .readarr_data + .add_root_folder_modal + .take() + .expect("AddReadarrRootFolderModal is None"); + + let tags = add_root_folder_modal.tags.text.clone(); + + let AddReadarrRootFolderModal { + name, + path, + monitor_list, + monitor_new_items_list, + quality_profile_list, + metadata_profile_list, + .. + } = add_root_folder_modal; + + let quality_profile = quality_profile_list.current_selection(); + let quality_profile_id = *self + .app + .data + .readarr_data + .quality_profile_map + .iter() + .filter(|(_, value)| *value == quality_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + let metadata_profile = metadata_profile_list.current_selection(); + let metadata_profile_id = *self + .app + .data + .readarr_data + .metadata_profile_map + .iter() + .filter(|(_, value)| *value == metadata_profile) + .map(|(key, _)| key) + .next() + .unwrap(); + + AddReadarrRootFolderBody { + name: name.text, + path: path.text, + default_quality_profile_id: quality_profile_id, + default_metadata_profile_id: metadata_profile_id, + default_monitor_option: *monitor_list.current_selection(), + default_new_item_monitor_option: *monitor_new_items_list.current_selection(), + default_tags: Vec::new(), + tag_input_string: Some(tags), + } + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for AddRootFolderHandler<'a, 'b> { + fn accepts(active_block: ActiveReadarrBlock) -> bool { + ADD_ROOT_FOLDER_BLOCKS.contains(&active_block) + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveReadarrBlock, + context: Option, + ) -> AddRootFolderHandler<'a, 'b> { + AddRootFolderHandler { + key, + app, + active_readarr_block: active_block, + context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && self.app.data.readarr_data.add_root_folder_modal.is_some() + } + + fn handle_scroll_up(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::AddRootFolderSelectMonitor => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_up(), + ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_up(), + ActiveReadarrBlock::AddRootFolderSelectQualityProfile => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_up(), + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_up(), + ActiveReadarrBlock::AddRootFolderPrompt => self.app.data.readarr_data.selected_block.up(), + _ => (), + } + } + + fn handle_scroll_down(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::AddRootFolderSelectMonitor => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_down(), + ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_down(), + ActiveReadarrBlock::AddRootFolderSelectQualityProfile => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_down(), + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_down(), + ActiveReadarrBlock::AddRootFolderPrompt => self.app.data.readarr_data.selected_block.down(), + _ => (), + } + } + + fn handle_home(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::AddRootFolderSelectMonitor => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_top(), + ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_to_top(), + ActiveReadarrBlock::AddRootFolderSelectQualityProfile => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_top(), + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_top(), + ActiveReadarrBlock::AddRootFolderNameInput => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .name + .scroll_home(), + ActiveReadarrBlock::AddRootFolderPathInput => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .path + .scroll_home(), + ActiveReadarrBlock::AddRootFolderTagsInput => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .tags + .scroll_home(), + _ => (), + } + } + + fn handle_end(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::AddRootFolderSelectMonitor => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .scroll_to_bottom(), + ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .scroll_to_bottom(), + ActiveReadarrBlock::AddRootFolderSelectQualityProfile => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .scroll_to_bottom(), + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .scroll_to_bottom(), + ActiveReadarrBlock::AddRootFolderNameInput => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .name + .reset_offset(), + ActiveReadarrBlock::AddRootFolderPathInput => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .path + .reset_offset(), + ActiveReadarrBlock::AddRootFolderTagsInput => self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .tags + .reset_offset(), + _ => (), + } + } + + fn handle_delete(&mut self) {} + + fn handle_left_right_action(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::AddRootFolderPrompt => handle_prompt_toggle(self.app, self.key), + ActiveReadarrBlock::AddRootFolderNameInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .name + ) + } + ActiveReadarrBlock::AddRootFolderPathInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .path + ) + } + ActiveReadarrBlock::AddRootFolderTagsInput => { + handle_text_box_left_right_keys!( + self, + self.key, + self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .tags + ) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::AddRootFolderPrompt => { + match self.app.data.readarr_data.selected_block.get_active_block() { + ActiveReadarrBlock::AddRootFolderConfirmPrompt => { + if self.app.data.readarr_data.prompt_confirm { + self.app.data.readarr_data.prompt_confirm_action = Some(ReadarrEvent::AddRootFolder( + self.build_add_root_folder_body(), + )); + self.app.should_refresh = true; + } + + self.app.pop_navigation_stack(); + } + ActiveReadarrBlock::AddRootFolderSelectMonitor + | ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems + | ActiveReadarrBlock::AddRootFolderSelectQualityProfile + | ActiveReadarrBlock::AddRootFolderSelectMetadataProfile => { + self.app.push_navigation_stack( + ( + self.app.data.readarr_data.selected_block.get_active_block(), + self.context, + ) + .into(), + ) + } + ActiveReadarrBlock::AddRootFolderNameInput + | ActiveReadarrBlock::AddRootFolderPathInput + | ActiveReadarrBlock::AddRootFolderTagsInput => { + self.app.push_navigation_stack( + ( + self.app.data.readarr_data.selected_block.get_active_block(), + self.context, + ) + .into(), + ); + self.app.ignore_special_keys_for_textbox_input = true; + } + _ => (), + } + } + ActiveReadarrBlock::AddRootFolderSelectMonitor + | ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems + | ActiveReadarrBlock::AddRootFolderSelectQualityProfile + | ActiveReadarrBlock::AddRootFolderSelectMetadataProfile => self.app.pop_navigation_stack(), + ActiveReadarrBlock::AddRootFolderNameInput + | ActiveReadarrBlock::AddRootFolderPathInput + | ActiveReadarrBlock::AddRootFolderTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_readarr_block { + ActiveReadarrBlock::AddRootFolderNameInput + | ActiveReadarrBlock::AddRootFolderPathInput + | ActiveReadarrBlock::AddRootFolderTagsInput => { + self.app.pop_navigation_stack(); + self.app.ignore_special_keys_for_textbox_input = false; + } + ActiveReadarrBlock::AddRootFolderPrompt => { + self.app.pop_navigation_stack(); + self.app.data.readarr_data.add_root_folder_modal = None; + self.app.data.readarr_data.prompt_confirm = false; + } + ActiveReadarrBlock::AddRootFolderSelectMonitor + | ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems + | ActiveReadarrBlock::AddRootFolderSelectQualityProfile + | ActiveReadarrBlock::AddRootFolderSelectMetadataProfile => self.app.pop_navigation_stack(), + _ => (), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_readarr_block { + ActiveReadarrBlock::AddRootFolderNameInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .name + ) + } + ActiveReadarrBlock::AddRootFolderPathInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .path + ) + } + ActiveReadarrBlock::AddRootFolderTagsInput => { + handle_text_box_keys!( + self, + key, + self + .app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .tags + ) + } + ActiveReadarrBlock::AddRootFolderPrompt + if self.app.data.readarr_data.selected_block.get_active_block() + == ActiveReadarrBlock::AddRootFolderConfirmPrompt + && matches_key!(confirm, key) => + { + self.app.data.readarr_data.prompt_confirm = true; + self.app.data.readarr_data.prompt_confirm_action = Some(ReadarrEvent::AddRootFolder( + self.build_add_root_folder_body(), + )); + self.app.should_refresh = true; + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn app_mut(&mut self) -> &mut App<'b> { + self.app + } + + fn current_route(&self) -> Route { + self.app.get_current_route() + } +} diff --git a/src/handlers/readarr_handlers/root_folders/add_root_folder_handler_tests.rs b/src/handlers/readarr_handlers/root_folders/add_root_folder_handler_tests.rs new file mode 100644 index 00000000..be9c5ada --- /dev/null +++ b/src/handlers/readarr_handlers/root_folders/add_root_folder_handler_tests.rs @@ -0,0 +1,1750 @@ +#[cfg(test)] +mod tests { + use bimap::BiMap; + use pretty_assertions::{assert_eq, assert_str_eq}; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::readarr_handlers::root_folders::add_root_folder_handler::AddRootFolderHandler; + use crate::models::readarr_models::{AddReadarrRootFolderBody, MonitorType, NewItemMonitorType}; + use crate::models::servarr_data::readarr::modals::AddReadarrRootFolderModal; + use crate::models::servarr_data::readarr::readarr_data::{ + ADD_ROOT_FOLDER_BLOCKS, ActiveReadarrBlock, + }; + use crate::network::readarr_network::ReadarrEvent; + + mod test_handle_scroll_up_and_down { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::models::BlockSelectionState; + use crate::models::servarr_data::readarr::readarr_data::ADD_ROOT_FOLDER_SELECTION_BLOCKS; + + use super::*; + + #[rstest] + fn test_add_root_folder_select_monitor_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let monitor_type_vec = Vec::from_iter(MonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_type_vec.clone()); + + if key == Key::Up { + for i in (0..monitor_type_vec.len()).rev() { + AddRootFolderHandler::new( + key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[i] + ); + } + } else { + for i in 0..monitor_type_vec.len() { + AddRootFolderHandler::new( + key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[(i + 1) % monitor_type_vec.len()] + ); + } + } + } + + #[rstest] + fn test_add_root_folder_select_monitor_new_items_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let monitor_type_vec = Vec::from_iter(NewItemMonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .set_items(monitor_type_vec.clone()); + + if key == Key::Up { + for i in (0..monitor_type_vec.len()).rev() { + AddRootFolderHandler::new( + key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_type_vec[i] + ); + } + } else { + for i in 0..monitor_type_vec.len() { + AddRootFolderHandler::new( + key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_type_vec[(i + 1) % monitor_type_vec.len()] + ); + } + } + } + + #[rstest] + fn test_add_root_folder_select_quality_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec!["EPUB".to_owned(), "MOBI".to_owned()]); + + AddRootFolderHandler::new( + key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "MOBI" + ); + + AddRootFolderHandler::new( + key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "EPUB" + ); + } + + #[rstest] + fn test_add_root_folder_select_metadata_profile_scroll( + #[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec!["Standard".to_owned(), "Comprehensive".to_owned()]); + + AddRootFolderHandler::new( + key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Comprehensive" + ); + + AddRootFolderHandler::new( + key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Standard" + ); + } + + #[rstest] + fn test_add_root_folder_prompt_scroll(#[values(Key::Up, Key::Down)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.down(); + + AddRootFolderHandler::new(key, &mut app, ActiveReadarrBlock::AddRootFolderPrompt, None) + .handle(); + + if key == Key::Up { + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::AddRootFolderNameInput + ); + } else { + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::AddRootFolderSelectMonitor + ); + } + } + + #[rstest] + fn test_add_root_folder_prompt_scroll_no_op_when_not_ready( + #[values(Key::Up, Key::Down)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.is_loading = true; + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.down(); + + AddRootFolderHandler::new(key, &mut app, ActiveReadarrBlock::AddRootFolderPrompt, None) + .handle(); + + assert_eq!( + app.data.readarr_data.selected_block.get_active_block(), + ActiveReadarrBlock::AddRootFolderPathInput + ); + } + } + + mod test_handle_home_end { + use pretty_assertions::assert_eq; + use std::sync::atomic::Ordering; + use strum::IntoEnumIterator; + + use super::*; + + #[test] + fn test_add_root_folder_select_monitor_home_end() { + let monitor_type_vec = Vec::from_iter(MonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_list + .set_items(monitor_type_vec.clone()); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[monitor_type_vec.len() - 1] + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMonitor, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_list + .current_selection(), + &monitor_type_vec[0] + ); + } + + #[test] + fn test_add_root_folder_select_monitor_new_items_home_end() { + let monitor_type_vec = Vec::from_iter(NewItemMonitorType::iter()); + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .monitor_new_items_list + .set_items(monitor_type_vec.clone()); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_type_vec[monitor_type_vec.len() - 1] + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .monitor_new_items_list + .current_selection(), + &monitor_type_vec[0] + ); + } + + #[test] + fn test_add_root_folder_select_quality_profile_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .quality_profile_list + .set_items(vec![ + "EPUB".to_owned(), + "MOBI".to_owned(), + "AZW3".to_owned(), + ]); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "AZW3" + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectQualityProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .quality_profile_list + .current_selection(), + "EPUB" + ); + } + + #[test] + fn test_add_root_folder_select_metadata_profile_home_end() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app + .data + .readarr_data + .add_root_folder_modal + .as_mut() + .unwrap() + .metadata_profile_list + .set_items(vec![ + "Standard".to_owned(), + "Comprehensive".to_owned(), + "Minimal".to_owned(), + ]); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Minimal" + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .metadata_profile_list + .current_selection(), + "Standard" + ); + } + + #[test] + fn test_add_root_folder_name_input_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + name: "Test".into(), + ..AddReadarrRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 4 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_root_folder_path_input_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + path: "Testing".into(), + ..AddReadarrRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 7 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_root_folder_tags_input_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + tags: "Testing tags".into(), + ..AddReadarrRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveReadarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 12 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveReadarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + use std::sync::atomic::Ordering; + + use super::*; + + #[rstest] + fn test_left_right_prompt_toggle(#[values(Key::Left, Key::Right)] key: Key) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + + AddRootFolderHandler::new(key, &mut app, ActiveReadarrBlock::AddRootFolderPrompt, None) + .handle(); + + assert!(app.data.readarr_data.prompt_confirm); + + AddRootFolderHandler::new(key, &mut app, ActiveReadarrBlock::AddRootFolderPrompt, None) + .handle(); + + assert!(!app.data.readarr_data.prompt_confirm); + } + + #[test] + fn test_add_root_folder_name_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + name: "Test".into(), + ..AddReadarrRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveReadarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 1 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveReadarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_root_folder_path_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + path: "Test".into(), + ..AddReadarrRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveReadarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 1 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveReadarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .offset + .load(Ordering::SeqCst), + 0 + ); + } + + #[test] + fn test_add_root_folder_tags_input_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + tags: "Test".into(), + ..AddReadarrRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveReadarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 1 + ); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveReadarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use crate::assert_navigation_popped; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::readarr::readarr_data::ADD_ROOT_FOLDER_SELECTION_BLOCKS; + + use super::*; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_add_root_folder_name_input_submit() { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = true; + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + name: "Test Name".into(), + ..AddReadarrRootFolderModal::default() + }); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPrompt.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderNameInput.into()); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .text, + "Test Name" + ); + assert_navigation_popped!(app, ActiveReadarrBlock::AddRootFolderPrompt.into()); + } + + #[test] + fn test_add_root_folder_path_input_submit() { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = true; + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + path: "/nfs/Test Path".into(), + ..AddReadarrRootFolderModal::default() + }); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPrompt.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPathInput.into()); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .text, + "/nfs/Test Path" + ); + assert_navigation_popped!(app, ActiveReadarrBlock::AddRootFolderPrompt.into()); + } + + #[test] + fn test_add_root_folder_tags_input_submit() { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = true; + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + tags: "usenet, testing".into(), + ..AddReadarrRootFolderModal::default() + }); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPrompt.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderTagsInput.into()); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .text, + "usenet, testing" + ); + assert_navigation_popped!(app, ActiveReadarrBlock::AddRootFolderPrompt.into()); + } + + #[test] + fn test_add_root_folder_prompt_prompt_decline_submit() { + let mut app = App::test_default(); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app + .data + .readarr_data + .selected_block + .set_index(0, ADD_ROOT_FOLDER_SELECTION_BLOCKS.len() - 1); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::RootFolders.into()); + assert_none!(app.data.readarr_data.prompt_confirm_action); + } + + #[test] + fn test_add_root_folder_confirm_prompt_prompt_confirmation_submit() { + let mut app = App::test_default(); + app.data.readarr_data.add_root_folder_modal = Some(populated_add_root_folder_modal()); + seed_distinct_profile_maps(&mut app); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPrompt.into()); + app.data.readarr_data.prompt_confirm = true; + app.data.readarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app + .data + .readarr_data + .selected_block + .set_index(0, ADD_ROOT_FOLDER_SELECTION_BLOCKS.len() - 1); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::RootFolders.into()); + assert_eq!( + app.data.readarr_data.prompt_confirm_action, + Some(ReadarrEvent::AddRootFolder(expected_add_root_folder_body())) + ); + assert_modal_absent!(app.data.readarr_data.add_root_folder_modal); + assert!(app.should_refresh); + } + + #[test] + fn test_add_root_folder_confirm_prompt_prompt_confirmation_submit_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPrompt.into()); + app.data.readarr_data.prompt_confirm = true; + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveReadarrBlock::AddRootFolderPrompt.into() + ); + assert_none!(app.data.readarr_data.prompt_confirm_action); + assert!(!app.should_refresh); + } + + #[rstest] + #[case(ActiveReadarrBlock::AddRootFolderNameInput, 0)] + #[case(ActiveReadarrBlock::AddRootFolderPathInput, 1)] + #[case(ActiveReadarrBlock::AddRootFolderSelectMonitor, 2)] + #[case(ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems, 3)] + #[case(ActiveReadarrBlock::AddRootFolderSelectQualityProfile, 4)] + #[case(ActiveReadarrBlock::AddRootFolderSelectMetadataProfile, 5)] + #[case(ActiveReadarrBlock::AddRootFolderTagsInput, 6)] + fn test_add_root_folder_prompt_selected_block_submit( + #[case] selected_block: ActiveReadarrBlock, + #[case] y_index: usize, + ) { + let mut app = App::test_default(); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack( + ( + ActiveReadarrBlock::AddRootFolderPrompt, + Some(ActiveReadarrBlock::RootFolders), + ) + .into(), + ); + app.data.readarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.set_index(0, y_index); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + Some(ActiveReadarrBlock::RootFolders), + ) + .handle(); + + assert_navigation_pushed!( + app, + (selected_block, Some(ActiveReadarrBlock::RootFolders)).into() + ); + assert_none!(app.data.readarr_data.prompt_confirm_action); + + if selected_block == ActiveReadarrBlock::AddRootFolderNameInput + || selected_block == ActiveReadarrBlock::AddRootFolderPathInput + || selected_block == ActiveReadarrBlock::AddRootFolderTagsInput + { + assert!(app.ignore_special_keys_for_textbox_input); + } + } + + #[rstest] + fn test_add_root_folder_prompt_selected_block_submit_no_op_when_not_ready( + #[values(0, 1, 2, 3, 4, 5, 6)] y_index: usize, + ) { + let mut app = App::test_default(); + app.is_loading = true; + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack( + ( + ActiveReadarrBlock::AddRootFolderPrompt, + Some(ActiveReadarrBlock::RootFolders), + ) + .into(), + ); + app.data.readarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app.data.readarr_data.selected_block.set_index(0, y_index); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + Some(ActiveReadarrBlock::RootFolders), + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ( + ActiveReadarrBlock::AddRootFolderPrompt, + Some(ActiveReadarrBlock::RootFolders), + ) + .into() + ); + assert_none!(app.data.readarr_data.prompt_confirm_action); + assert!(!app.ignore_special_keys_for_textbox_input); + } + + #[rstest] + fn test_add_root_folder_prompt_selecting_preferences_blocks_submit( + #[values( + ActiveReadarrBlock::AddRootFolderSelectMonitor, + ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems, + ActiveReadarrBlock::AddRootFolderSelectQualityProfile, + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile, + ActiveReadarrBlock::AddRootFolderNameInput, + ActiveReadarrBlock::AddRootFolderPathInput, + ActiveReadarrBlock::AddRootFolderTagsInput + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPrompt.into()); + app.push_navigation_stack(active_readarr_block.into()); + + AddRootFolderHandler::new( + SUBMIT_KEY, + &mut app, + active_readarr_block, + Some(ActiveReadarrBlock::RootFolders), + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::AddRootFolderPrompt.into()); + + if active_readarr_block == ActiveReadarrBlock::AddRootFolderNameInput + || active_readarr_block == ActiveReadarrBlock::AddRootFolderPathInput + || active_readarr_block == ActiveReadarrBlock::AddRootFolderTagsInput + { + assert!(!app.ignore_special_keys_for_textbox_input); + } + } + } + + mod test_handle_esc { + use rstest::rstest; + + use crate::assert_navigation_popped; + + use super::*; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + fn test_add_root_folder_input_esc( + #[values( + ActiveReadarrBlock::AddRootFolderTagsInput, + ActiveReadarrBlock::AddRootFolderPathInput, + ActiveReadarrBlock::AddRootFolderNameInput + )] + active_readarr_block: ActiveReadarrBlock, + ) { + let mut app = App::test_default(); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app.ignore_special_keys_for_textbox_input = true; + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPrompt.into()); + app.push_navigation_stack(active_readarr_block.into()); + + AddRootFolderHandler::new(ESC_KEY, &mut app, active_readarr_block, None).handle(); + + assert!(!app.ignore_special_keys_for_textbox_input); + assert_navigation_popped!(app, ActiveReadarrBlock::AddRootFolderPrompt.into()); + } + + #[test] + fn test_add_root_folder_prompt_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPrompt.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app.data.readarr_data.prompt_confirm = true; + + AddRootFolderHandler::new( + ESC_KEY, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::RootFolders.into()); + assert_modal_absent!(app.data.readarr_data.add_root_folder_modal); + assert!(!app.data.readarr_data.prompt_confirm); + } + + #[rstest] + fn test_add_root_folder_esc( + #[values( + ActiveReadarrBlock::AddRootFolderSelectMonitor, + ActiveReadarrBlock::AddRootFolderSelectMonitorNewItems, + ActiveReadarrBlock::AddRootFolderSelectQualityProfile, + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile + )] + active_readarr_block: ActiveReadarrBlock, + #[values(true, false)] is_ready: bool, + ) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(active_readarr_block.into()); + + AddRootFolderHandler::new(ESC_KEY, &mut app, active_readarr_block, None).handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::RootFolders.into()); + } + } + + mod test_handle_key_char { + use pretty_assertions::assert_eq; + + use crate::assert_navigation_popped; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::readarr::readarr_data::ADD_ROOT_FOLDER_SELECTION_BLOCKS; + + use super::*; + + #[test] + fn test_add_root_folder_name_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + name: "Test".into(), + ..AddReadarrRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveReadarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .text, + "Tes" + ); + } + + #[test] + fn test_add_root_folder_path_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + path: "Path".into(), + ..AddReadarrRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveReadarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .text, + "Pat" + ); + } + + #[test] + fn test_add_root_folder_tags_input_backspace() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal { + tags: "Tags".into(), + ..AddReadarrRootFolderModal::default() + }); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveReadarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .text, + "Tag" + ); + } + + #[test] + fn test_add_root_folder_name_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + + AddRootFolderHandler::new( + Key::Char('n'), + &mut app, + ActiveReadarrBlock::AddRootFolderNameInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .text, + "n" + ); + assert_is_empty!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .text + ); + assert_is_empty!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .text + ); + } + + #[test] + fn test_add_root_folder_path_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + + AddRootFolderHandler::new( + Key::Char('p'), + &mut app, + ActiveReadarrBlock::AddRootFolderPathInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .text, + "p" + ); + assert_is_empty!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .text + ); + assert_is_empty!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .text + ); + } + + #[test] + fn test_add_root_folder_tags_input_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + + AddRootFolderHandler::new( + Key::Char('t'), + &mut app, + ActiveReadarrBlock::AddRootFolderTagsInput, + None, + ) + .handle(); + + assert_str_eq!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .tags + .text, + "t" + ); + assert_is_empty!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .name + .text + ); + assert_is_empty!( + app + .data + .readarr_data + .add_root_folder_modal + .as_ref() + .unwrap() + .path + .text + ); + } + + #[test] + fn test_add_root_folder_confirm_prompt_prompt_confirm() { + let mut app = App::test_default(); + app.data.readarr_data.add_root_folder_modal = Some(populated_add_root_folder_modal()); + seed_distinct_profile_maps(&mut app); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPrompt.into()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app + .data + .readarr_data + .selected_block + .set_index(0, ADD_ROOT_FOLDER_SELECTION_BLOCKS.len() - 1); + + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveReadarrBlock::RootFolders.into()); + assert_eq!( + app.data.readarr_data.prompt_confirm_action, + Some(ReadarrEvent::AddRootFolder(expected_add_root_folder_body())) + ); + assert_modal_absent!(app.data.readarr_data.add_root_folder_modal); + assert!(app.should_refresh); + } + } + + #[test] + fn test_add_root_folder_handler_accepts() { + ActiveReadarrBlock::iter().for_each(|active_readarr_block| { + if ADD_ROOT_FOLDER_BLOCKS.contains(&active_readarr_block) { + assert!( + AddRootFolderHandler::accepts(active_readarr_block), + "{active_readarr_block} is not accepted by the AddRootFolderHandler" + ); + } else { + assert!( + !AddRootFolderHandler::accepts(active_readarr_block), + "{active_readarr_block} is accepted by the AddRootFolderHandler" + ); + } + }); + } + + #[rstest] + fn test_add_root_folder_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_build_add_root_folder_body() { + let mut app = App::test_default(); + app.data.readarr_data.add_root_folder_modal = Some(populated_add_root_folder_modal()); + seed_distinct_profile_maps(&mut app); + + let add_root_folder_body = AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + None, + ) + .build_add_root_folder_body(); + + assert_eq!(add_root_folder_body, expected_add_root_folder_body()); + assert_modal_absent!(app.data.readarr_data.add_root_folder_modal); + } + + #[test] + fn test_build_add_root_folder_body_uses_the_selected_profiles() { + let mut app = App::test_default(); + app.data.readarr_data.add_root_folder_modal = Some(populated_add_root_folder_modal()); + seed_distinct_profile_maps(&mut app); + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectQualityProfile, + None, + ) + .handle(); + AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.down.key, + &mut app, + ActiveReadarrBlock::AddRootFolderSelectMetadataProfile, + None, + ) + .handle(); + + let add_root_folder_body = AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + None, + ) + .build_add_root_folder_body(); + + assert_eq!(add_root_folder_body.default_quality_profile_id, 2222); + assert_eq!(add_root_folder_body.default_metadata_profile_id, 4444); + } + + #[test] + fn test_add_root_folder_handler_is_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.is_loading = true; + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + + let handler = AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_add_root_folder_handler_is_not_ready_when_add_root_folder_modal_is_none() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.is_loading = false; + + let handler = AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_add_root_folder_handler_is_ready_when_add_root_folder_modal_is_some() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.is_loading = false; + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + + let handler = AddRootFolderHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + None, + ); + + assert!(handler.is_ready()); + } + + fn populated_add_root_folder_modal() -> AddReadarrRootFolderModal { + let mut add_root_folder_modal = AddReadarrRootFolderModal { + name: "Test Name".to_owned().into(), + path: "/nfs/Test Path".to_owned().into(), + tags: "usenet, testing".to_owned().into(), + ..AddReadarrRootFolderModal::default() + }; + add_root_folder_modal + .quality_profile_list + .set_items(vec!["EPUB".to_owned(), "MOBI".to_owned()]); + add_root_folder_modal + .metadata_profile_list + .set_items(vec!["Standard".to_owned(), "Comprehensive".to_owned()]); + add_root_folder_modal + .monitor_list + .set_items(Vec::from_iter(MonitorType::iter())); + add_root_folder_modal + .monitor_new_items_list + .set_items(Vec::from_iter(NewItemMonitorType::iter())); + + add_root_folder_modal + } + + fn seed_distinct_profile_maps(app: &mut App<'_>) { + app.data.readarr_data.quality_profile_map = + BiMap::from_iter([(1111, "EPUB".to_owned()), (2222, "MOBI".to_owned())]); + app.data.readarr_data.metadata_profile_map = BiMap::from_iter([ + (3333, "Standard".to_owned()), + (4444, "Comprehensive".to_owned()), + ]); + } + + fn expected_add_root_folder_body() -> AddReadarrRootFolderBody { + AddReadarrRootFolderBody { + name: "Test Name".to_owned(), + path: "/nfs/Test Path".to_owned(), + default_quality_profile_id: 1111, + default_metadata_profile_id: 3333, + default_monitor_option: MonitorType::All, + default_new_item_monitor_option: NewItemMonitorType::All, + default_tags: Vec::new(), + tag_input_string: Some("usenet, testing".to_owned()), + } + } +} diff --git a/src/handlers/readarr_handlers/root_folders/mod.rs b/src/handlers/readarr_handlers/root_folders/mod.rs index 58f98703..fc0cc20c 100644 --- a/src/handlers/readarr_handlers/root_folders/mod.rs +++ b/src/handlers/readarr_handlers/root_folders/mod.rs @@ -1,3 +1,5 @@ +use add_root_folder_handler::AddRootFolderHandler; + use crate::app::App; use crate::event::Key; use crate::handlers::readarr_handlers::handle_change_tab_left_right_keys; @@ -11,6 +13,8 @@ use crate::models::servarr_data::readarr::readarr_data::{ use crate::models::{BlockSelectionState, Route}; use crate::network::readarr_network::ReadarrEvent; +mod add_root_folder_handler; + #[cfg(test)] #[path = "root_folders_handler_tests.rs"] mod root_folders_handler_tests; @@ -19,7 +23,7 @@ pub(super) struct RootFoldersHandler<'a, 'b> { key: Key, app: &'a mut App<'b>, active_readarr_block: ActiveReadarrBlock, - _context: Option, + context: Option, } impl RootFoldersHandler<'_, '_> { @@ -39,6 +43,16 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for RootFoldersHandler< let root_folders_table_handling_config = TableHandlingConfig::new(ActiveReadarrBlock::RootFolders.into()); + if AddRootFolderHandler::accepts(self.active_readarr_block) { + return AddRootFolderHandler::new( + self.key, + self.app, + self.active_readarr_block, + self.context, + ) + .handle(); + } + if !handle_table( self, |app| &mut app.data.readarr_data.root_folders, @@ -60,13 +74,13 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for RootFoldersHandler< key: Key, app: &'a mut App<'b>, active_block: ActiveReadarrBlock, - _context: Option, + context: Option, ) -> RootFoldersHandler<'a, 'b> { RootFoldersHandler { key, app, active_readarr_block: active_block, - _context, + context, } } diff --git a/src/handlers/readarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/readarr_handlers/root_folders/root_folders_handler_tests.rs index efd2f7fd..0d220853 100644 --- a/src/handlers/readarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/readarr_handlers/root_folders/root_folders_handler_tests.rs @@ -143,10 +143,40 @@ mod tests { use super::*; use crate::assert_navigation_popped; + use crate::models::BlockSelectionState; + use crate::models::servarr_data::readarr::modals::AddReadarrRootFolderModal; + use crate::models::servarr_data::readarr::readarr_data::ADD_ROOT_FOLDER_SELECTION_BLOCKS; use crate::network::readarr_network::ReadarrEvent; const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + #[test] + fn test_add_root_folder_prompt_submit_passes_the_context_to_the_add_root_folder_handler() { + let mut app = App::test_default(); + app.data.readarr_data.add_root_folder_modal = Some(AddReadarrRootFolderModal::default()); + app.data.readarr_data.selected_block = + BlockSelectionState::new(ADD_ROOT_FOLDER_SELECTION_BLOCKS); + app.push_navigation_stack(ActiveReadarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveReadarrBlock::AddRootFolderPrompt.into()); + + RootFoldersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveReadarrBlock::AddRootFolderPrompt, + Some(ActiveReadarrBlock::RootFolders), + ) + .handle(); + + assert_navigation_pushed!( + app, + ( + ActiveReadarrBlock::AddRootFolderNameInput, + Some(ActiveReadarrBlock::RootFolders) + ) + .into() + ); + } + #[test] fn test_delete_root_folder_prompt_confirm_submit() { let mut app = App::test_default();