A page is 20 rows and the index wrapped modulo one less than the row count, so the last row was unreachable by paging, 21-row and 5-row tables did nothing on page down from the first row, and 4-row tables cycled 0, 2, 1 and never reached row 3. Page down now clamps to the last row and page up clamps to the first. With no division left, a single-row table is safe, so the early return only guards the empty table again, matching scroll_to_top and scroll_to_bottom.
1427 lines
37 KiB
Rust
1427 lines
37 KiB
Rust
#[cfg(test)]
|
|
mod tests {
|
|
use crate::app::App;
|
|
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
|
use crate::assert_modal_absent;
|
|
use crate::assert_modal_present;
|
|
use crate::assert_navigation_pushed;
|
|
use crate::event::Key;
|
|
use crate::handlers::KeyEventHandler;
|
|
use crate::handlers::table_handler::TableHandlingConfig;
|
|
use crate::handlers::table_handler::handle_table;
|
|
use crate::models::Route;
|
|
use crate::models::radarr_models::Movie;
|
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
|
use crate::models::servarr_models::Language;
|
|
use crate::models::stateful_table::SortOption;
|
|
use rstest::rstest;
|
|
|
|
struct TableHandlerUnit<'a, 'b> {
|
|
key: Key,
|
|
app: &'a mut App<'b>,
|
|
active_radarr_block: ActiveRadarrBlock,
|
|
_context: Option<ActiveRadarrBlock>,
|
|
}
|
|
|
|
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for TableHandlerUnit<'a, 'b> {
|
|
fn handle(&mut self) {
|
|
let movie_table_handling_config = TableHandlingConfig::new(ActiveRadarrBlock::Movies.into())
|
|
.sorting_block(ActiveRadarrBlock::MoviesSortPrompt.into())
|
|
.sort_options(sort_options())
|
|
.searching_block(ActiveRadarrBlock::SearchMovie.into())
|
|
.search_error_block(ActiveRadarrBlock::SearchMovieError.into())
|
|
.search_field_fn(|movie| &movie.title.text)
|
|
.filtering_block(ActiveRadarrBlock::FilterMovies.into())
|
|
.filter_error_block(ActiveRadarrBlock::FilterMoviesError.into())
|
|
.filter_field_fn(|movie| &movie.title.text);
|
|
let minimal_movie_table_handling_config =
|
|
TableHandlingConfig::new(ActiveRadarrBlock::Movies.into());
|
|
|
|
let config = match self.active_radarr_block {
|
|
ActiveRadarrBlock::MovieDetails => minimal_movie_table_handling_config,
|
|
_ => movie_table_handling_config,
|
|
};
|
|
|
|
if !handle_table(self, |app| &mut app.data.radarr_data.movies, config) {
|
|
self.handle_key_event();
|
|
}
|
|
}
|
|
|
|
fn accepts(_: ActiveRadarrBlock) -> bool {
|
|
true
|
|
}
|
|
|
|
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: ActiveRadarrBlock,
|
|
_context: Option<ActiveRadarrBlock>,
|
|
) -> Self {
|
|
Self {
|
|
key,
|
|
app,
|
|
active_radarr_block: active_block,
|
|
_context,
|
|
}
|
|
}
|
|
|
|
fn get_key(&self) -> Key {
|
|
self.key
|
|
}
|
|
|
|
fn is_ready(&self) -> bool {
|
|
!self.app.is_loading
|
|
}
|
|
|
|
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) {}
|
|
|
|
fn handle_left_right_action(&mut self) {}
|
|
|
|
fn handle_submit(&mut self) {}
|
|
|
|
fn handle_esc(&mut self) {}
|
|
|
|
fn handle_char_key_event(&mut self) {}
|
|
|
|
fn app_mut(&mut self) -> &mut App<'b> {
|
|
self.app
|
|
}
|
|
|
|
fn current_route(&self) -> Route {
|
|
self.app.get_current_route()
|
|
}
|
|
}
|
|
|
|
mod test_handle_scroll_up_and_down {
|
|
use super::*;
|
|
use crate::models::HorizontallyScrollableText;
|
|
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
|
|
use pretty_assertions::assert_str_eq;
|
|
|
|
test_iterable_scroll!(
|
|
test_table_scroll,
|
|
TableHandlerUnit,
|
|
radarr_data,
|
|
movies,
|
|
simple_stateful_iterable_vec!(Movie, HorizontallyScrollableText),
|
|
ActiveRadarrBlock::Movies,
|
|
None,
|
|
title,
|
|
to_string
|
|
);
|
|
|
|
#[rstest]
|
|
fn test_table_scroll_no_op_when_not_ready(
|
|
#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key,
|
|
) {
|
|
let mut app = App::test_default();
|
|
app.is_loading = true;
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(simple_stateful_iterable_vec!(
|
|
Movie,
|
|
HorizontallyScrollableText
|
|
));
|
|
|
|
TableHandlerUnit::new(key, &mut app, ActiveRadarrBlock::Movies, None).handle();
|
|
|
|
assert_str_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.current_selection()
|
|
.title
|
|
.to_string(),
|
|
"Test 1"
|
|
);
|
|
|
|
TableHandlerUnit::new(key, &mut app, ActiveRadarrBlock::Movies, None).handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.current_selection()
|
|
.title
|
|
.to_string(),
|
|
"Test 1"
|
|
);
|
|
}
|
|
|
|
#[rstest]
|
|
fn test_table_sort_scroll(
|
|
#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key,
|
|
) {
|
|
let movie_field_vec = sort_options();
|
|
let mut app = App::test_default();
|
|
app.data.radarr_data.movies.sorting(sort_options());
|
|
|
|
if key == Key::Up {
|
|
for i in (0..movie_field_vec.len()).rev() {
|
|
TableHandlerUnit::new(key, &mut app, ActiveRadarrBlock::MoviesSortPrompt, None).handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.sort
|
|
.as_ref()
|
|
.unwrap()
|
|
.current_selection(),
|
|
&movie_field_vec[i]
|
|
);
|
|
}
|
|
} else {
|
|
for i in 0..movie_field_vec.len() {
|
|
TableHandlerUnit::new(key, &mut app, ActiveRadarrBlock::MoviesSortPrompt, None).handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.sort
|
|
.as_ref()
|
|
.unwrap()
|
|
.current_selection(),
|
|
&movie_field_vec[(i + 1) % movie_field_vec.len()]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_table_sort_scroll_up_with_sort_some() {
|
|
let movie_field_vec = sort_options();
|
|
let mut app = App::test_default();
|
|
app.data.radarr_data.movies.sorting(sort_options());
|
|
app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into());
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.sort
|
|
.as_ref()
|
|
.unwrap()
|
|
.current_selection(),
|
|
&movie_field_vec[0]
|
|
);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.up.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::MoviesSortPrompt,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.sort
|
|
.as_ref()
|
|
.unwrap()
|
|
.current_selection(),
|
|
&movie_field_vec[movie_field_vec.len() - 1]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_table_sort_scroll_down_with_sort_some() {
|
|
let movie_field_vec = sort_options();
|
|
let mut app = App::test_default();
|
|
app.data.radarr_data.movies.sorting(sort_options());
|
|
app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into());
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.sort
|
|
.as_ref()
|
|
.unwrap()
|
|
.current_selection(),
|
|
&movie_field_vec[0]
|
|
);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.down.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::MoviesSortPrompt,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.sort
|
|
.as_ref()
|
|
.unwrap()
|
|
.current_selection(),
|
|
&movie_field_vec[0]
|
|
);
|
|
}
|
|
}
|
|
|
|
mod test_handle_home_end {
|
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
|
use std::sync::atomic::Ordering::SeqCst;
|
|
|
|
use super::*;
|
|
use crate::models::HorizontallyScrollableText;
|
|
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
|
|
|
|
test_iterable_home_and_end!(
|
|
test_table_home_end,
|
|
TableHandlerUnit,
|
|
radarr_data,
|
|
movies,
|
|
extended_stateful_iterable_vec!(Movie, HorizontallyScrollableText),
|
|
ActiveRadarrBlock::Movies,
|
|
None,
|
|
title,
|
|
to_string
|
|
);
|
|
|
|
#[test]
|
|
fn test_table_home_end_no_op_when_not_ready() {
|
|
let mut app = App::test_default();
|
|
app.is_loading = true;
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(extended_stateful_iterable_vec!(
|
|
Movie,
|
|
HorizontallyScrollableText
|
|
));
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.end.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::Movies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_str_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.current_selection()
|
|
.title
|
|
.to_string(),
|
|
"Test 1"
|
|
);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.home.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::Movies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_str_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.current_selection()
|
|
.title
|
|
.to_string(),
|
|
"Test 1"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_movie_search_box_home_end_keys() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
app.data.radarr_data.movies.search = Some("Test".into());
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.home.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::SearchMovie,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.search
|
|
.as_ref()
|
|
.unwrap()
|
|
.offset
|
|
.load(SeqCst),
|
|
4
|
|
);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.end.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::SearchMovie,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.search
|
|
.as_ref()
|
|
.unwrap()
|
|
.offset
|
|
.load(SeqCst),
|
|
0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_movie_filter_box_home_end_keys() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
app.data.radarr_data.movies.filter = Some("Test".into());
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.home.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::FilterMovies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.filter
|
|
.as_ref()
|
|
.unwrap()
|
|
.offset
|
|
.load(SeqCst),
|
|
4
|
|
);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.end.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::FilterMovies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.filter
|
|
.as_ref()
|
|
.unwrap()
|
|
.offset
|
|
.load(SeqCst),
|
|
0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_table_sort_home_end() {
|
|
let movie_field_vec = sort_options();
|
|
let mut app = App::test_default();
|
|
app.data.radarr_data.movies.sorting(sort_options());
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.end.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::MoviesSortPrompt,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.sort
|
|
.as_ref()
|
|
.unwrap()
|
|
.current_selection(),
|
|
&movie_field_vec[movie_field_vec.len() - 1]
|
|
);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.home.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::MoviesSortPrompt,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.sort
|
|
.as_ref()
|
|
.unwrap()
|
|
.current_selection(),
|
|
&movie_field_vec[0]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_table_sort_home_with_sort_some() {
|
|
let movie_field_vec = sort_options();
|
|
let mut app = App::test_default();
|
|
app.data.radarr_data.movies.sorting(sort_options());
|
|
app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into());
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.down.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::MoviesSortPrompt,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.home.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::MoviesSortPrompt,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.sort
|
|
.as_ref()
|
|
.unwrap()
|
|
.current_selection(),
|
|
&movie_field_vec[0]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_table_sort_end_with_sort_some() {
|
|
let movie_field_vec = sort_options();
|
|
let mut app = App::test_default();
|
|
app.data.radarr_data.movies.sorting(sort_options());
|
|
app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into());
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.end.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::MoviesSortPrompt,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.sort
|
|
.as_ref()
|
|
.unwrap()
|
|
.current_selection(),
|
|
&movie_field_vec[movie_field_vec.len() - 1]
|
|
);
|
|
}
|
|
}
|
|
|
|
mod test_handle_pagination_scroll {
|
|
use super::*;
|
|
use crate::handlers::table_handler::table_handler_tests::tests::TableHandlerUnit;
|
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
|
use pretty_assertions::assert_str_eq;
|
|
use rstest::rstest;
|
|
use std::iter;
|
|
|
|
#[rstest]
|
|
fn test_table_pagination_scroll(
|
|
#[values(DEFAULT_KEYBINDINGS.pg_up.key, DEFAULT_KEYBINDINGS.pg_down.key)] key: Key,
|
|
) {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
let mut curr = 0;
|
|
let movies_vec = iter::repeat_with(|| {
|
|
let tmp = curr;
|
|
curr += 1;
|
|
Movie {
|
|
title: format!("Test {tmp}").into(),
|
|
..Movie::default()
|
|
}
|
|
})
|
|
.take(100)
|
|
.collect();
|
|
app.data.radarr_data.movies.set_items(movies_vec);
|
|
app.data.radarr_data.movies.state.select(Some(50));
|
|
TableHandlerUnit::new(key, &mut app, ActiveRadarrBlock::Movies, None).handle();
|
|
|
|
if key == Key::PgUp {
|
|
assert_str_eq!(
|
|
app.data.radarr_data.movies.current_selection().title.text,
|
|
"Test 30"
|
|
);
|
|
} else {
|
|
assert_str_eq!(
|
|
app.data.radarr_data.movies.current_selection().title.text,
|
|
"Test 70"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
mod test_handle_left_right_action {
|
|
use pretty_assertions::assert_eq;
|
|
use std::sync::atomic::Ordering::SeqCst;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_movie_search_box_left_right_keys() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
app.data.radarr_data.movies.search = Some("Test".into());
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.left.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::SearchMovie,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.search
|
|
.as_ref()
|
|
.unwrap()
|
|
.offset
|
|
.load(SeqCst),
|
|
1
|
|
);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.right.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::SearchMovie,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.search
|
|
.as_ref()
|
|
.unwrap()
|
|
.offset
|
|
.load(SeqCst),
|
|
0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_movie_filter_box_left_right_keys() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
app.data.radarr_data.movies.filter = Some("Test".into());
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.left.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::FilterMovies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.filter
|
|
.as_ref()
|
|
.unwrap()
|
|
.offset
|
|
.load(SeqCst),
|
|
1
|
|
);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.right.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::FilterMovies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.filter
|
|
.as_ref()
|
|
.unwrap()
|
|
.offset
|
|
.load(SeqCst),
|
|
0
|
|
);
|
|
}
|
|
}
|
|
|
|
mod test_handle_submit {
|
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
|
|
|
use crate::models::HorizontallyScrollableText;
|
|
use crate::{assert_navigation_popped, extended_stateful_iterable_vec};
|
|
|
|
use super::*;
|
|
|
|
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
|
|
|
|
#[test]
|
|
fn test_search_movie_submit() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(extended_stateful_iterable_vec!(
|
|
Movie,
|
|
HorizontallyScrollableText
|
|
));
|
|
app.data.radarr_data.movies.search = Some("Test 2".into());
|
|
|
|
TableHandlerUnit::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::SearchMovie, None).handle();
|
|
|
|
assert_str_eq!(
|
|
app.data.radarr_data.movies.current_selection().title.text,
|
|
"Test 2"
|
|
);
|
|
assert_navigation_popped!(app, ActiveRadarrBlock::Movies.into());
|
|
}
|
|
|
|
#[test]
|
|
fn test_search_movie_submit_error_on_no_search_hits() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(extended_stateful_iterable_vec!(
|
|
Movie,
|
|
HorizontallyScrollableText
|
|
));
|
|
app.data.radarr_data.movies.search = Some("Test 5".into());
|
|
|
|
TableHandlerUnit::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::SearchMovie, None).handle();
|
|
|
|
assert_str_eq!(
|
|
app.data.radarr_data.movies.current_selection().title.text,
|
|
"Test 1"
|
|
);
|
|
assert_navigation_pushed!(app, ActiveRadarrBlock::SearchMovieError.into());
|
|
}
|
|
|
|
#[test]
|
|
fn test_search_filtered_table_submit() {
|
|
let mut app = App::test_default();
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_filtered_items(extended_stateful_iterable_vec!(
|
|
Movie,
|
|
HorizontallyScrollableText
|
|
));
|
|
app.data.radarr_data.movies.search = Some("Test 2".into());
|
|
|
|
TableHandlerUnit::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::SearchMovie, None).handle();
|
|
|
|
assert_str_eq!(
|
|
app.data.radarr_data.movies.current_selection().title.text,
|
|
"Test 2"
|
|
);
|
|
assert_navigation_popped!(app, ActiveRadarrBlock::Movies.into());
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_table_submit() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(extended_stateful_iterable_vec!(
|
|
Movie,
|
|
HorizontallyScrollableText
|
|
));
|
|
app.data.radarr_data.movies.filter = Some("Test".into());
|
|
|
|
TableHandlerUnit::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::FilterMovies, None).handle();
|
|
|
|
assert_modal_present!(app.data.radarr_data.movies.filtered_items);
|
|
assert!(!app.ignore_special_keys_for_textbox_input);
|
|
assert_eq!(
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.filtered_items
|
|
.as_ref()
|
|
.unwrap()
|
|
.len(),
|
|
3
|
|
);
|
|
assert_str_eq!(
|
|
app.data.radarr_data.movies.current_selection().title.text,
|
|
"Test 1"
|
|
);
|
|
assert_navigation_popped!(app, ActiveRadarrBlock::Movies.into());
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_table_submit_error_on_no_filter_matches() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(extended_stateful_iterable_vec!(
|
|
Movie,
|
|
HorizontallyScrollableText
|
|
));
|
|
app.data.radarr_data.movies.filter = Some("Test 5".into());
|
|
|
|
TableHandlerUnit::new(SUBMIT_KEY, &mut app, ActiveRadarrBlock::FilterMovies, None).handle();
|
|
|
|
assert!(!app.ignore_special_keys_for_textbox_input);
|
|
assert_modal_absent!(app.data.radarr_data.movies.filtered_items);
|
|
assert_navigation_pushed!(app, ActiveRadarrBlock::FilterMoviesError.into());
|
|
}
|
|
|
|
#[test]
|
|
fn test_table_sort_prompt_submit() {
|
|
let mut app = App::test_default();
|
|
app.data.radarr_data.movies.sort_asc = true;
|
|
app.data.radarr_data.movies.sorting(sort_options());
|
|
app.data.radarr_data.movies.set_items(movies_vec());
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into());
|
|
|
|
let mut expected_vec = movies_vec();
|
|
expected_vec.sort_by_key(|a| a.id);
|
|
expected_vec.reverse();
|
|
|
|
TableHandlerUnit::new(
|
|
SUBMIT_KEY,
|
|
&mut app,
|
|
ActiveRadarrBlock::MoviesSortPrompt,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_navigation_popped!(app, ActiveRadarrBlock::Movies.into());
|
|
assert_eq!(app.data.radarr_data.movies.items, expected_vec);
|
|
}
|
|
}
|
|
|
|
mod test_handle_esc {
|
|
use pretty_assertions::assert_eq;
|
|
use ratatui::widgets::TableState;
|
|
|
|
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
|
|
use crate::models::stateful_table::StatefulTable;
|
|
|
|
use super::*;
|
|
use crate::assert_navigation_popped;
|
|
|
|
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
|
|
|
|
#[rstest]
|
|
fn test_search_movie_block_esc(
|
|
#[values(ActiveRadarrBlock::SearchMovie, ActiveRadarrBlock::SearchMovieError)]
|
|
active_radarr_block: ActiveRadarrBlock,
|
|
) {
|
|
let mut app = App::test_default();
|
|
app.ignore_special_keys_for_textbox_input = true;
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app.push_navigation_stack(active_radarr_block.into());
|
|
app.data.radarr_data = create_test_radarr_data();
|
|
app.data.radarr_data.movies.search = Some("Test".into());
|
|
|
|
TableHandlerUnit::new(ESC_KEY, &mut app, active_radarr_block, None).handle();
|
|
|
|
assert_navigation_popped!(app, ActiveRadarrBlock::Movies.into());
|
|
assert!(!app.ignore_special_keys_for_textbox_input);
|
|
assert_none!(app.data.radarr_data.movies.search);
|
|
}
|
|
|
|
#[rstest]
|
|
fn test_filter_table_block_esc(
|
|
#[values(ActiveRadarrBlock::FilterMovies, ActiveRadarrBlock::FilterMoviesError)]
|
|
active_radarr_block: ActiveRadarrBlock,
|
|
) {
|
|
let mut app = App::test_default();
|
|
app.ignore_special_keys_for_textbox_input = true;
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app.push_navigation_stack(active_radarr_block.into());
|
|
app.data.radarr_data = create_test_radarr_data();
|
|
app.data.radarr_data.movies = StatefulTable {
|
|
filter: Some("Test".into()),
|
|
filtered_items: Some(Vec::new()),
|
|
filtered_state: Some(TableState::default()),
|
|
..StatefulTable::default()
|
|
};
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(ESC_KEY, &mut app, active_radarr_block, None).handle();
|
|
|
|
assert_navigation_popped!(app, ActiveRadarrBlock::Movies.into());
|
|
assert!(!app.ignore_special_keys_for_textbox_input);
|
|
assert_none!(app.data.radarr_data.movies.filter);
|
|
assert_none!(app.data.radarr_data.movies.filtered_items);
|
|
assert_none!(app.data.radarr_data.movies.filtered_state);
|
|
}
|
|
|
|
#[test]
|
|
fn test_table_sort_prompt_block_esc() {
|
|
let mut app = App::test_default();
|
|
app.data.radarr_data.movies.set_items(movies_vec());
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into());
|
|
|
|
TableHandlerUnit::new(ESC_KEY, &mut app, ActiveRadarrBlock::MoviesSortPrompt, None).handle();
|
|
|
|
assert_navigation_popped!(app, ActiveRadarrBlock::Movies.into());
|
|
}
|
|
|
|
#[test]
|
|
fn test_table_block_esc_with_filter_applied() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app.data.radarr_data.movies = StatefulTable {
|
|
filter: Some("Test".into()),
|
|
filtered_items: Some(Vec::new()),
|
|
filtered_state: Some(TableState::default()),
|
|
..StatefulTable::default()
|
|
};
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(ESC_KEY, &mut app, ActiveRadarrBlock::Movies, None).handle();
|
|
|
|
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
|
|
assert_none!(app.data.radarr_data.movies.filter);
|
|
assert_none!(app.data.radarr_data.movies.filtered_items);
|
|
assert_none!(app.data.radarr_data.movies.filtered_state);
|
|
}
|
|
}
|
|
|
|
mod test_handle_key_char {
|
|
use crate::models::HorizontallyScrollableText;
|
|
use crate::models::servarr_data::radarr::radarr_data::radarr_test_utils::utils::create_test_radarr_data;
|
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_search_table_key() {
|
|
let mut app = App::test_default();
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.search.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::Movies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_navigation_pushed!(app, ActiveRadarrBlock::SearchMovie.into());
|
|
assert!(app.ignore_special_keys_for_textbox_input);
|
|
assert_some_eq_x!(
|
|
&app.data.radarr_data.movies.search,
|
|
&HorizontallyScrollableText::default()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_search_table_key_no_op_when_not_ready() {
|
|
let mut app = App::test_default();
|
|
app.is_loading = true;
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.search.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::Movies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
|
|
assert!(!app.ignore_special_keys_for_textbox_input);
|
|
assert_none!(app.data.radarr_data.movies.search);
|
|
}
|
|
|
|
#[test]
|
|
fn test_search_table_key_no_op_when_search_table_block_is_not_defined() {
|
|
let mut app = App::test_default();
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.search.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::MovieDetails,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
|
|
assert!(!app.ignore_special_keys_for_textbox_input);
|
|
assert_none!(app.data.radarr_data.movies.search);
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_table_key() {
|
|
let mut app = App::test_default();
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.filter.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::Movies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_navigation_pushed!(app, ActiveRadarrBlock::FilterMovies.into());
|
|
assert!(app.ignore_special_keys_for_textbox_input);
|
|
assert_modal_present!(app.data.radarr_data.movies.filter);
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_table_key_no_op_when_not_ready() {
|
|
let mut app = App::test_default();
|
|
app.is_loading = true;
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.filter.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::Movies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
|
|
assert!(!app.ignore_special_keys_for_textbox_input);
|
|
assert_modal_absent!(app.data.radarr_data.movies.filter);
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_table_key_resets_previous_filter() {
|
|
let mut app = App::test_default();
|
|
app.ignore_special_keys_for_textbox_input = true;
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app.data.radarr_data = create_test_radarr_data();
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
app.data.radarr_data.movies.filter = Some("Test".into());
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.filter.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::Movies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_navigation_pushed!(app, ActiveRadarrBlock::FilterMovies.into());
|
|
assert!(app.ignore_special_keys_for_textbox_input);
|
|
assert_some_eq_x!(
|
|
&app.data.radarr_data.movies.filter,
|
|
&HorizontallyScrollableText::default()
|
|
);
|
|
assert_modal_absent!(app.data.radarr_data.movies.filtered_items);
|
|
assert_modal_absent!(app.data.radarr_data.movies.filtered_state);
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_table_key_no_op_when_filter_table_block_is_not_defined() {
|
|
let mut app = App::test_default();
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.filter.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::MovieDetails,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
|
|
assert!(!app.ignore_special_keys_for_textbox_input);
|
|
assert_none!(app.data.radarr_data.movies.filter);
|
|
}
|
|
|
|
#[test]
|
|
fn test_search_table_box_backspace_key() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into());
|
|
app.data.radarr_data.movies.search = Some("Test".into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.backspace.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::SearchMovie,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_str_eq!(
|
|
app.data.radarr_data.movies.search.as_ref().unwrap().text,
|
|
"Tes"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_table_box_backspace_key() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
app.data.radarr_data.movies.filter = Some("Test".into());
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.backspace.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::FilterMovies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_str_eq!(
|
|
app.data.radarr_data.movies.filter.as_ref().unwrap().text,
|
|
"Tes"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_search_table_box_char_key() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
app.data.radarr_data.movies.search = Some(HorizontallyScrollableText::default());
|
|
|
|
TableHandlerUnit::new(
|
|
Key::Char('a'),
|
|
&mut app,
|
|
ActiveRadarrBlock::SearchMovie,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_str_eq!(
|
|
app.data.radarr_data.movies.search.as_ref().unwrap().text,
|
|
"a"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_table_box_char_key() {
|
|
let mut app = App::test_default();
|
|
app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
app.data.radarr_data.movies.filter = Some(HorizontallyScrollableText::default());
|
|
|
|
TableHandlerUnit::new(
|
|
Key::Char('a'),
|
|
&mut app,
|
|
ActiveRadarrBlock::FilterMovies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_str_eq!(
|
|
app.data.radarr_data.movies.filter.as_ref().unwrap().text,
|
|
"a"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sort_key() {
|
|
let mut app = App::test_default();
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.sort.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::Movies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_navigation_pushed!(app, ActiveRadarrBlock::MoviesSortPrompt.into());
|
|
assert_eq!(
|
|
app.data.radarr_data.movies.sort.as_ref().unwrap().items,
|
|
sort_options()
|
|
);
|
|
assert!(!app.data.radarr_data.movies.sort_asc);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sort_key_no_op_when_not_ready() {
|
|
let mut app = App::test_default();
|
|
app.is_loading = true;
|
|
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.sort.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::Movies,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
|
|
assert_modal_absent!(app.data.radarr_data.movies.sort);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sort_key_no_op_when_sort_table_block_is_undefined() {
|
|
let mut app = App::test_default();
|
|
app
|
|
.data
|
|
.radarr_data
|
|
.movies
|
|
.set_items(vec![Movie::default()]);
|
|
|
|
TableHandlerUnit::new(
|
|
DEFAULT_KEYBINDINGS.sort.key,
|
|
&mut app,
|
|
ActiveRadarrBlock::MovieDetails,
|
|
None,
|
|
)
|
|
.handle();
|
|
|
|
assert_eq!(app.get_current_route(), ActiveRadarrBlock::Movies.into());
|
|
assert_modal_absent!(app.data.radarr_data.movies.sort);
|
|
}
|
|
}
|
|
|
|
fn movies_vec() -> Vec<Movie> {
|
|
vec![
|
|
Movie {
|
|
id: 3,
|
|
title: "test 1".into(),
|
|
original_language: Language {
|
|
id: 1,
|
|
name: "English".to_owned(),
|
|
},
|
|
size_on_disk: 1024,
|
|
studio: Some("Studio 1".to_owned()),
|
|
year: 2024,
|
|
monitored: false,
|
|
runtime: 12.into(),
|
|
quality_profile_id: 1,
|
|
certification: Some("PG-13".to_owned()),
|
|
tags: vec![1.into(), 2.into()],
|
|
..Movie::default()
|
|
},
|
|
Movie {
|
|
id: 2,
|
|
title: "test 2".into(),
|
|
original_language: Language {
|
|
id: 2,
|
|
name: "Chinese".to_owned(),
|
|
},
|
|
size_on_disk: 2048,
|
|
studio: Some("Studio 2".to_owned()),
|
|
year: 1998,
|
|
monitored: false,
|
|
runtime: 60.into(),
|
|
quality_profile_id: 2,
|
|
certification: Some("R".to_owned()),
|
|
tags: vec![1.into(), 3.into()],
|
|
..Movie::default()
|
|
},
|
|
Movie {
|
|
id: 1,
|
|
title: "test 3".into(),
|
|
original_language: Language {
|
|
id: 3,
|
|
name: "Japanese".to_owned(),
|
|
},
|
|
size_on_disk: 512,
|
|
studio: Some("studio 3".to_owned()),
|
|
year: 1954,
|
|
monitored: true,
|
|
runtime: 120.into(),
|
|
quality_profile_id: 3,
|
|
certification: Some("G".to_owned()),
|
|
tags: vec![2.into(), 3.into()],
|
|
..Movie::default()
|
|
},
|
|
]
|
|
}
|
|
|
|
fn sort_options() -> Vec<SortOption<Movie>> {
|
|
vec![SortOption {
|
|
name: "Test 1",
|
|
cmp_fn: Some(|a, b| {
|
|
b.title
|
|
.text
|
|
.to_lowercase()
|
|
.cmp(&a.title.text.to_lowercase())
|
|
}),
|
|
}]
|
|
}
|
|
}
|