From c68cd75015073faecf8d8f80803222001fdec1b8 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 13 Jan 2026 13:40:18 -0700 Subject: [PATCH] feat: Downloads tab support in Lidarr --- src/app/lidarr/lidarr_tests.rs | 24 + src/app/lidarr/mod.rs | 5 + src/cli/lidarr/delete_command_handler.rs | 12 + .../lidarr/delete_command_handler_tests.rs | 60 +++ src/cli/lidarr/list_command_handler.rs | 12 + src/cli/lidarr/list_command_handler_tests.rs | 49 ++ src/cli/lidarr/refresh_command_handler.rs | 9 + .../lidarr/refresh_command_handler_tests.rs | 21 +- src/handlers/handler_test_utils.rs | 88 +--- .../downloads/downloads_handler_tests.rs | 481 ++++++++++++++++++ src/handlers/lidarr_handlers/downloads/mod.rs | 171 +++++++ .../history/history_handler_tests.rs | 8 +- .../lidarr_handlers/lidarr_handler_tests.rs | 79 ++- src/handlers/lidarr_handlers/mod.rs | 5 + src/models/servarr_data/lidarr/lidarr_data.rs | 17 +- .../servarr_data/lidarr/lidarr_data_tests.rs | 31 +- .../lidarr_downloads_network_tests.rs | 49 ++ src/network/lidarr_network/downloads/mod.rs | 47 +- .../lidarr_network/lidarr_network_tests.rs | 11 +- src/network/lidarr_network/mod.rs | 15 +- .../lidarr_ui/downloads/downloads_ui_tests.rs | 72 +++ src/ui/lidarr_ui/downloads/mod.rs | 146 ++++++ ...ts__downloads_ui_DeleteDownloadPrompt.snap | 38 ++ ...napshot_tests__downloads_ui_Downloads.snap | 7 + ...s__downloads_ui_UpdateDownloadsPrompt.snap | 38 ++ ..._downloads_ui_renders_empty_downloads.snap | 5 + ...t_tests__downloads_ui_renders_loading.snap | 8 + src/ui/lidarr_ui/lidarr_ui_tests.rs | 26 + src/ui/lidarr_ui/mod.rs | 20 +- ...__snapshot_tests__lidarr_tabs_Artists.snap | 54 ++ ...snapshot_tests__lidarr_tabs_Downloads.snap | 54 ++ ...__snapshot_tests__lidarr_tabs_History.snap | 54 ++ 32 files changed, 1551 insertions(+), 165 deletions(-) create mode 100644 src/handlers/lidarr_handlers/downloads/downloads_handler_tests.rs create mode 100644 src/handlers/lidarr_handlers/downloads/mod.rs create mode 100644 src/ui/lidarr_ui/downloads/downloads_ui_tests.rs create mode 100644 src/ui/lidarr_ui/downloads/mod.rs create mode 100644 src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_DeleteDownloadPrompt.snap create mode 100644 src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_Downloads.snap create mode 100644 src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_UpdateDownloadsPrompt.snap create mode 100644 src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_empty_downloads.snap create mode 100644 src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_loading.snap create mode 100644 src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap create mode 100644 src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap create mode 100644 src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap diff --git a/src/app/lidarr/lidarr_tests.rs b/src/app/lidarr/lidarr_tests.rs index d585897..8b994c7 100644 --- a/src/app/lidarr/lidarr_tests.rs +++ b/src/app/lidarr/lidarr_tests.rs @@ -12,6 +12,7 @@ mod tests { async fn test_dispatch_by_lidarr_block_artists() { let (tx, mut rx) = mpsc::channel::(500); let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; app.network_tx = Some(tx); app @@ -37,6 +38,7 @@ mod tests { async fn test_dispatch_by_lidarr_block_artist_details() { let (tx, mut rx) = mpsc::channel::(500); let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; app.data.lidarr_data.artists.set_items(vec![artist()]); app.network_tx = Some(tx); @@ -50,10 +52,31 @@ mod tests { assert_eq!(app.tick_count, 0); } + #[tokio::test] + async fn test_dispatch_by_downloads_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::Downloads) + .await; + + assert!(app.is_loading); + assert_eq!( + rx.recv().await.unwrap(), + LidarrEvent::GetDownloads(500).into() + ); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + #[tokio::test] async fn test_dispatch_by_lidarr_block_add_artist_search_results() { let (tx, mut rx) = mpsc::channel::(500); let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; app.network_tx = Some(tx); app.data.lidarr_data.add_artist_search = Some("test artist".into()); @@ -74,6 +97,7 @@ mod tests { async fn test_dispatch_by_history_block() { let (tx, mut rx) = mpsc::channel::(500); let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; app.network_tx = Some(tx); app diff --git a/src/app/lidarr/mod.rs b/src/app/lidarr/mod.rs index af9cad2..e20830f 100644 --- a/src/app/lidarr/mod.rs +++ b/src/app/lidarr/mod.rs @@ -28,6 +28,11 @@ impl App<'_> { .dispatch_network_event(LidarrEvent::ListArtists.into()) .await; } + ActiveLidarrBlock::Downloads => { + self + .dispatch_network_event(LidarrEvent::GetDownloads(500).into()) + .await; + } ActiveLidarrBlock::ArtistDetails => { self .dispatch_network_event(LidarrEvent::GetAlbums(self.extract_artist_id().await).into()) diff --git a/src/cli/lidarr/delete_command_handler.rs b/src/cli/lidarr/delete_command_handler.rs index 006d87c..e69d185 100644 --- a/src/cli/lidarr/delete_command_handler.rs +++ b/src/cli/lidarr/delete_command_handler.rs @@ -37,6 +37,11 @@ pub enum LidarrDeleteCommand { #[arg(long, help = "Add a list exclusion for this artist")] add_list_exclusion: bool, }, + #[command(about = "Delete the specified download")] + Download { + #[arg(long, help = "The ID of the download to delete", required = true)] + download_id: i64, + }, #[command(about = "Delete the tag with the specified ID")] Tag { #[arg(long, help = "The ID of the tag to delete", required = true)] @@ -103,6 +108,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteComm .await?; serde_json::to_string_pretty(&resp)? } + LidarrDeleteCommand::Download { download_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteDownload(download_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrDeleteCommand::Tag { tag_id } => { let resp = self .network diff --git a/src/cli/lidarr/delete_command_handler_tests.rs b/src/cli/lidarr/delete_command_handler_tests.rs index 1cc64f4..ac42ab4 100644 --- a/src/cli/lidarr/delete_command_handler_tests.rs +++ b/src/cli/lidarr/delete_command_handler_tests.rs @@ -145,6 +145,40 @@ mod tests { assert_eq!(delete_command, expected_args); } + #[test] + fn test_delete_download_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "download"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_download_success() { + let expected_args = LidarrDeleteCommand::Download { download_id: 1 }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "download", + "--download-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + #[test] fn test_delete_tag_requires_arguments() { let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "tag"]); @@ -260,6 +294,32 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_handle_delete_download_command() { + let expected_download_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteDownload(expected_download_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_download_command = LidarrDeleteCommand::Download { download_id: 1 }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_download_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_delete_tag_command() { let expected_tag_id = 1; diff --git a/src/cli/lidarr/list_command_handler.rs b/src/cli/lidarr/list_command_handler.rs index 4aeb5f0..e0f457f 100644 --- a/src/cli/lidarr/list_command_handler.rs +++ b/src/cli/lidarr/list_command_handler.rs @@ -29,6 +29,11 @@ pub enum LidarrListCommand { }, #[command(about = "List all artists in your Lidarr library")] Artists, + #[command(about = "List all active downloads in Lidarr")] + Downloads { + #[arg(long, help = "How many downloads to fetch", default_value_t = 500)] + count: u64, + }, #[command(about = "Fetch all Lidarr history events")] History { #[arg(long, help = "How many history events to fetch", default_value_t = 500)] @@ -83,6 +88,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH .await?; serde_json::to_string_pretty(&resp)? } + LidarrListCommand::Downloads { count } => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetDownloads(count).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrListCommand::History { events: items } => { let resp = self .network diff --git a/src/cli/lidarr/list_command_handler_tests.rs b/src/cli/lidarr/list_command_handler_tests.rs index 7675499..cc61ac6 100644 --- a/src/cli/lidarr/list_command_handler_tests.rs +++ b/src/cli/lidarr/list_command_handler_tests.rs @@ -58,6 +58,29 @@ mod tests { assert_eq!(album_command, expected_args); } + #[test] + fn test_list_downloads_count_flag_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "list", "downloads", "--count"]); + + assert_err!(&result); + assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidValue); + } + + #[test] + fn test_list_downloads_default_values() { + let expected_args = LidarrListCommand::Downloads { count: 500 }; + let result = Cli::try_parse_from(["managarr", "lidarr", "list", "downloads"]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::List(downloads_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(downloads_command, expected_args); + } + #[test] fn test_list_history_events_flag_requires_arguments() { let result = @@ -151,6 +174,32 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_handle_list_downloads_command() { + let expected_count = 1000; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::GetDownloads(expected_count).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let list_downloads_command = LidarrListCommand::Downloads { count: 1000 }; + + let result = + LidarrListCommandHandler::with(&app_arc, list_downloads_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_list_history_command() { let expected_events = 1000; diff --git a/src/cli/lidarr/refresh_command_handler.rs b/src/cli/lidarr/refresh_command_handler.rs index ee89aee..71901ca 100644 --- a/src/cli/lidarr/refresh_command_handler.rs +++ b/src/cli/lidarr/refresh_command_handler.rs @@ -28,6 +28,8 @@ pub enum LidarrRefreshCommand { )] artist_id: i64, }, + #[command(about = "Refresh all downloads in Lidarr")] + Downloads, } impl From for Command { @@ -73,6 +75,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrRefreshCommand> .await?; serde_json::to_string_pretty(&resp)? } + LidarrRefreshCommand::Downloads => { + let resp = self + .network + .handle_network_event(LidarrEvent::UpdateDownloads.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } }; Ok(result) diff --git a/src/cli/lidarr/refresh_command_handler_tests.rs b/src/cli/lidarr/refresh_command_handler_tests.rs index dc9e63d..686c2f2 100644 --- a/src/cli/lidarr/refresh_command_handler_tests.rs +++ b/src/cli/lidarr/refresh_command_handler_tests.rs @@ -22,11 +22,14 @@ mod tests { use super::*; use clap::{Parser, error::ErrorKind}; use pretty_assertions::assert_eq; + use rstest::rstest; - #[test] - fn test_refresh_all_artists_has_no_arg_requirements() { + #[rstest] + fn test_refresh_commands_have_no_arg_requirements( + #[values("all-artists", "downloads")] subcommand: &str, + ) { let result = - Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", "all-artists"]); + Cli::command().try_get_matches_from(["managarr", "lidarr", "refresh", subcommand]); assert_ok!(&result); } @@ -67,6 +70,7 @@ mod tests { use std::sync::Arc; use mockall::predicate::eq; + use rstest::rstest; use serde_json::json; use tokio::sync::Mutex; @@ -80,12 +84,18 @@ mod tests { network::{MockNetworkTrait, NetworkEvent}, }; + #[rstest] + #[case(LidarrRefreshCommand::AllArtists, LidarrEvent::UpdateAllArtists)] + #[case(LidarrRefreshCommand::Downloads, LidarrEvent::UpdateDownloads)] #[tokio::test] - async fn test_handle_refresh_all_artists_command() { + async fn test_handle_refresh_command( + #[case] refresh_command: LidarrRefreshCommand, + #[case] expected_sonarr_event: LidarrEvent, + ) { let mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() - .with(eq::(LidarrEvent::UpdateAllArtists.into())) + .with(eq::(expected_sonarr_event.into())) .times(1) .returning(|_| { Ok(Serdeable::Lidarr(LidarrSerdeable::Value( @@ -93,7 +103,6 @@ mod tests { ))) }); let app_arc = Arc::new(Mutex::new(App::test_default())); - let refresh_command = LidarrRefreshCommand::AllArtists; let result = LidarrRefreshCommandHandler::with(&app_arc, refresh_command, &mut mock_network) .handle() diff --git a/src/handlers/handler_test_utils.rs b/src/handlers/handler_test_utils.rs index 268e5ce..592f7c6 100644 --- a/src/handlers/handler_test_utils.rs +++ b/src/handlers/handler_test_utils.rs @@ -330,93 +330,7 @@ mod test_utils { #[macro_export] macro_rules! test_handler_delegation { ($handler:ident, $base:expr, $active_block:expr) => { - let mut app = App::test_default(); - app.data.sonarr_data.history.set_items(vec![ - $crate::models::sonarr_models::SonarrHistoryItem::default(), - ]); - app - .data - .sonarr_data - .root_folders - .set_items(vec![$crate::models::servarr_models::RootFolder::default()]); - app - .data - .sonarr_data - .indexers - .set_items(vec![$crate::models::servarr_models::Indexer::default()]); - app - .data - .sonarr_data - .blocklist - .set_items(vec![$crate::models::sonarr_models::BlocklistItem::default()]); - app.data.sonarr_data.add_searched_series = - Some($crate::models::stateful_table::StatefulTable::default()); - app - .data - .radarr_data - .movies - .set_items(vec![$crate::models::radarr_models::Movie::default()]); - app.data.radarr_data.history.set_items(vec![ - $crate::models::radarr_models::RadarrHistoryItem::default(), - ]); - app - .data - .radarr_data - .collections - .set_items(vec![$crate::models::radarr_models::Collection::default()]); - app.data.radarr_data.collection_movies.set_items(vec![ - $crate::models::radarr_models::CollectionMovie::default(), - ]); - app - .data - .radarr_data - .indexers - .set_items(vec![$crate::models::servarr_models::Indexer::default()]); - app - .data - .radarr_data - .root_folders - .set_items(vec![$crate::models::servarr_models::RootFolder::default()]); - app - .data - .radarr_data - .blocklist - .set_items(vec![$crate::models::radarr_models::BlocklistItem::default()]); - app.data.radarr_data.add_searched_movies = - Some($crate::models::stateful_table::StatefulTable::default()); - let mut movie_details_modal = - $crate::models::servarr_data::radarr::modals::MovieDetailsModal::default(); - movie_details_modal.movie_history.set_items(vec![ - $crate::models::radarr_models::MovieHistoryItem::default(), - ]); - movie_details_modal - .movie_cast - .set_items(vec![$crate::models::radarr_models::Credit::default()]); - movie_details_modal - .movie_crew - .set_items(vec![$crate::models::radarr_models::Credit::default()]); - movie_details_modal - .movie_releases - .set_items(vec![$crate::models::radarr_models::RadarrRelease::default()]); - app.data.radarr_data.movie_details_modal = Some(movie_details_modal); - let mut season_details_modal = - $crate::models::servarr_data::sonarr::modals::SeasonDetailsModal::default(); - season_details_modal.season_history.set_items(vec![ - $crate::models::sonarr_models::SonarrHistoryItem::default(), - ]); - season_details_modal.episode_details_modal = - Some($crate::models::servarr_data::sonarr::modals::EpisodeDetailsModal::default()); - app.data.sonarr_data.season_details_modal = Some(season_details_modal); - let mut series_history = $crate::models::stateful_table::StatefulTable::default(); - series_history.set_items(vec![ - $crate::models::sonarr_models::SonarrHistoryItem::default(), - ]); - app.data.sonarr_data.series_history = Some(series_history); - app - .data - .sonarr_data - .series - .set_items(vec![$crate::models::sonarr_models::Series::default()]); + let mut app = App::test_default_fully_populated(); app.push_navigation_stack($base.into()); app.push_navigation_stack($active_block.into()); diff --git a/src/handlers/lidarr_handlers/downloads/downloads_handler_tests.rs b/src/handlers/lidarr_handlers/downloads/downloads_handler_tests.rs new file mode 100644 index 0000000..b349e0d --- /dev/null +++ b/src/handlers/lidarr_handlers/downloads/downloads_handler_tests.rs @@ -0,0 +1,481 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::downloads::DownloadsHandler; + use crate::models::lidarr_models::DownloadRecord; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS}; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record; + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_download_prompt() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteDownloadPrompt.into()); + } + + #[test] + fn test_delete_download_prompt_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into()); + } + } + + mod test_handle_left_right_action { + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_pushed; + + #[rstest] + fn test_downloads_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(1); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Artists.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into()); + } + + #[rstest] + fn test_downloads_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(1); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::History.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::History.into()); + } + + #[rstest] + fn test_downloads_left_right_prompt_toggle( + #[values( + ActiveLidarrBlock::DeleteDownloadPrompt, + ActiveLidarrBlock::UpdateDownloadsPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + + DownloadsHandler::new(key, &mut app, active_lidarr_block, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + DownloadsHandler::new(key, &mut app, active_lidarr_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + } + + mod test_handle_submit { + use rstest::rstest; + + use crate::network::lidarr_network::LidarrEvent; + + use super::*; + use crate::assert_navigation_popped; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[rstest] + #[case( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::DeleteDownloadPrompt, + LidarrEvent::DeleteDownload(1) + )] + #[case( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::UpdateDownloadsPrompt, + LidarrEvent::UpdateDownloads + )] + fn test_downloads_prompt_confirm_submit( + #[case] base_route: ActiveLidarrBlock, + #[case] prompt_block: ActiveLidarrBlock, + #[case] expected_action: LidarrEvent, + ) { + let mut app = App::test_default(); + app + .data + .lidarr_data + .downloads + .set_items(vec![download_record()]); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_action + ); + assert_navigation_popped!(app, base_route.into()); + } + + #[rstest] + #[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::DeleteDownloadPrompt)] + #[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::UpdateDownloadsPrompt)] + fn test_downloads_prompt_decline_submit( + #[case] base_route: ActiveLidarrBlock, + #[case] prompt_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + DownloadsHandler::new(SUBMIT_KEY, &mut app, prompt_block, None).handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert_navigation_popped!(app, base_route.into()); + } + } + + mod test_handle_esc { + use rstest::rstest; + + use super::*; + use crate::assert_navigation_popped; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[rstest] + #[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::DeleteDownloadPrompt)] + #[case(ActiveLidarrBlock::Downloads, ActiveLidarrBlock::UpdateDownloadsPrompt)] + fn test_downloads_prompt_blocks_esc( + #[case] base_block: ActiveLidarrBlock, + #[case] prompt_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(base_block.into()); + app.push_navigation_stack(prompt_block.into()); + app.data.lidarr_data.prompt_confirm = true; + + DownloadsHandler::new(ESC_KEY, &mut app, prompt_block, None).handle(); + + assert_navigation_popped!(app, base_block.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + + DownloadsHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::Downloads, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::Downloads.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use super::*; + use crate::assert_navigation_popped; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[test] + fn test_update_downloads_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::UpdateDownloadsPrompt.into()); + } + + #[test] + fn test_update_downloads_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.update.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into()); + } + + #[test] + fn test_refresh_downloads_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_downloads_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .handle(); + + assert_eq!(app.get_current_route(), ActiveLidarrBlock::Downloads.into()); + assert!(!app.should_refresh); + } + + #[rstest] + #[case( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::DeleteDownloadPrompt, + LidarrEvent::DeleteDownload(1) + )] + #[case( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::UpdateDownloadsPrompt, + LidarrEvent::UpdateDownloads + )] + fn test_downloads_prompt_confirm_submit( + #[case] base_route: ActiveLidarrBlock, + #[case] prompt_block: ActiveLidarrBlock, + #[case] expected_action: LidarrEvent, + ) { + let mut app = App::test_default(); + app + .data + .lidarr_data + .downloads + .set_items(vec![download_record()]); + app.push_navigation_stack(base_route.into()); + app.push_navigation_stack(prompt_block.into()); + + DownloadsHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + prompt_block, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &expected_action + ); + assert_navigation_popped!(app, base_route.into()); + } + } + + #[test] + fn test_downloads_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if DOWNLOADS_BLOCKS.contains(&active_lidarr_block) { + assert!(DownloadsHandler::accepts(active_lidarr_block)); + } else { + assert!(!DownloadsHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_downloads_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 = DownloadsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_extract_download_id() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .downloads + .set_items(vec![download_record()]); + + let download_id = DownloadsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ) + .extract_download_id(); + + assert_eq!(download_id, 1); + } + + #[test] + fn test_downloads_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.is_loading = true; + + let handler = DownloadsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_downloads_handler_not_ready_when_downloads_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.is_loading = false; + + let handler = DownloadsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_downloads_handler_ready_when_not_loading_and_downloads_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + app.is_loading = false; + + app + .data + .lidarr_data + .downloads + .set_items(vec![DownloadRecord::default()]); + let handler = DownloadsHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::Downloads, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/handlers/lidarr_handlers/downloads/mod.rs b/src/handlers/lidarr_handlers/downloads/mod.rs new file mode 100644 index 0000000..eaf8f95 --- /dev/null +++ b/src/handlers/lidarr_handlers/downloads/mod.rs @@ -0,0 +1,171 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; +use crate::matches_key; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS}; +use crate::network::lidarr_network::LidarrEvent; + +#[cfg(test)] +#[path = "downloads_handler_tests.rs"] +mod downloads_handler_tests; + +pub(super) struct DownloadsHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl DownloadsHandler<'_, '_> { + fn extract_download_id(&self) -> i64 { + self.app.data.lidarr_data.downloads.current_selection().id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for DownloadsHandler<'a, 'b> { + fn handle(&mut self) { + let download_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::Downloads.into()); + + if !handle_table( + self, + |app| &mut app.data.lidarr_data.downloads, + download_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + DOWNLOADS_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: ActiveLidarrBlock, + _context: Option, + ) -> DownloadsHandler<'a, 'b> { + DownloadsHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.lidarr_data.downloads.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) {} + + fn handle_end(&mut self) {} + + fn handle_delete(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::Downloads { + self + .app + .push_navigation_stack(ActiveLidarrBlock::DeleteDownloadPrompt.into()) + } + } + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::Downloads => handle_change_tab_left_right_keys(self.app, self.key), + ActiveLidarrBlock::DeleteDownloadPrompt | ActiveLidarrBlock::UpdateDownloadsPrompt => { + handle_prompt_toggle(self.app, self.key) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteDownloadPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteDownload(self.extract_download_id())); + } + + self.app.pop_navigation_stack(); + } + ActiveLidarrBlock::UpdateDownloadsPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateDownloads); + } + + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteDownloadPrompt | ActiveLidarrBlock::UpdateDownloadsPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::Downloads => match self.key { + _ if matches_key!(update, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::UpdateDownloadsPrompt.into()); + } + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ => (), + }, + ActiveLidarrBlock::DeleteDownloadPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteDownload(self.extract_download_id())); + + self.app.pop_navigation_stack(); + } + } + ActiveLidarrBlock::UpdateDownloadsPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::UpdateDownloads); + + 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/lidarr_handlers/history/history_handler_tests.rs b/src/handlers/lidarr_handlers/history/history_handler_tests.rs index 8dc7c31..814203b 100644 --- a/src/handlers/lidarr_handlers/history/history_handler_tests.rs +++ b/src/handlers/lidarr_handlers/history/history_handler_tests.rs @@ -29,7 +29,7 @@ mod tests { let mut app = App::test_default(); app.push_navigation_stack(ActiveLidarrBlock::History.into()); app.is_loading = is_ready; - app.data.lidarr_data.main_tabs.set_index(1); + app.data.lidarr_data.main_tabs.set_index(2); HistoryHandler::new( DEFAULT_KEYBINDINGS.left.key, @@ -41,9 +41,9 @@ mod tests { assert_eq!( app.data.lidarr_data.main_tabs.get_active_route(), - ActiveLidarrBlock::Artists.into() + ActiveLidarrBlock::Downloads.into() ); - assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into()); + assert_navigation_pushed!(app, ActiveLidarrBlock::Downloads.into()); } #[rstest] @@ -51,7 +51,7 @@ mod tests { let mut app = App::test_default(); app.push_navigation_stack(ActiveLidarrBlock::History.into()); app.is_loading = is_ready; - app.data.lidarr_data.main_tabs.set_index(1); + app.data.lidarr_data.main_tabs.set_index(2); HistoryHandler::new( DEFAULT_KEYBINDINGS.right.key, diff --git a/src/handlers/lidarr_handlers/lidarr_handler_tests.rs b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs index 598c1de..1bbf8f5 100644 --- a/src/handlers/lidarr_handlers/lidarr_handler_tests.rs +++ b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs @@ -2,13 +2,10 @@ mod tests { use crate::app::App; use crate::app::key_binding::DEFAULT_KEYBINDINGS; - use crate::assert_navigation_pushed; use crate::handlers::KeyEventHandler; use crate::handlers::lidarr_handlers::{LidarrHandler, handle_change_tab_left_right_keys}; - use crate::models::lidarr_models::Artist; - use crate::models::lidarr_models::LidarrHistoryItem; use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock; - use crate::models::servarr_data::lidarr::modals::EditArtistModal; + use crate::{assert_navigation_pushed, test_handler_delegation}; use pretty_assertions::assert_eq; use rstest::rstest; use strum::IntoEnumIterator; @@ -55,8 +52,9 @@ mod tests { } #[rstest] - #[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::History)] - #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::Artists)] + #[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::Downloads)] + #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)] + #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::Artists)] fn test_lidarr_handler_change_tab_left_right_keys( #[case] index: usize, #[case] left_block: ActiveLidarrBlock, @@ -85,8 +83,9 @@ mod tests { } #[rstest] - #[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::History)] - #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::Artists)] + #[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::Downloads)] + #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)] + #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::Artists)] fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation( #[case] index: usize, #[case] left_block: ActiveLidarrBlock, @@ -116,7 +115,8 @@ mod tests { #[rstest] #[case(0, ActiveLidarrBlock::Artists)] - #[case(1, ActiveLidarrBlock::History)] + #[case(1, ActiveLidarrBlock::Downloads)] + #[case(2, ActiveLidarrBlock::History)] fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key( #[case] index: usize, #[case] block: ActiveLidarrBlock, @@ -165,25 +165,27 @@ mod tests { )] active_lidarr_block: ActiveLidarrBlock, ) { - let mut app = App::test_default(); - app - .data - .lidarr_data - .artists - .set_items(vec![Artist::default()]); - app.data.lidarr_data.edit_artist_modal = Some(EditArtistModal::default()); - app.push_navigation_stack(ActiveLidarrBlock::Artists.into()); - app.push_navigation_stack(active_lidarr_block.into()); + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::Artists, + active_lidarr_block + ); + } - LidarrHandler::new( - DEFAULT_KEYBINDINGS.esc.key, - &mut app, - active_lidarr_block, - None, - ) - .handle(); - - assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); + #[rstest] + fn test_delegates_downloads_blocks_to_downloads_handler( + #[values( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::DeleteDownloadPrompt, + ActiveLidarrBlock::UpdateDownloadsPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::Downloads, + active_lidarr_block + ); } #[rstest] @@ -199,23 +201,10 @@ mod tests { )] active_lidarr_block: ActiveLidarrBlock, ) { - let mut app = App::test_default(); - app - .data - .lidarr_data - .history - .set_items(vec![LidarrHistoryItem::default()]); - app.push_navigation_stack(ActiveLidarrBlock::History.into()); - app.push_navigation_stack(active_lidarr_block.into()); - - LidarrHandler::new( - DEFAULT_KEYBINDINGS.esc.key, - &mut app, - active_lidarr_block, - None, - ) - .handle(); - - assert_eq!(app.get_current_route(), ActiveLidarrBlock::History.into()); + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::History, + active_lidarr_block + ); } } diff --git a/src/handlers/lidarr_handlers/mod.rs b/src/handlers/lidarr_handlers/mod.rs index 3e23c8c..9d38a39 100644 --- a/src/handlers/lidarr_handlers/mod.rs +++ b/src/handlers/lidarr_handlers/mod.rs @@ -2,6 +2,7 @@ use history::HistoryHandler; use library::LibraryHandler; use super::KeyEventHandler; +use crate::handlers::lidarr_handlers::downloads::DownloadsHandler; use crate::models::Route; use crate::{ app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock, @@ -10,6 +11,7 @@ use crate::{ mod history; mod library; +mod downloads; #[cfg(test)] #[path = "lidarr_handler_tests.rs"] mod lidarr_handler_tests; @@ -27,6 +29,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b _ if LibraryHandler::accepts(self.active_lidarr_block) => { LibraryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); } + _ if DownloadsHandler::accepts(self.active_lidarr_block) => { + DownloadsHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); + } _ if HistoryHandler::accepts(self.active_lidarr_block) => { HistoryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); } diff --git a/src/models/servarr_data/lidarr/lidarr_data.rs b/src/models/servarr_data/lidarr/lidarr_data.rs index fca4e77..ef8edea 100644 --- a/src/models/servarr_data/lidarr/lidarr_data.rs +++ b/src/models/servarr_data/lidarr/lidarr_data.rs @@ -1,7 +1,7 @@ use serde_json::Number; use super::modals::{AddArtistModal, EditArtistModal}; -use crate::app::context_clues::HISTORY_CONTEXT_CLUES; +use crate::app::context_clues::{DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES}; use crate::app::lidarr::lidarr_context_clues::{ ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, }; @@ -131,6 +131,12 @@ impl<'a> Default for LidarrData<'a> { contextual_help: Some(&ARTISTS_CONTEXT_CLUES), config: None, }, + TabRoute { + title: "Downloads".to_string(), + route: ActiveLidarrBlock::Downloads.into(), + contextual_help: Some(&DOWNLOADS_CONTEXT_CLUES), + config: None, + }, TabRoute { title: "History".to_string(), route: ActiveLidarrBlock::History.into(), @@ -252,6 +258,8 @@ pub enum ActiveLidarrBlock { DeleteArtistConfirmPrompt, DeleteArtistToggleDeleteFile, DeleteArtistToggleAddListExclusion, + DeleteDownloadPrompt, + Downloads, EditArtistPrompt, EditArtistConfirmPrompt, EditArtistPathInput, @@ -275,6 +283,7 @@ pub enum ActiveLidarrBlock { SearchHistoryError, UpdateAllArtistsPrompt, UpdateAndScanArtistPrompt, + UpdateDownloadsPrompt, } pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 7] = [ @@ -295,6 +304,12 @@ pub static ARTIST_DETAILS_BLOCKS: [ActiveLidarrBlock; 5] = [ ActiveLidarrBlock::UpdateAndScanArtistPrompt, ]; +pub static DOWNLOADS_BLOCKS: [ActiveLidarrBlock; 3] = [ + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::DeleteDownloadPrompt, + ActiveLidarrBlock::UpdateDownloadsPrompt, +]; + pub static HISTORY_BLOCKS: [ActiveLidarrBlock; 7] = [ ActiveLidarrBlock::History, ActiveLidarrBlock::HistoryItemDetails, diff --git a/src/models/servarr_data/lidarr/lidarr_data_tests.rs b/src/models/servarr_data/lidarr/lidarr_data_tests.rs index 7d7e9e7..03611ad 100644 --- a/src/models/servarr_data/lidarr/lidarr_data_tests.rs +++ b/src/models/servarr_data/lidarr/lidarr_data_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use crate::app::context_clues::HISTORY_CONTEXT_CLUES; + use crate::app::context_clues::{DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES}; use crate::app::lidarr::lidarr_context_clues::{ ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, }; @@ -8,7 +8,7 @@ mod tests { use crate::models::servarr_data::lidarr::lidarr_data::{ ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ARTIST_DETAILS_BLOCKS, DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, - EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, HISTORY_BLOCKS, + DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, HISTORY_BLOCKS, }; use crate::models::{ BlockSelectionState, Route, @@ -146,7 +146,7 @@ mod tests { assert_is_empty!(lidarr_data.tags_map); assert_is_empty!(lidarr_data.version); - assert_eq!(lidarr_data.main_tabs.tabs.len(), 2); + assert_eq!(lidarr_data.main_tabs.tabs.len(), 3); assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library"); assert_eq!( @@ -159,17 +159,28 @@ mod tests { ); assert_none!(lidarr_data.main_tabs.tabs[0].config); - assert_str_eq!(lidarr_data.main_tabs.tabs[1].title, "History"); + assert_str_eq!(lidarr_data.main_tabs.tabs[1].title, "Downloads"); assert_eq!( lidarr_data.main_tabs.tabs[1].route, - ActiveLidarrBlock::History.into() + ActiveLidarrBlock::Downloads.into() ); assert_some_eq_x!( &lidarr_data.main_tabs.tabs[1].contextual_help, - &HISTORY_CONTEXT_CLUES + &DOWNLOADS_CONTEXT_CLUES ); assert_none!(lidarr_data.main_tabs.tabs[1].config); + assert_str_eq!(lidarr_data.main_tabs.tabs[2].title, "History"); + assert_eq!( + lidarr_data.main_tabs.tabs[2].route, + ActiveLidarrBlock::History.into() + ); + assert_some_eq_x!( + &lidarr_data.main_tabs.tabs[2].contextual_help, + &HISTORY_CONTEXT_CLUES + ); + assert_none!(lidarr_data.main_tabs.tabs[2].config); + assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 1); assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums"); assert_eq!( @@ -205,6 +216,14 @@ mod tests { assert!(ARTIST_DETAILS_BLOCKS.contains(&ActiveLidarrBlock::UpdateAndScanArtistPrompt)); } + #[test] + fn test_downloads_blocks_contains_expected_blocks() { + assert_eq!(DOWNLOADS_BLOCKS.len(), 3); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::Downloads)); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::DeleteDownloadPrompt)); + assert!(DOWNLOADS_BLOCKS.contains(&ActiveLidarrBlock::UpdateDownloadsPrompt)); + } + #[test] fn test_history_blocks_contains_expected_blocks() { assert_eq!(HISTORY_BLOCKS.len(), 7); diff --git a/src/network/lidarr_network/downloads/lidarr_downloads_network_tests.rs b/src/network/lidarr_network/downloads/lidarr_downloads_network_tests.rs index fe31d05..32aa0ec 100644 --- a/src/network/lidarr_network/downloads/lidarr_downloads_network_tests.rs +++ b/src/network/lidarr_network/downloads/lidarr_downloads_network_tests.rs @@ -2,10 +2,37 @@ mod tests { use crate::models::lidarr_models::{DownloadsResponse, LidarrSerdeable}; use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::download_record; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use pretty_assertions::assert_eq; use serde_json::json; + #[tokio::test] + async fn test_handle_delete_lidarr_download_event() { + let (mock, app, _server) = MockServarrApi::delete() + .path("/1") + .build_for(LidarrEvent::DeleteDownload(1)) + .await; + app + .lock() + .await + .data + .lidarr_data + .downloads + .set_items(vec![download_record()]); + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteDownload(1)) + .await + .is_ok() + ); + + mock.assert_async().await; + } + #[tokio::test] async fn test_handle_get_downloads_event() { let downloads_json = json!({ @@ -40,4 +67,26 @@ mod tests { assert_eq!(downloads_response, response); assert!(!app.lock().await.data.lidarr_data.downloads.is_empty()); } + + #[tokio::test] + async fn test_handle_update_lidarr_downloads_event() { + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "name": "RefreshMonitoredDownloads" + })) + .returns(json!({})) + .build_for(LidarrEvent::UpdateDownloads) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::UpdateDownloads) + .await + .is_ok() + ); + + mock.assert_async().await; + } } diff --git a/src/network/lidarr_network/downloads/mod.rs b/src/network/lidarr_network/downloads/mod.rs index 3a0e918..d3d6d1f 100644 --- a/src/network/lidarr_network/downloads/mod.rs +++ b/src/network/lidarr_network/downloads/mod.rs @@ -1,15 +1,38 @@ -use anyhow::Result; -use log::info; - use crate::models::lidarr_models::DownloadsResponse; +use crate::models::servarr_models::CommandBody; use crate::network::lidarr_network::LidarrEvent; use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::info; +use serde_json::Value; #[cfg(test)] #[path = "lidarr_downloads_network_tests.rs"] mod lidarr_downloads_network_tests; impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn delete_lidarr_download( + &mut self, + download_id: i64, + ) -> Result<()> { + let event = LidarrEvent::DeleteDownload(download_id); + info!("Deleting Lidarr download for download with id: {download_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{download_id}")), + None, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + pub(in crate::network::lidarr_network) async fn get_lidarr_downloads( &mut self, count: u64, @@ -37,4 +60,22 @@ impl Network<'_, '_> { }) .await } + + pub(in crate::network::lidarr_network) async fn update_lidarr_downloads( + &mut self, + ) -> Result { + info!("Updating Lidarr downloads"); + let event = LidarrEvent::UpdateDownloads; + let body = CommandBody { + name: "RefreshMonitoredDownloads".to_owned(), + }; + + let request_props = self + .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } } diff --git a/src/network/lidarr_network/lidarr_network_tests.rs b/src/network/lidarr_network/lidarr_network_tests.rs index 682bca7..2ca52e9 100644 --- a/src/network/lidarr_network/lidarr_network_tests.rs +++ b/src/network/lidarr_network/lidarr_network_tests.rs @@ -30,6 +30,13 @@ mod tests { assert_str_eq!(event.resource(), "/artist"); } + #[rstest] + fn test_resource_downloads( + #[values(LidarrEvent::GetDownloads(0), LidarrEvent::DeleteDownload(0))] event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/queue"); + } + #[rstest] fn test_resource_history(#[values(LidarrEvent::GetHistory(0))] event: LidarrEvent) { assert_str_eq!(event.resource(), "/history"); @@ -59,7 +66,8 @@ mod tests { #[values( LidarrEvent::UpdateAllArtists, LidarrEvent::TriggerAutomaticArtistSearch(0), - LidarrEvent::UpdateAndScanArtist(0) + LidarrEvent::UpdateAndScanArtist(0), + LidarrEvent::UpdateDownloads )] event: LidarrEvent, ) { @@ -81,7 +89,6 @@ mod tests { #[rstest] #[case(LidarrEvent::GetDiskSpace, "/diskspace")] - #[case(LidarrEvent::GetDownloads(500), "/queue")] #[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")] #[case(LidarrEvent::GetQualityProfiles, "/qualityprofile")] #[case(LidarrEvent::GetRootFolders, "/rootfolder")] diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs index cf9086d..dbef5e4 100644 --- a/src/network/lidarr_network/mod.rs +++ b/src/network/lidarr_network/mod.rs @@ -28,6 +28,7 @@ pub enum LidarrEvent { AddTag(String), DeleteAlbum(DeleteParams), DeleteArtist(DeleteParams), + DeleteDownload(i64), DeleteTag(i64), EditArtist(EditArtistParams), GetAlbums(i64), @@ -52,6 +53,7 @@ pub enum LidarrEvent { TriggerAutomaticArtistSearch(i64), UpdateAllArtists, UpdateAndScanArtist(i64), + UpdateDownloads, } impl NetworkResource for LidarrEvent { @@ -69,13 +71,14 @@ impl NetworkResource for LidarrEvent { | LidarrEvent::GetAlbumDetails(_) | LidarrEvent::DeleteAlbum(_) => "/album", LidarrEvent::GetDiskSpace => "/diskspace", - LidarrEvent::GetDownloads(_) => "/queue", + LidarrEvent::GetDownloads(_) | LidarrEvent::DeleteDownload(_) => "/queue", LidarrEvent::GetHistory(_) => "/history", LidarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed", LidarrEvent::GetHostConfig | LidarrEvent::GetSecurityConfig => "/config/host", LidarrEvent::TriggerAutomaticArtistSearch(_) | LidarrEvent::UpdateAllArtists - | LidarrEvent::UpdateAndScanArtist(_) => "/command", + | LidarrEvent::UpdateAndScanArtist(_) + | LidarrEvent::UpdateDownloads => "/command", LidarrEvent::GetMetadataProfiles => "/metadataprofile", LidarrEvent::GetQualityProfiles => "/qualityprofile", LidarrEvent::GetRootFolders => "/rootfolder", @@ -105,6 +108,10 @@ impl Network<'_, '_> { LidarrEvent::DeleteArtist(params) => { self.delete_artist(params).await.map(LidarrSerdeable::from) } + LidarrEvent::DeleteDownload(download_id) => self + .delete_lidarr_download(download_id) + .await + .map(LidarrSerdeable::from), LidarrEvent::DeleteTag(tag_id) => self .delete_lidarr_tag(tag_id) .await @@ -182,6 +189,10 @@ impl Network<'_, '_> { .map(LidarrSerdeable::from), LidarrEvent::EditArtist(params) => self.edit_artist(params).await.map(LidarrSerdeable::from), LidarrEvent::AddArtist(body) => self.add_artist(body).await.map(LidarrSerdeable::from), + LidarrEvent::UpdateDownloads => self + .update_lidarr_downloads() + .await + .map(LidarrSerdeable::from), } } diff --git a/src/ui/lidarr_ui/downloads/downloads_ui_tests.rs b/src/ui/lidarr_ui/downloads/downloads_ui_tests.rs new file mode 100644 index 0000000..f6be857 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/downloads_ui_tests.rs @@ -0,0 +1,72 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS}; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::downloads::DownloadsUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_downloads_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if DOWNLOADS_BLOCKS.contains(&active_lidarr_block) { + assert!(DownloadsUi::accepts(active_lidarr_block.into())); + } else { + assert!(!DownloadsUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[test] + fn test_downloads_ui_renders_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DownloadsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_downloads_ui_renders_empty_downloads() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::Downloads.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DownloadsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_downloads_ui_renders( + #[values( + ActiveLidarrBlock::Downloads, + ActiveLidarrBlock::DeleteDownloadPrompt, + ActiveLidarrBlock::UpdateDownloadsPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + DownloadsUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("downloads_ui_{active_lidarr_block}"), output); + } + } +} diff --git a/src/ui/lidarr_ui/downloads/mod.rs b/src/ui/lidarr_ui/downloads/mod.rs new file mode 100644 index 0000000..2e0ed1f --- /dev/null +++ b/src/ui/lidarr_ui/downloads/mod.rs @@ -0,0 +1,146 @@ +use ratatui::Frame; +use ratatui::layout::{Constraint, Rect}; +use ratatui::widgets::{Cell, Row}; + +use crate::app::App; +use crate::models::lidarr_models::DownloadRecord; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, DOWNLOADS_BLOCKS}; +use crate::models::{HorizontallyScrollableText, Route}; +use crate::ui::DrawUi; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::utils::convert_f64_to_gb; + +#[cfg(test)] +#[path = "downloads_ui_tests.rs"] +mod downloads_ui_tests; + +pub(super) struct DownloadsUi; + +impl DrawUi for DownloadsUi { + fn accepts(route: Route) -> bool { + if let Route::Lidarr(active_lidarr_block, _) = route { + return DOWNLOADS_BLOCKS.contains(&active_lidarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + draw_downloads(f, app, area); + + match active_lidarr_block { + ActiveLidarrBlock::DeleteDownloadPrompt => { + let prompt = format!( + "Do you really want to delete this download: \n{}?", + app.data.lidarr_data.downloads.current_selection().title + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Cancel Download") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + ActiveLidarrBlock::UpdateDownloadsPrompt => { + let confirmation_prompt = ConfirmationPrompt::new() + .title("Update Downloads") + .prompt("Do you want to update your downloads?") + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + _ => (), + } + } + } +} + +fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let current_selection = if app.data.lidarr_data.downloads.items.is_empty() { + DownloadRecord::default() + } else { + app.data.lidarr_data.downloads.current_selection().clone() + }; + + let downloads_row_mapping = |download_record: &DownloadRecord| { + let DownloadRecord { + title, + size, + sizeleft, + download_client, + indexer, + output_path, + .. + } = download_record; + + if output_path.is_some() { + output_path.as_ref().unwrap().scroll_left_or_reset( + get_width_from_percentage(area, 18), + current_selection == *download_record, + app.ui_scroll_tick_count == 0, + ); + } + + let percent = if *size == 0.0 { + 0.0 + } else { + 1f64 - (*sizeleft / *size) + }; + let file_size: f64 = convert_f64_to_gb(*size); + + Row::new(vec![ + Cell::from(title.to_owned()), + Cell::from(format!("{:.0}%", percent * 100.0)), + Cell::from(format!("{file_size:.2} GB")), + Cell::from( + output_path + .as_ref() + .unwrap_or(&HorizontallyScrollableText::default()) + .to_string(), + ), + Cell::from(indexer.to_owned()), + Cell::from( + download_client + .as_ref() + .unwrap_or(&String::new()) + .to_owned(), + ), + ]) + .primary() + }; + let downloads_table = ManagarrTable::new( + Some(&mut app.data.lidarr_data.downloads), + downloads_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .headers([ + "Title", + "Percent Complete", + "Size", + "Output Path", + "Indexer", + "Download Client", + ]) + .constraints([ + Constraint::Percentage(30), + Constraint::Percentage(11), + Constraint::Percentage(11), + Constraint::Percentage(18), + Constraint::Percentage(17), + Constraint::Percentage(13), + ]); + + f.render_widget(downloads_table, area); +} diff --git a/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_DeleteDownloadPrompt.snap b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_DeleteDownloadPrompt.snap new file mode 100644 index 0000000..fae0b18 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_DeleteDownloadPrompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Title Percent Complete Size Output Path Indexer Download Client +=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission + + + + + + + + + + + + + + ╭──────────────────── Cancel Download ────────────────────╮ + │ Do you really want to delete this download: │ + │ Test download title? │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_Downloads.snap b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_Downloads.snap new file mode 100644 index 0000000..1be9a81 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_Downloads.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Title Percent Complete Size Output Path Indexer Download Client +=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission diff --git a/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_UpdateDownloadsPrompt.snap b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_UpdateDownloadsPrompt.snap new file mode 100644 index 0000000..992b606 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_UpdateDownloadsPrompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Title Percent Complete Size Output Path Indexer Download Client +=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission + + + + + + + + + + + + + + ╭─────────────────── Update Downloads ────────────────────╮ + │ Do you want to update your downloads? │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_empty_downloads.snap b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_empty_downloads.snap new file mode 100644 index 0000000..d20a8d6 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_empty_downloads.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_loading.snap b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_loading.snap new file mode 100644 index 0000000..7146ea5 --- /dev/null +++ b/src/ui/lidarr_ui/downloads/snapshots/managarr__ui__lidarr_ui__downloads__downloads_ui_tests__tests__snapshot_tests__downloads_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/lidarr_ui/downloads/downloads_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/lidarr_ui/lidarr_ui_tests.rs b/src/ui/lidarr_ui/lidarr_ui_tests.rs index 46fd44a..62a8663 100644 --- a/src/ui/lidarr_ui/lidarr_ui_tests.rs +++ b/src/ui/lidarr_ui/lidarr_ui_tests.rs @@ -13,4 +13,30 @@ mod tests { assert!(LidarrUi::accepts(Route::Lidarr(lidarr_block, None))); } } + + mod snapshot_tests { + use super::*; + use crate::app::App; + use crate::ui::ui_test_utils::test_utils::{TerminalSize, render_to_string_with_app}; + use rstest::rstest; + + #[rstest] + #[case(ActiveLidarrBlock::Artists, 0)] + #[case(ActiveLidarrBlock::Downloads, 1)] + #[case(ActiveLidarrBlock::History, 2)] + fn test_lidarr_ui_renders_lidarr_tabs( + #[case] active_lidarr_block: ActiveLidarrBlock, + #[case] index: usize, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + app.data.lidarr_data.main_tabs.set_index(index); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + LidarrUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(format!("lidarr_tabs_{active_lidarr_block}"), output); + } + } } diff --git a/src/ui/lidarr_ui/mod.rs b/src/ui/lidarr_ui/mod.rs index 9dcc2bc..112db62 100644 --- a/src/ui/lidarr_ui/mod.rs +++ b/src/ui/lidarr_ui/mod.rs @@ -15,6 +15,15 @@ use ratatui::{ widgets::Paragraph, }; +use super::{ + DrawUi, draw_tabs, + styles::ManagarrStyle, + utils::{ + borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block, + }, + widgets::loading_block::LoadingBlock, +}; +use crate::ui::lidarr_ui::downloads::DownloadsUi; use crate::{ app::App, logos::LIDARR_LOGO, @@ -27,15 +36,7 @@ use crate::{ utils::convert_to_gb, }; -use super::{ - DrawUi, draw_tabs, - styles::ManagarrStyle, - utils::{ - borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block, - }, - widgets::loading_block::LoadingBlock, -}; - +mod downloads; mod history; mod library; mod lidarr_ui_utils; @@ -57,6 +58,7 @@ impl DrawUi for LidarrUi { match route { _ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area), + _ if DownloadsUi::accepts(route) => DownloadsUi::draw(f, app, content_area), _ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area), _ => (), } diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap new file mode 100644 index 0000000..7fc3b4c --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ History │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags │ +│=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap new file mode 100644 index 0000000..8e14902 --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ History │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Title Percent Complete Size Output Path Indexer Download Client │ +│=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap new file mode 100644 index 0000000..50a3497 --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ History │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Source Title ▼ Event Type Quality Date │ +│=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯