feat(readarr): add a scrollable overview popup to author details
An author's overview renders as the last line of a description box that does not scroll, so a long overview overflows with no way to read the rest. Pressing v on the author details Books tab now opens a popup holding the full overview. The popup owns its own ScrollableText, built fresh from the author's overview when it opens and torn down on Esc so a reopened popup starts at the top. Scrolling mutates only that owned state. The background paragraph renders the Author record's overview, which carries no scroll offset, so it cannot scroll along with the popup. Add a view keybinding bound to v, cite it from the author details context clues, and give the popup its own clue set so it does not inherit the hints of the view underneath.
This commit is contained in:
59 files changed
+1493
-97
No files matched your search
@@ -1,3 +1,4 @@
|
||||
use super::author_overview_handler::AuthorOverviewHandler;
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::readarr_handlers::history::history_sorting_options;
|
||||
@@ -5,13 +6,14 @@ use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
||||
use crate::matches_key;
|
||||
use crate::models::readarr_models::{Book, ReadarrHistoryItem, ReadarrRelease};
|
||||
use crate::models::servarr_data::readarr::modals::AuthorOverviewModal;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, DELETE_BOOK_SELECTION_BLOCKS,
|
||||
EDIT_AUTHOR_SELECTION_BLOCKS,
|
||||
};
|
||||
use crate::models::servarr_models::ReleaseDownloadBody;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::models::{BlockSelectionState, Route};
|
||||
use crate::models::{BlockSelectionState, Route, ScrollableText};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use serde_json::Number;
|
||||
|
||||
@@ -34,10 +36,36 @@ impl AuthorDetailsHandler<'_, '_> {
|
||||
fn extract_book_id(&self) -> i64 {
|
||||
self.app.data.readarr_data.books.current_selection().id
|
||||
}
|
||||
|
||||
fn build_author_overview_modal(&mut self) {
|
||||
let overview = self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.authors
|
||||
.current_selection()
|
||||
.overview
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
|
||||
self.app.data.readarr_data.author_overview_modal = Some(AuthorOverviewModal {
|
||||
overview: ScrollableText::with_string(overview),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for AuthorDetailsHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
if AuthorOverviewHandler::accepts(self.active_readarr_block) {
|
||||
return AuthorOverviewHandler::new(
|
||||
self.key,
|
||||
self.app,
|
||||
self.active_readarr_block,
|
||||
self._context,
|
||||
)
|
||||
.handle();
|
||||
}
|
||||
|
||||
let books_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveReadarrBlock::AuthorDetails.into())
|
||||
.searching_block(ActiveReadarrBlock::SearchBooks.into())
|
||||
@@ -78,7 +106,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for AuthorDetailsHandle
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveReadarrBlock) -> bool {
|
||||
AUTHOR_DETAILS_BLOCKS.contains(&active_block)
|
||||
AuthorOverviewHandler::accepts(active_block) || AUTHOR_DETAILS_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
@@ -294,6 +322,12 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for AuthorDetailsHandle
|
||||
self.app.data.readarr_data.selected_block =
|
||||
BlockSelectionState::new(EDIT_AUTHOR_SELECTION_BLOCKS);
|
||||
}
|
||||
_ if matches_key!(view, key) => {
|
||||
self.build_author_overview_modal();
|
||||
self
|
||||
.app
|
||||
.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
|
||||
}
|
||||
_ if matches_key!(toggle_monitoring, key)
|
||||
&& !self.app.data.readarr_data.books.is_empty() =>
|
||||
{
|
||||
|
||||
@@ -15,9 +15,10 @@ mod tests {
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::readarr_models::{ReadarrHistoryItem, ReadarrRelease};
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock,
|
||||
AUTHOR_DETAILS_BLOCKS, AUTHOR_OVERVIEW_BLOCKS, ActiveReadarrBlock,
|
||||
};
|
||||
use crate::models::servarr_models::{Quality, QualityWrapper};
|
||||
use crate::test_handler_delegation;
|
||||
|
||||
mod test_handle_delete {
|
||||
use super::*;
|
||||
@@ -465,6 +466,7 @@ mod tests {
|
||||
use crate::assert_navigation_pushed;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::readarr_handlers::library::author_details_handler::AuthorDetailsHandler;
|
||||
use crate::handlers::readarr_handlers::library::author_overview_handler::AuthorOverviewHandler;
|
||||
use crate::models::readarr_models::Author;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
ActiveReadarrBlock, EDIT_AUTHOR_SELECTION_BLOCKS,
|
||||
@@ -473,7 +475,7 @@ mod tests {
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::network::readarr_network::readarr_network_test_utils::test_utils::torrent_release;
|
||||
use crate::{assert_modal_absent, assert_modal_present, assert_navigation_popped};
|
||||
use pretty_assertions::assert_eq;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
@@ -538,6 +540,161 @@ mod tests {
|
||||
assert_modal_absent!(app.data.readarr_data.edit_author_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_view_key_opens_the_author_overview() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.readarr_data.author_overview_modal = None;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
|
||||
|
||||
AuthorDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::AuthorOverview.into());
|
||||
assert_modal_present!(app.data.readarr_data.author_overview_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_view_key_builds_the_overview_from_the_selected_author() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.readarr_data.author_overview_modal = None;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
|
||||
|
||||
AuthorDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
let overview = &app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_overview_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.overview;
|
||||
assert_eq!(overview.offset, 0);
|
||||
assert_eq!(
|
||||
overview.get_text(),
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.authors
|
||||
.current_selection()
|
||||
.overview
|
||||
.clone()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_view_key_for_an_author_with_no_overview() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.readarr_data.author_overview_modal = None;
|
||||
let mut author = app.data.readarr_data.authors.current_selection().clone();
|
||||
author.overview = None;
|
||||
app.data.readarr_data.authors.set_items(vec![author]);
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
|
||||
|
||||
AuthorDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::AuthorOverview.into());
|
||||
assert_str_eq!(
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_overview_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.overview
|
||||
.get_text(),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_view_key_reopens_the_author_overview_at_the_top() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.readarr_data.author_overview_modal = None;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
|
||||
AuthorDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
AuthorDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::AuthorOverview.into());
|
||||
assert_eq!(
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_overview_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.overview
|
||||
.offset,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_view_key_no_op_when_not_ready() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.readarr_data.author_overview_modal = None;
|
||||
app.is_loading = true;
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
|
||||
|
||||
AuthorDetailsHandler::new(
|
||||
DEFAULT_KEYBINDINGS.view.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorDetails,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(
|
||||
app.get_current_route(),
|
||||
ActiveReadarrBlock::AuthorDetails.into()
|
||||
);
|
||||
assert_modal_absent!(app.data.readarr_data.author_overview_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_details_toggle_monitoring_key() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
@@ -828,8 +985,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_author_details_handler_accepts() {
|
||||
let mut author_details_blocks = AUTHOR_DETAILS_BLOCKS.to_vec();
|
||||
author_details_blocks.extend(AUTHOR_OVERVIEW_BLOCKS);
|
||||
|
||||
ActiveReadarrBlock::iter().for_each(|readarr_block| {
|
||||
if AUTHOR_DETAILS_BLOCKS.contains(&readarr_block) {
|
||||
if author_details_blocks.contains(&readarr_block) {
|
||||
assert!(
|
||||
AuthorDetailsHandler::accepts(readarr_block),
|
||||
"{readarr_block} is not accepted by the AuthorDetailsHandler"
|
||||
@@ -840,6 +1000,15 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delegates_author_overview_blocks_to_author_overview_handler() {
|
||||
test_handler_delegation!(
|
||||
AuthorDetailsHandler,
|
||||
ActiveReadarrBlock::AuthorDetails,
|
||||
ActiveReadarrBlock::AuthorOverview
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_author_id() {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
use crate::app::App;
|
||||
use crate::event::Key;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::matches_key;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
AUTHOR_OVERVIEW_BLOCKS, ActiveReadarrBlock,
|
||||
};
|
||||
use crate::models::{Route, Scrollable, ScrollableText};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "author_overview_handler_tests.rs"]
|
||||
mod author_overview_handler_tests;
|
||||
|
||||
pub(super) struct AuthorOverviewHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_readarr_block: ActiveReadarrBlock,
|
||||
_context: Option<ActiveReadarrBlock>,
|
||||
}
|
||||
|
||||
impl AuthorOverviewHandler<'_, '_> {
|
||||
fn overview(&mut self) -> &mut ScrollableText {
|
||||
&mut self
|
||||
.app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_overview_modal
|
||||
.as_mut()
|
||||
.expect("Author overview modal is undefined")
|
||||
.overview
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveReadarrBlock> for AuthorOverviewHandler<'a, 'b> {
|
||||
fn accepts(active_block: ActiveReadarrBlock) -> bool {
|
||||
AUTHOR_OVERVIEW_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveReadarrBlock,
|
||||
_context: Option<ActiveReadarrBlock>,
|
||||
) -> AuthorOverviewHandler<'a, 'b> {
|
||||
AuthorOverviewHandler {
|
||||
key,
|
||||
app,
|
||||
active_readarr_block: active_block,
|
||||
_context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
if self.app.is_loading {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.app.data.readarr_data.author_overview_modal.is_some()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {
|
||||
self.overview().scroll_up();
|
||||
}
|
||||
|
||||
fn handle_scroll_down(&mut self) {
|
||||
self.overview().scroll_down();
|
||||
}
|
||||
|
||||
fn handle_home(&mut self) {
|
||||
self.overview().scroll_to_top();
|
||||
}
|
||||
|
||||
fn handle_end(&mut self) {
|
||||
self.overview().scroll_to_bottom();
|
||||
}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {}
|
||||
|
||||
fn handle_submit(&mut self) {}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
self.app.pop_navigation_stack();
|
||||
self.app.data.readarr_data.author_overview_modal = None;
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
if matches_key!(refresh, self.key) {
|
||||
self
|
||||
.app
|
||||
.pop_and_push_navigation_stack(self.active_readarr_block.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::readarr_handlers::library::author_overview_handler::AuthorOverviewHandler;
|
||||
use crate::models::ScrollableText;
|
||||
use crate::models::servarr_data::readarr::modals::AuthorOverviewModal;
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
AUTHOR_OVERVIEW_BLOCKS, ActiveReadarrBlock,
|
||||
};
|
||||
|
||||
mod test_handle_scroll_up_and_down {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_scroll_up() {
|
||||
let mut app = app_with_author_overview();
|
||||
set_offset(&mut app, 2);
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_scroll_up_no_op_when_not_ready() {
|
||||
let mut app = app_with_author_overview();
|
||||
set_offset(&mut app, 2);
|
||||
app.is_loading = true;
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_scroll_down() {
|
||||
let mut app = app_with_author_overview();
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_scroll_down_no_op_when_not_ready() {
|
||||
let mut app = app_with_author_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 0);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_home_end {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_home() {
|
||||
let mut app = app_with_author_overview();
|
||||
set_offset(&mut app, 3);
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.home.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_end() {
|
||||
let mut app = app_with_author_overview();
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_home_no_op_when_not_ready() {
|
||||
let mut app = app_with_author_overview();
|
||||
set_offset(&mut app, 3);
|
||||
app.is_loading = true;
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.home.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_end_no_op_when_not_ready() {
|
||||
let mut app = app_with_author_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 0);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_esc {
|
||||
use super::*;
|
||||
use crate::{assert_modal_absent, assert_navigation_popped};
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_esc_tears_down_the_author_overview_modal() {
|
||||
let mut app = app_with_author_overview();
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::AuthorDetails.into());
|
||||
assert_modal_absent!(app.data.readarr_data.author_overview_modal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_esc_tears_down_the_modal_even_when_loading() {
|
||||
let mut app = app_with_author_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.esc.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_popped!(app, ActiveReadarrBlock::AuthorDetails.into());
|
||||
assert_modal_absent!(app.data.readarr_data.author_overview_modal);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_key_char {
|
||||
use super::*;
|
||||
use crate::assert_navigation_pushed;
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_refresh_key() {
|
||||
let mut app = app_with_author_overview();
|
||||
app.is_routing = false;
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_navigation_pushed!(app, ActiveReadarrBlock::AuthorOverview.into());
|
||||
assert!(app.is_routing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_refresh_key_no_op_when_not_ready() {
|
||||
let mut app = app_with_author_overview();
|
||||
app.is_loading = true;
|
||||
app.is_routing = false;
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert!(!app.is_routing);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scrolling_the_overview_does_not_mutate_the_author_record() {
|
||||
let mut app = app_with_author_overview();
|
||||
let overview_before = app
|
||||
.data
|
||||
.readarr_data
|
||||
.authors
|
||||
.current_selection()
|
||||
.overview
|
||||
.clone();
|
||||
|
||||
AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
)
|
||||
.handle();
|
||||
|
||||
assert_eq!(overview_offset(&app), 1);
|
||||
assert_eq!(
|
||||
app.data.readarr_data.authors.current_selection().overview,
|
||||
overview_before
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_handler_accepts() {
|
||||
ActiveReadarrBlock::iter().for_each(|active_readarr_block| {
|
||||
if AUTHOR_OVERVIEW_BLOCKS.contains(&active_readarr_block) {
|
||||
assert!(
|
||||
AuthorOverviewHandler::accepts(active_readarr_block),
|
||||
"{active_readarr_block} is not accepted by the AuthorOverviewHandler"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
!AuthorOverviewHandler::accepts(active_readarr_block),
|
||||
"{active_readarr_block} is wrongly accepted by the AuthorOverviewHandler"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_handler_dispatches_no_network_events() {
|
||||
let mut app = app_with_author_overview();
|
||||
|
||||
for key in [
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
DEFAULT_KEYBINDINGS.down.key,
|
||||
DEFAULT_KEYBINDINGS.home.key,
|
||||
DEFAULT_KEYBINDINGS.end.key,
|
||||
DEFAULT_KEYBINDINGS.delete.key,
|
||||
DEFAULT_KEYBINDINGS.left.key,
|
||||
DEFAULT_KEYBINDINGS.right.key,
|
||||
DEFAULT_KEYBINDINGS.submit.key,
|
||||
DEFAULT_KEYBINDINGS.refresh.key,
|
||||
] {
|
||||
AuthorOverviewHandler::new(key, &mut app, ActiveReadarrBlock::AuthorOverview, None).handle();
|
||||
|
||||
assert_none!(app.data.readarr_data.prompt_confirm_action);
|
||||
assert!(!app.data.readarr_data.prompt_confirm);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_handler_is_not_ready_when_loading() {
|
||||
let mut app = app_with_author_overview();
|
||||
app.is_loading = true;
|
||||
|
||||
let handler = AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_handler_is_not_ready_when_the_modal_is_absent() {
|
||||
let mut app = app_with_author_overview();
|
||||
app.data.readarr_data.author_overview_modal = None;
|
||||
|
||||
let handler = AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_handler_is_ready() {
|
||||
let mut app = app_with_author_overview();
|
||||
|
||||
let handler = AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_author_overview_handler_ignores_special_keys_for_textbox_input() {
|
||||
let mut app = app_with_author_overview();
|
||||
app.ignore_special_keys_for_textbox_input = true;
|
||||
|
||||
let handler = AuthorOverviewHandler::new(
|
||||
DEFAULT_KEYBINDINGS.up.key,
|
||||
&mut app,
|
||||
ActiveReadarrBlock::AuthorOverview,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(handler.ignore_special_keys());
|
||||
}
|
||||
|
||||
fn app_with_author_overview() -> App<'static> {
|
||||
let mut app = App::test_default_fully_populated();
|
||||
app.data.readarr_data.author_overview_modal = Some(AuthorOverviewModal {
|
||||
overview: ScrollableText::with_string(
|
||||
"some interesting description of the author\r\n\
|
||||
\r\n\
|
||||
He was born in Madison, Wisconsin: a city he has never really left.\r\n\
|
||||
\r\n\
|
||||
His first novel took him seven years to finish.\r\n"
|
||||
.to_owned(),
|
||||
),
|
||||
});
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AuthorDetails.into());
|
||||
app.push_navigation_stack(ActiveReadarrBlock::AuthorOverview.into());
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
fn overview_offset(app: &App<'_>) -> u16 {
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_overview_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.overview
|
||||
.offset
|
||||
}
|
||||
|
||||
fn set_offset(app: &mut App<'_>, offset: u16) {
|
||||
app
|
||||
.data
|
||||
.readarr_data
|
||||
.author_overview_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.overview
|
||||
.offset = offset;
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ mod tests {
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::models::readarr_models::{Author, AuthorStatistics, AuthorStatus};
|
||||
use crate::models::servarr_data::readarr::readarr_data::{
|
||||
ADD_AUTHOR_BLOCKS, AUTHOR_DETAILS_BLOCKS, ActiveReadarrBlock, BOOK_DETAILS_BLOCKS,
|
||||
DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS, EDIT_AUTHOR_BLOCKS, EDITION_DETAILS_BLOCKS,
|
||||
LIBRARY_BLOCKS,
|
||||
ADD_AUTHOR_BLOCKS, AUTHOR_DETAILS_BLOCKS, AUTHOR_OVERVIEW_BLOCKS, ActiveReadarrBlock,
|
||||
BOOK_DETAILS_BLOCKS, DELETE_AUTHOR_BLOCKS, DELETE_BOOK_BLOCKS, EDIT_AUTHOR_BLOCKS,
|
||||
EDITION_DETAILS_BLOCKS, LIBRARY_BLOCKS,
|
||||
};
|
||||
use crate::test_handler_delegation;
|
||||
|
||||
@@ -931,6 +931,7 @@ mod tests {
|
||||
let mut library_handler_blocks = Vec::new();
|
||||
library_handler_blocks.extend(LIBRARY_BLOCKS);
|
||||
library_handler_blocks.extend(AUTHOR_DETAILS_BLOCKS);
|
||||
library_handler_blocks.extend(AUTHOR_OVERVIEW_BLOCKS);
|
||||
library_handler_blocks.extend(BOOK_DETAILS_BLOCKS);
|
||||
library_handler_blocks.extend(EDITION_DETAILS_BLOCKS);
|
||||
library_handler_blocks.extend(ADD_AUTHOR_BLOCKS);
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::models::Route;
|
||||
|
||||
mod add_author_handler;
|
||||
mod author_details_handler;
|
||||
mod author_overview_handler;
|
||||
mod book_details_handler;
|
||||
mod delete_author_handler;
|
||||
mod delete_book_handler;
|
||||
|
||||
Reference in new issue
Block a user