feat(handler): Full handler support for the Season details UI in Sonarr
This commit is contained in:
@@ -10,10 +10,7 @@ mod tests {
|
||||
use crate::event::Key;
|
||||
use crate::handlers::sonarr_handlers::library::{series_sorting_options, LibraryHandler};
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ActiveSonarrBlock, ADD_SERIES_BLOCKS, DELETE_SERIES_BLOCKS, EDIT_SERIES_BLOCKS, LIBRARY_BLOCKS,
|
||||
SERIES_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS, DELETE_SERIES_BLOCKS, EDIT_SERIES_BLOCKS, LIBRARY_BLOCKS, SEASON_DETAILS_BLOCKS, SERIES_DETAILS_BLOCKS};
|
||||
use crate::models::sonarr_models::{Series, SeriesStatus, SeriesType};
|
||||
use crate::test_handler_delegation;
|
||||
|
||||
@@ -543,6 +540,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_season_details_blocks_to_season_details_handler(
|
||||
#[values(
|
||||
ActiveSonarrBlock::SeasonDetails,
|
||||
ActiveSonarrBlock::SeasonHistory,
|
||||
ActiveSonarrBlock::SearchEpisodes,
|
||||
ActiveSonarrBlock::SearchEpisodesError,
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
|
||||
ActiveSonarrBlock::SearchSeasonHistory,
|
||||
ActiveSonarrBlock::SearchSeasonHistoryError,
|
||||
ActiveSonarrBlock::FilterSeasonHistory,
|
||||
ActiveSonarrBlock::FilterSeasonHistoryError,
|
||||
ActiveSonarrBlock::SeasonHistorySortPrompt,
|
||||
ActiveSonarrBlock::SeasonHistoryDetails,
|
||||
ActiveSonarrBlock::ManualSeasonSearch,
|
||||
ActiveSonarrBlock::ManualSeasonSearchSortPrompt,
|
||||
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
|
||||
)]
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
) {
|
||||
test_handler_delegation!(
|
||||
LibraryHandler,
|
||||
ActiveSonarrBlock::Series,
|
||||
active_sonarr_block
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_delegates_edit_series_blocks_to_edit_series_handler(
|
||||
#[values(
|
||||
@@ -768,6 +792,7 @@ mod tests {
|
||||
library_handler_blocks.extend(DELETE_SERIES_BLOCKS);
|
||||
library_handler_blocks.extend(EDIT_SERIES_BLOCKS);
|
||||
library_handler_blocks.extend(SERIES_DETAILS_BLOCKS);
|
||||
library_handler_blocks.extend(SEASON_DETAILS_BLOCKS);
|
||||
|
||||
ActiveSonarrBlock::iter().for_each(|active_sonarr_block| {
|
||||
if library_handler_blocks.contains(&active_sonarr_block) {
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::{
|
||||
|
||||
use super::handle_change_tab_left_right_keys;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::handlers::sonarr_handlers::library::season_details_handler::SeasonDetailsHandler;
|
||||
use crate::handlers::sonarr_handlers::library::series_details_handler::SeriesDetailsHandler;
|
||||
use crate::handlers::table_handler::TableHandlingConfig;
|
||||
|
||||
@@ -32,6 +33,7 @@ mod delete_series_handler;
|
||||
#[path = "library_handler_tests.rs"]
|
||||
mod library_handler_tests;
|
||||
mod series_details_handler;
|
||||
mod season_details_handler;
|
||||
|
||||
pub(super) struct LibraryHandler<'a, 'b> {
|
||||
key: Key,
|
||||
@@ -75,6 +77,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, '
|
||||
SeriesDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ if SeasonDetailsHandler::accepts(self.active_sonarr_block) => {
|
||||
SeasonDetailsHandler::with(self.key, self.app, self.active_sonarr_block, self.context)
|
||||
.handle();
|
||||
}
|
||||
_ => self.handle_key_event(),
|
||||
}
|
||||
}
|
||||
@@ -85,6 +91,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for LibraryHandler<'a, '
|
||||
|| DeleteSeriesHandler::accepts(active_block)
|
||||
|| EditSeriesHandler::accepts(active_block)
|
||||
|| SeriesDetailsHandler::accepts(active_block)
|
||||
|| SeasonDetailsHandler::accepts(active_block)
|
||||
|| LIBRARY_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handle_table_events;
|
||||
use crate::handlers::sonarr_handlers::history::history_sorting_options;
|
||||
use crate::handlers::table_handler::TableHandlingConfig;
|
||||
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SEASON_DETAILS_BLOCKS};
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::sonarr_models::{
|
||||
Episode, SonarrHistoryItem, SonarrRelease, SonarrReleaseDownloadBody,
|
||||
};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use serde_json::Number;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "season_details_handler_tests.rs"]
|
||||
mod season_details_handler_tests;
|
||||
|
||||
pub(super) struct SeasonDetailsHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
_context: Option<ActiveSonarrBlock>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> SeasonDetailsHandler<'a, 'b> {
|
||||
handle_table_events!(
|
||||
self,
|
||||
episodes,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.expect("Season details modal is undefined")
|
||||
.episodes,
|
||||
Episode
|
||||
);
|
||||
handle_table_events!(
|
||||
self,
|
||||
season_history,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.expect("Season details modal is undefined")
|
||||
.season_history,
|
||||
SonarrHistoryItem
|
||||
);
|
||||
handle_table_events!(
|
||||
self,
|
||||
season_releases,
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.expect("Season details modal is undefined")
|
||||
.season_releases,
|
||||
SonarrRelease
|
||||
);
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeasonDetailsHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let episodes_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveSonarrBlock::SeasonDetails.into())
|
||||
.searching_block(ActiveSonarrBlock::SearchEpisodes.into())
|
||||
.search_error_block(ActiveSonarrBlock::SearchEpisodesError.into())
|
||||
.search_field_fn(|episode: &Episode| &episode.title);
|
||||
let season_history_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveSonarrBlock::SeasonHistory.into())
|
||||
.sorting_block(ActiveSonarrBlock::SeasonHistorySortPrompt.into())
|
||||
.sort_options(history_sorting_options())
|
||||
.sort_by_fn(|a: &SonarrHistoryItem, b: &SonarrHistoryItem| a.id.cmp(&b.id))
|
||||
.searching_block(ActiveSonarrBlock::SearchSeasonHistory.into())
|
||||
.search_error_block(ActiveSonarrBlock::SearchSeasonHistoryError.into())
|
||||
.search_field_fn(|history_item: &SonarrHistoryItem| &history_item.source_title.text)
|
||||
.filtering_block(ActiveSonarrBlock::FilterSeasonHistory.into())
|
||||
.filter_error_block(ActiveSonarrBlock::FilterSeasonHistoryError.into())
|
||||
.filter_field_fn(|history_item: &SonarrHistoryItem| &history_item.source_title.text);
|
||||
let season_releases_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveSonarrBlock::ManualSeasonSearch.into())
|
||||
.sorting_block(ActiveSonarrBlock::ManualSeasonSearchSortPrompt.into())
|
||||
.sort_options(releases_sorting_options());
|
||||
|
||||
if !self.handle_episodes_table_events(episodes_table_handling_config)
|
||||
&& !self.handle_season_history_table_events(season_history_table_handling_config)
|
||||
&& !self.handle_season_releases_table_events(season_releases_table_handling_config)
|
||||
{
|
||||
self.handle_key_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveSonarrBlock) -> bool {
|
||||
SEASON_DETAILS_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn with(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveSonarrBlock,
|
||||
context: Option<ActiveSonarrBlock>,
|
||||
) -> SeasonDetailsHandler<'a, 'b> {
|
||||
SeasonDetailsHandler {
|
||||
key,
|
||||
app,
|
||||
active_sonarr_block: active_block,
|
||||
_context: context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading
|
||||
&& if let Some(season_details_modal) = &self.app.data.sonarr_data.season_details_modal {
|
||||
match self.active_sonarr_block {
|
||||
ActiveSonarrBlock::SeasonDetails => !season_details_modal.episodes.is_empty(),
|
||||
ActiveSonarrBlock::SeasonHistory => !season_details_modal.season_history.is_empty(),
|
||||
ActiveSonarrBlock::ManualSeasonSearch => !season_details_modal.season_releases.is_empty(),
|
||||
_ => true,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
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_sonarr_block == ActiveSonarrBlock::SeasonDetails {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveSonarrBlock::DeleteEpisodeFilePrompt.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
match self.active_sonarr_block {
|
||||
ActiveSonarrBlock::SeasonDetails
|
||||
| ActiveSonarrBlock::SeasonHistory
|
||||
| ActiveSonarrBlock::ManualSeasonSearch => match self.key {
|
||||
_ if self.key == DEFAULT_KEYBINDINGS.left.key => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.season_details_tabs
|
||||
.previous();
|
||||
self.app.pop_and_push_navigation_stack(
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_details_tabs
|
||||
.get_active_route(),
|
||||
);
|
||||
}
|
||||
_ if self.key == DEFAULT_KEYBINDINGS.right.key => {
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.season_details_tabs
|
||||
.next();
|
||||
self.app.pop_and_push_navigation_stack(
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_details_tabs
|
||||
.get_active_route(),
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt
|
||||
| ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt
|
||||
| ActiveSonarrBlock::DeleteEpisodeFilePrompt => {
|
||||
handle_prompt_toggle(self.app, self.key);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match self.active_sonarr_block {
|
||||
ActiveSonarrBlock::SeasonHistory => self
|
||||
.app
|
||||
.push_navigation_stack(ActiveSonarrBlock::SeasonHistoryDetails.into()),
|
||||
ActiveSonarrBlock::DeleteEpisodeFilePrompt => {
|
||||
if self.app.data.sonarr_data.prompt_confirm {
|
||||
self.app.data.sonarr_data.prompt_confirm_action =
|
||||
Some(SonarrEvent::DeleteEpisodeFile(None));
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt => {
|
||||
if self.app.data.sonarr_data.prompt_confirm {
|
||||
self.app.data.sonarr_data.prompt_confirm_action =
|
||||
Some(SonarrEvent::TriggerAutomaticSeasonSearch(None));
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveSonarrBlock::ManualSeasonSearch => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into());
|
||||
}
|
||||
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt => {
|
||||
if self.app.data.sonarr_data.prompt_confirm {
|
||||
let SonarrRelease {
|
||||
guid, indexer_id, ..
|
||||
} = self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_releases
|
||||
.current_selection();
|
||||
let series_id = self.app.data.sonarr_data.series.current_selection().id;
|
||||
let season_number = self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.seasons
|
||||
.current_selection()
|
||||
.season_number;
|
||||
let params = SonarrReleaseDownloadBody {
|
||||
guid: guid.clone(),
|
||||
indexer_id: *indexer_id,
|
||||
series_id: Some(series_id),
|
||||
season_number: Some(season_number),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
};
|
||||
self.app.data.sonarr_data.prompt_confirm_action =
|
||||
Some(SonarrEvent::DownloadRelease(params));
|
||||
}
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
match self.active_sonarr_block {
|
||||
ActiveSonarrBlock::SeasonDetails | ActiveSonarrBlock::ManualSeasonSearch => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.sonarr_data.season_details_modal = None;
|
||||
}
|
||||
ActiveSonarrBlock::SeasonHistoryDetails => {
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveSonarrBlock::SeasonHistory => {
|
||||
if self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_history
|
||||
.filtered_items
|
||||
.is_some()
|
||||
{
|
||||
self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.season_history
|
||||
.filtered_items = None;
|
||||
} else {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.sonarr_data.season_details_modal = None;
|
||||
}
|
||||
}
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt
|
||||
| ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt
|
||||
| ActiveSonarrBlock::DeleteEpisodeFilePrompt => {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.sonarr_data.prompt_confirm = false;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
match self.active_sonarr_block {
|
||||
ActiveSonarrBlock::SeasonDetails
|
||||
| ActiveSonarrBlock::SeasonHistory
|
||||
| ActiveSonarrBlock::ManualSeasonSearch => match self.key {
|
||||
_ if self.key == DEFAULT_KEYBINDINGS.refresh.key => {
|
||||
self
|
||||
.app
|
||||
.pop_and_push_navigation_stack(self.active_sonarr_block.into());
|
||||
}
|
||||
_ if self.key == DEFAULT_KEYBINDINGS.auto_search.key => {
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchSeasonPrompt.into());
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt
|
||||
if key == DEFAULT_KEYBINDINGS.confirm.key =>
|
||||
{
|
||||
self.app.data.sonarr_data.prompt_confirm = true;
|
||||
self.app.data.sonarr_data.prompt_confirm_action =
|
||||
Some(SonarrEvent::TriggerAutomaticSeasonSearch(None));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveSonarrBlock::DeleteEpisodeFilePrompt if key == DEFAULT_KEYBINDINGS.confirm.key => {
|
||||
self.app.data.sonarr_data.prompt_confirm = true;
|
||||
self.app.data.sonarr_data.prompt_confirm_action =
|
||||
Some(SonarrEvent::DeleteEpisodeFile(None));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt
|
||||
if key == DEFAULT_KEYBINDINGS.confirm.key =>
|
||||
{
|
||||
self.app.data.sonarr_data.prompt_confirm = true;
|
||||
let SonarrRelease {
|
||||
guid, indexer_id, ..
|
||||
} = self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_releases
|
||||
.current_selection();
|
||||
let series_id = self.app.data.sonarr_data.series.current_selection().id;
|
||||
let season_number = self
|
||||
.app
|
||||
.data
|
||||
.sonarr_data
|
||||
.seasons
|
||||
.current_selection()
|
||||
.season_number;
|
||||
let params = SonarrReleaseDownloadBody {
|
||||
guid: guid.clone(),
|
||||
indexer_id: *indexer_id,
|
||||
series_id: Some(series_id),
|
||||
season_number: Some(season_number),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
};
|
||||
self.app.data.sonarr_data.prompt_confirm_action =
|
||||
Some(SonarrEvent::DownloadRelease(params));
|
||||
|
||||
self.app.pop_navigation_stack();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn releases_sorting_options() -> Vec<SortOption<SonarrRelease>> {
|
||||
vec![
|
||||
SortOption {
|
||||
name: "Source",
|
||||
cmp_fn: Some(|a, b| a.protocol.cmp(&b.protocol)),
|
||||
},
|
||||
SortOption {
|
||||
name: "Age",
|
||||
cmp_fn: Some(|a, b| a.age.cmp(&b.age)),
|
||||
},
|
||||
SortOption {
|
||||
name: "Rejected",
|
||||
cmp_fn: Some(|a, b| a.rejected.cmp(&b.rejected)),
|
||||
},
|
||||
SortOption {
|
||||
name: "Title",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.title
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.title.text.to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Indexer",
|
||||
cmp_fn: Some(|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase())),
|
||||
},
|
||||
SortOption {
|
||||
name: "Size",
|
||||
cmp_fn: Some(|a, b| a.size.cmp(&b.size)),
|
||||
},
|
||||
SortOption {
|
||||
name: "Peers",
|
||||
cmp_fn: Some(|a, b| {
|
||||
let default_number = Number::from(i64::MAX);
|
||||
let seeder_a = a
|
||||
.seeders
|
||||
.as_ref()
|
||||
.unwrap_or(&default_number)
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let seeder_b = b
|
||||
.seeders
|
||||
.as_ref()
|
||||
.unwrap_or(&default_number)
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
|
||||
seeder_a.cmp(&seeder_b)
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Language",
|
||||
cmp_fn: Some(|a, b| {
|
||||
let default_language_vec = vec![Language {
|
||||
id: 1,
|
||||
name: "_".to_owned(),
|
||||
}];
|
||||
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
||||
let language_b = &b.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
||||
|
||||
language_a.cmp(language_b)
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Quality",
|
||||
cmp_fn: Some(|a, b| a.quality.cmp(&b.quality)),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,982 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::App;
|
||||
use crate::handlers::sonarr_handlers::library::season_details_handler::{
|
||||
releases_sorting_options, SeasonDetailsHandler,
|
||||
};
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ActiveSonarrBlock, SEASON_DETAILS_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::{Language, Quality, QualityWrapper};
|
||||
use crate::models::sonarr_models::{SonarrRelease, SonarrReleaseDownloadBody};
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::Number;
|
||||
use std::cmp::Ordering;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
mod test_handle_delete {
|
||||
use super::*;
|
||||
use crate::event::Key;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
|
||||
|
||||
#[test]
|
||||
fn test_delete_episode_prompt() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
|
||||
|
||||
SeasonDetailsHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::SeasonDetails, None)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::DeleteEpisodeFilePrompt.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_episode_prompt_no_op_when_not_ready() {
|
||||
let mut app = App::default();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
|
||||
app.is_loading = true;
|
||||
|
||||
SeasonDetailsHandler::with(DELETE_KEY, &mut app, ActiveSonarrBlock::SeasonDetails, None)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::SeasonDetails.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_left_right_actions {
|
||||
use super::*;
|
||||
use crate::event::Key;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
fn test_left_right_prompt_toggle(
|
||||
#[values(
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
|
||||
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt,
|
||||
ActiveSonarrBlock::DeleteEpisodeFilePrompt
|
||||
)]
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
#[values(Key::Left, Key::Right)] key: Key,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
|
||||
|
||||
SeasonDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle();
|
||||
|
||||
assert!(app.data.sonarr_data.prompt_confirm);
|
||||
|
||||
SeasonDetailsHandler::with(key, &mut app, active_sonarr_block, None).handle();
|
||||
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory)]
|
||||
#[case(
|
||||
ActiveSonarrBlock::SeasonHistory,
|
||||
ActiveSonarrBlock::ManualSeasonSearch
|
||||
)]
|
||||
#[case(
|
||||
ActiveSonarrBlock::ManualSeasonSearch,
|
||||
ActiveSonarrBlock::SeasonDetails
|
||||
)]
|
||||
fn test_season_details_tabs_left_right_action(
|
||||
#[case] left_block: ActiveSonarrBlock,
|
||||
#[case] right_block: ActiveSonarrBlock,
|
||||
#[values(true, false)] is_ready: bool,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into());
|
||||
app.is_loading = is_ready;
|
||||
app.push_navigation_stack(right_block.into());
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.season_details_tabs
|
||||
.index = app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_details_tabs
|
||||
.tabs
|
||||
.iter()
|
||||
.position(|tab_route| tab_route.route == right_block.into())
|
||||
.unwrap_or_default();
|
||||
|
||||
SeasonDetailsHandler::with(DEFAULT_KEYBINDINGS.left.key, &mut app, right_block, None)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_details_tabs
|
||||
.get_active_route()
|
||||
);
|
||||
assert_eq!(app.get_current_route(), left_block.into());
|
||||
|
||||
SeasonDetailsHandler::with(DEFAULT_KEYBINDINGS.right.key, &mut app, left_block, None)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_details_tabs
|
||||
.get_active_route()
|
||||
);
|
||||
assert_eq!(app.get_current_route(), right_block.into());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_submit {
|
||||
use super::*;
|
||||
use crate::event::Key;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
||||
|
||||
#[test]
|
||||
fn test_season_history_submit() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
|
||||
SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::SeasonHistoryDetails.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_history_submit_no_op_when_season_history_is_empty() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.season_history = StatefulTable::default();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into());
|
||||
|
||||
SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::SeasonHistory.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_history_submit_no_op_when_not_ready() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into());
|
||||
|
||||
SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::SeasonHistory.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(None)
|
||||
)]
|
||||
#[case(
|
||||
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
|
||||
SonarrEvent::DeleteEpisodeFile(None)
|
||||
)]
|
||||
fn test_season_details_prompt_confirm_submit(
|
||||
#[case] prompt_block: ActiveSonarrBlock,
|
||||
#[case] expected_action: SonarrEvent,
|
||||
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory)]
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.data.sonarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(active_sonarr_block.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.get_current_route(), active_sonarr_block.into());
|
||||
assert_eq!(
|
||||
app.data.sonarr_data.prompt_confirm_action,
|
||||
Some(expected_action)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_details_manual_search_confirm_prompt_confirm_submit() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.data.sonarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into());
|
||||
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into());
|
||||
|
||||
SeasonDetailsHandler::with(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::ManualSeasonSearch.into()
|
||||
);
|
||||
assert_eq!(
|
||||
app.data.sonarr_data.prompt_confirm_action,
|
||||
Some(SonarrEvent::DownloadRelease(SonarrReleaseDownloadBody {
|
||||
guid: String::new(),
|
||||
indexer_id: 0,
|
||||
series_id: Some(0),
|
||||
season_number: Some(0),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_season_details_prompt_decline_submit(
|
||||
#[values(
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
|
||||
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
|
||||
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt
|
||||
)]
|
||||
prompt_block: ActiveSonarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
SeasonDetailsHandler::with(SUBMIT_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::SeasonDetails.into()
|
||||
);
|
||||
assert_eq!(app.data.sonarr_data.prompt_confirm_action, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_season_search_submit() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into());
|
||||
|
||||
SeasonDetailsHandler::with(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveSonarrBlock::ManualSeasonSearch,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_season_search_submit_no_op_when_not_ready() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into());
|
||||
|
||||
SeasonDetailsHandler::with(
|
||||
SUBMIT_KEY,
|
||||
&mut app,
|
||||
ActiveSonarrBlock::ManualSeasonSearch,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::ManualSeasonSearch.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use super::*;
|
||||
use crate::event::Key;
|
||||
use crate::models::sonarr_models::SonarrHistoryItem;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::widgets::TableState;
|
||||
|
||||
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
||||
|
||||
#[test]
|
||||
fn test_season_history_details_block_esc() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into());
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistoryDetails.into());
|
||||
|
||||
SeasonDetailsHandler::with(
|
||||
ESC_KEY,
|
||||
&mut app,
|
||||
ActiveSonarrBlock::SeasonHistoryDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::SeasonHistory.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_season_details_prompts_esc(
|
||||
#[values(
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
|
||||
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
|
||||
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt
|
||||
)]
|
||||
prompt_block: ActiveSonarrBlock,
|
||||
#[values(true, false)] is_ready: bool,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.is_loading = is_ready;
|
||||
app.data.sonarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
SeasonDetailsHandler::with(ESC_KEY, &mut app, prompt_block, None).handle();
|
||||
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::SeasonDetails.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_history_esc_resets_filter_if_one_is_set_instead_of_closing_the_window() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
let mut season_history = StatefulTable {
|
||||
filter: Some("Test".into()),
|
||||
filtered_items: Some(vec![SonarrHistoryItem::default()]),
|
||||
filtered_state: Some(TableState::default()),
|
||||
..StatefulTable::default()
|
||||
};
|
||||
season_history.set_items(vec![SonarrHistoryItem::default()]);
|
||||
app.data.sonarr_data.season_details_modal.as_mut().unwrap().season_history = season_history;
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into());
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into());
|
||||
|
||||
SeasonDetailsHandler::with(ESC_KEY, &mut app, ActiveSonarrBlock::SeasonHistory, None)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::SeasonHistory.into()
|
||||
);
|
||||
assert!(app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_history
|
||||
.filter
|
||||
.is_none());
|
||||
assert!(app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_history
|
||||
.filtered_items
|
||||
.is_none());
|
||||
assert!(app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_history
|
||||
.filtered_state
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_season_details_tabs_esc(
|
||||
#[values(
|
||||
ActiveSonarrBlock::SeasonDetails,
|
||||
ActiveSonarrBlock::SeasonHistory,
|
||||
ActiveSonarrBlock::ManualSeasonSearch
|
||||
)] active_sonarr_block: ActiveSonarrBlock
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into());
|
||||
app.push_navigation_stack(active_sonarr_block.into());
|
||||
|
||||
SeasonDetailsHandler::with(ESC_KEY, &mut app, active_sonarr_block, None).handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::SeriesDetails.into()
|
||||
);
|
||||
assert!(app.data.sonarr_data.season_details_modal.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use super::*;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::sonarr_test_utils::utils::create_test_sonarr_data;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[rstest]
|
||||
fn test_auto_search_key(
|
||||
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory, ActiveSonarrBlock::ManualSeasonSearch)]
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.push_navigation_stack(active_sonarr_block.into());
|
||||
|
||||
SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.auto_search.key,
|
||||
&mut app,
|
||||
active_sonarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_auto_search_key_no_op_when_not_ready(
|
||||
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory, ActiveSonarrBlock::ManualSeasonSearch)]
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(active_sonarr_block.into());
|
||||
|
||||
SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.auto_search.key,
|
||||
&mut app,
|
||||
active_sonarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), active_sonarr_block.into());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_refresh_key(
|
||||
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory, ActiveSonarrBlock::ManualSeasonSearch)]
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.push_navigation_stack(active_sonarr_block.into());
|
||||
app.is_routing = false;
|
||||
|
||||
SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
active_sonarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), active_sonarr_block.into());
|
||||
assert!(app.is_routing);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_refresh_key_no_op_when_not_ready(
|
||||
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory, ActiveSonarrBlock::ManualSeasonSearch)]
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(active_sonarr_block.into());
|
||||
app.is_routing = false;
|
||||
|
||||
SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
active_sonarr_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(app.get_current_route(), active_sonarr_block.into());
|
||||
assert!(!app.is_routing);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
ActiveSonarrBlock::AutomaticallySearchSeasonPrompt,
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(None)
|
||||
)]
|
||||
#[case(
|
||||
ActiveSonarrBlock::DeleteEpisodeFilePrompt,
|
||||
SonarrEvent::DeleteEpisodeFile(None)
|
||||
)]
|
||||
fn test_season_details_prompt_confirm_confirm_key(
|
||||
#[case] prompt_block: ActiveSonarrBlock,
|
||||
#[case] expected_action: SonarrEvent,
|
||||
#[values(ActiveSonarrBlock::SeasonDetails, ActiveSonarrBlock::SeasonHistory)]
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.data.sonarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(active_sonarr_block.into());
|
||||
app.push_navigation_stack(prompt_block.into());
|
||||
|
||||
SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
prompt_block,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.get_current_route(), active_sonarr_block.into());
|
||||
assert_eq!(
|
||||
app.data.sonarr_data.prompt_confirm_action,
|
||||
Some(expected_action)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_details_manual_search_confirm_prompt_confirm_confirm_key() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.data.sonarr_data.prompt_confirm = true;
|
||||
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into());
|
||||
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt.into());
|
||||
|
||||
SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.confirm.key,
|
||||
&mut app,
|
||||
ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveSonarrBlock::ManualSeasonSearch.into()
|
||||
);
|
||||
assert_eq!(
|
||||
app.data.sonarr_data.prompt_confirm_action,
|
||||
Some(SonarrEvent::DownloadRelease(SonarrReleaseDownloadBody {
|
||||
guid: String::new(),
|
||||
indexer_id: 0,
|
||||
series_id: Some(0),
|
||||
season_number: Some(0),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_details_handler_accepts() {
|
||||
ActiveSonarrBlock::iter().for_each(|active_sonarr_block| {
|
||||
if SEASON_DETAILS_BLOCKS.contains(&active_sonarr_block) {
|
||||
assert!(SeasonDetailsHandler::accepts(active_sonarr_block));
|
||||
} else {
|
||||
assert!(!SeasonDetailsHandler::accepts(active_sonarr_block));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_details_handler_is_not_ready_when_loading() {
|
||||
let mut app = App::default();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveSonarrBlock::SeasonDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_details_handler_is_not_ready_when_not_loading_and_season_details_is_none() {
|
||||
let mut app = App::default();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
|
||||
|
||||
let handler = SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveSonarrBlock::SeasonDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_details_handler_is_not_ready_when_not_loading_and_episodes_table_is_empty() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
|
||||
|
||||
let handler = SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveSonarrBlock::SeasonDetails,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_details_handler_is_not_ready_when_not_loading_and_history_table_is_empty() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeasonHistory.into());
|
||||
|
||||
let handler = SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveSonarrBlock::SeasonHistory,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_season_details_handler_is_not_ready_when_not_loading_and_releases_table_is_empty() {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
|
||||
app.push_navigation_stack(ActiveSonarrBlock::ManualSeasonSearch.into());
|
||||
|
||||
let handler = SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveSonarrBlock::ManualSeasonSearch,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_season_details_handler_is_ready_when_not_loading_and_season_details_modal_is_populated(
|
||||
#[values(
|
||||
ActiveSonarrBlock::SeasonDetails,
|
||||
ActiveSonarrBlock::SeasonHistory,
|
||||
ActiveSonarrBlock::ManualSeasonSearch
|
||||
)]
|
||||
active_sonarr_block: ActiveSonarrBlock,
|
||||
) {
|
||||
let mut app = App::default();
|
||||
app.data.sonarr_data = create_test_sonarr_data();
|
||||
app.push_navigation_stack(ActiveSonarrBlock::SeriesDetails.into());
|
||||
app.push_navigation_stack(active_sonarr_block.into());
|
||||
|
||||
let handler = SeasonDetailsHandler::with(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
active_sonarr_block,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_source() {
|
||||
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering =
|
||||
|a, b| a.protocol.cmp(&b.protocol);
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = releases_sorting_options()[0].clone();
|
||||
let mut sorted_releases_vec = release_vec();
|
||||
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_releases_vec, expected_releases_vec);
|
||||
assert_str_eq!(sort_option.name, "Source");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_age() {
|
||||
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| a.age.cmp(&b.age);
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = releases_sorting_options()[1].clone();
|
||||
let mut sorted_releases_vec = release_vec();
|
||||
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_releases_vec, expected_releases_vec);
|
||||
assert_str_eq!(sort_option.name, "Age");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_rejected() {
|
||||
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering =
|
||||
|a, b| a.rejected.cmp(&b.rejected);
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = releases_sorting_options()[2].clone();
|
||||
let mut sorted_releases_vec = release_vec();
|
||||
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_releases_vec, expected_releases_vec);
|
||||
assert_str_eq!(sort_option.name, "Rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_title() {
|
||||
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| {
|
||||
a.title
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.title.text.to_lowercase())
|
||||
};
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = releases_sorting_options()[3].clone();
|
||||
let mut sorted_releases_vec = release_vec();
|
||||
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_releases_vec, expected_releases_vec);
|
||||
assert_str_eq!(sort_option.name, "Title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_indexer() {
|
||||
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering =
|
||||
|a, b| a.indexer.to_lowercase().cmp(&b.indexer.to_lowercase());
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = releases_sorting_options()[4].clone();
|
||||
let mut sorted_releases_vec = release_vec();
|
||||
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_releases_vec, expected_releases_vec);
|
||||
assert_str_eq!(sort_option.name, "Indexer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_size() {
|
||||
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering =
|
||||
|a, b| a.size.cmp(&b.size);
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = releases_sorting_options()[5].clone();
|
||||
let mut sorted_releases_vec = release_vec();
|
||||
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_releases_vec, expected_releases_vec);
|
||||
assert_str_eq!(sort_option.name, "Size");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_peers() {
|
||||
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| {
|
||||
let default_number = Number::from(i64::MAX);
|
||||
let seeder_a = a
|
||||
.seeders
|
||||
.as_ref()
|
||||
.unwrap_or(&default_number)
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let seeder_b = b
|
||||
.seeders
|
||||
.as_ref()
|
||||
.unwrap_or(&default_number)
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
|
||||
seeder_a.cmp(&seeder_b)
|
||||
};
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = releases_sorting_options()[6].clone();
|
||||
let mut sorted_releases_vec = release_vec();
|
||||
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_releases_vec, expected_releases_vec);
|
||||
assert_str_eq!(sort_option.name, "Peers");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_language() {
|
||||
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| {
|
||||
let default_language_vec = vec![Language {
|
||||
id: 1,
|
||||
name: "_".to_owned(),
|
||||
}];
|
||||
let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
||||
let language_b = &b.languages.as_ref().unwrap_or(&default_language_vec)[0];
|
||||
|
||||
language_a.cmp(language_b)
|
||||
};
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = releases_sorting_options()[7].clone();
|
||||
let mut sorted_releases_vec = release_vec();
|
||||
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_releases_vec, expected_releases_vec);
|
||||
assert_str_eq!(sort_option.name, "Language");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releases_sorting_options_quality() {
|
||||
let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering =
|
||||
|a, b| a.quality.cmp(&b.quality);
|
||||
let mut expected_releases_vec = release_vec();
|
||||
expected_releases_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = releases_sorting_options()[8].clone();
|
||||
let mut sorted_releases_vec = release_vec();
|
||||
sorted_releases_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_releases_vec, expected_releases_vec);
|
||||
assert_str_eq!(sort_option.name, "Quality");
|
||||
}
|
||||
|
||||
fn release_vec() -> Vec<SonarrRelease> {
|
||||
let release_a = SonarrRelease {
|
||||
protocol: "Protocol A".to_owned(),
|
||||
age: 1,
|
||||
title: HorizontallyScrollableText::from("Title A"),
|
||||
indexer: "Indexer A".to_owned(),
|
||||
size: 1,
|
||||
rejected: true,
|
||||
seeders: Some(Number::from(1)),
|
||||
languages: Some(vec![Language {
|
||||
id: 1,
|
||||
name: "Language A".to_owned(),
|
||||
}]),
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "Quality A".to_owned(),
|
||||
},
|
||||
},
|
||||
..SonarrRelease::default()
|
||||
};
|
||||
let release_b = SonarrRelease {
|
||||
protocol: "Protocol B".to_owned(),
|
||||
age: 2,
|
||||
title: HorizontallyScrollableText::from("title B"),
|
||||
indexer: "indexer B".to_owned(),
|
||||
size: 2,
|
||||
rejected: false,
|
||||
seeders: Some(Number::from(2)),
|
||||
languages: Some(vec![Language {
|
||||
id: 2,
|
||||
name: "Language B".to_owned(),
|
||||
}]),
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "Quality B".to_owned(),
|
||||
},
|
||||
},
|
||||
..SonarrRelease::default()
|
||||
};
|
||||
let release_c = SonarrRelease {
|
||||
protocol: "Protocol C".to_owned(),
|
||||
age: 3,
|
||||
title: HorizontallyScrollableText::from("Title C"),
|
||||
indexer: "Indexer C".to_owned(),
|
||||
size: 3,
|
||||
rejected: false,
|
||||
seeders: None,
|
||||
languages: None,
|
||||
quality: QualityWrapper {
|
||||
quality: Quality {
|
||||
name: "Quality C".to_owned(),
|
||||
},
|
||||
},
|
||||
..SonarrRelease::default()
|
||||
};
|
||||
|
||||
vec![release_a, release_b, release_c]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user