use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tokio::sync::mpsc;
use crate::state::{AppState, PackageItem, QueryInput};
use crate::theme::KeyChord;
use super::super::utils::matches_any;
use super::handle_search_key;
use super::helpers::navigate_pane;
fn new_app() -> AppState {
AppState::default()
}
#[test]
fn search_insert_typing_and_backspace() {
let mut app = new_app();
let (qtx, mut qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('r'), KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
let _ = handle_search_key(
KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert_eq!(app.input, "r");
assert!(qrx.try_recv().ok().is_some());
}
#[test]
fn search_normal_mode_selection() {
let mut app = new_app();
app.input = "rip".into();
let (qtx, _qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
let _ = handle_search_key(
KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('l'), KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert!(app.search_select_anchor.is_some());
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('h'), KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert!(app.search_caret <= crate::events::utils::char_count(&app.input));
}
#[test]
fn helpers_matches_any_shift_handling() {
use crossterm::event::KeyCode;
let ke1 = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
let chord1 = vec![KeyChord {
code: KeyCode::Char('r'),
mods: KeyModifiers::CONTROL,
}];
assert!(matches_any(&ke1, &chord1));
let ke2 = KeyEvent::new(KeyCode::Char('R'), KeyModifiers::empty());
let chord2 = vec![KeyChord {
code: KeyCode::Char('r'),
mods: KeyModifiers::SHIFT,
}];
assert!(matches_any(&ke2, &chord2));
let ke3 = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::SHIFT);
let chord3 = vec![KeyChord {
code: KeyCode::Char('r'),
mods: KeyModifiers::SHIFT,
}];
assert!(matches_any(&ke3, &chord3));
let ke4 = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty());
let chord4 = vec![KeyChord {
code: KeyCode::Char('r'),
mods: KeyModifiers::SHIFT,
}];
assert!(!matches_any(&ke4, &chord4));
}
#[test]
fn search_normal_mode_deletion() {
let mut app = new_app();
app.input = "hello world".into();
app.search_caret = 6; let (qtx, mut qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
app.search_normal_mode = true;
app.search_select_anchor = Some(0);
app.search_caret = 5;
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('d'), KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert_eq!(app.input, " world");
assert_eq!(app.search_caret, 0);
assert!(app.search_select_anchor.is_none());
assert!(qrx.try_recv().ok().is_some());
}
#[test]
fn search_normal_mode_clear() {
let mut app = new_app();
app.input = "test query".into();
app.search_caret = 5;
app.search_select_anchor = Some(3);
let (qtx, mut qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
app.search_normal_mode = true;
app.keymap.search_normal_clear = vec![KeyChord {
code: KeyCode::Delete,
mods: KeyModifiers::SHIFT,
}];
let _ = handle_search_key(
KeyEvent::new(KeyCode::Delete, KeyModifiers::SHIFT),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert_eq!(app.input, "");
assert_eq!(app.search_caret, 0);
assert!(app.search_select_anchor.is_none());
assert!(qrx.try_recv().ok().is_some());
}
#[test]
fn search_mode_toggle() {
let mut app = new_app();
app.search_normal_mode = false;
app.search_select_anchor = Some(5);
let (qtx, _qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
app.keymap.search_normal_toggle = vec![KeyChord {
code: KeyCode::Esc,
mods: KeyModifiers::empty(),
}];
let _ = handle_search_key(
KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert!(app.search_normal_mode);
let _ = handle_search_key(
KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert!(!app.search_normal_mode);
}
#[test]
fn helpers_navigate_pane_directions() {
let mut app = new_app();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
app.installed_only_mode = false;
app.install_list.push(crate::state::PackageItem {
name: "test-pkg".to_string(),
version: "1.0".to_string(),
description: String::new(),
source: crate::state::Source::Official {
repo: "extra".to_string(),
arch: "x86_64".to_string(),
},
popularity: None,
out_of_date: None,
orphaned: false,
});
navigate_pane(&mut app, "right", &dtx, &ptx);
assert!(matches!(app.focus, crate::state::Focus::Install));
app.installed_only_mode = true;
app.downgrade_list.push(crate::state::PackageItem {
name: "downgrade-pkg".to_string(),
version: "1.0".to_string(),
description: String::new(),
source: crate::state::Source::Official {
repo: "extra".to_string(),
arch: "x86_64".to_string(),
},
popularity: None,
out_of_date: None,
orphaned: false,
});
navigate_pane(&mut app, "right", &dtx, &ptx);
assert!(matches!(app.focus, crate::state::Focus::Install));
assert!(matches!(
app.right_pane_focus,
crate::state::RightPaneFocus::Downgrade
));
}
#[test]
fn search_insert_mode_space_adds_items() {
let mut app = new_app();
let test_item = crate::state::PackageItem {
name: "test-package".to_string(),
version: "1.0".to_string(),
description: String::new(),
source: crate::state::Source::Official {
repo: "extra".to_string(),
arch: "x86_64".to_string(),
},
popularity: None,
out_of_date: None,
orphaned: false,
};
app.results.push(test_item.clone());
app.selected = 0;
let (qtx, _qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, mut arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
app.installed_only_mode = false;
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
let received = arx.try_recv().ok();
assert!(received.is_some());
assert_eq!(
received
.expect("received should be Some after is_some() check")
.name,
"test-package"
);
app.installed_only_mode = true;
app.results.push(test_item);
app.selected = 0;
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert!(app.remove_list.iter().any(|p| p.name == "test-package"));
}
#[test]
fn search_normal_mode_navigation() {
let mut app = new_app();
app.results = vec![
crate::state::PackageItem {
name: "pkg1".to_string(),
version: "1.0".to_string(),
description: String::new(),
source: crate::state::Source::Official {
repo: "extra".to_string(),
arch: "x86_64".to_string(),
},
popularity: None,
out_of_date: None,
orphaned: false,
},
crate::state::PackageItem {
name: "pkg2".to_string(),
version: "1.0".to_string(),
description: String::new(),
source: crate::state::Source::Official {
repo: "extra".to_string(),
arch: "x86_64".to_string(),
},
popularity: None,
out_of_date: None,
orphaned: false,
},
crate::state::PackageItem {
name: "pkg3".to_string(),
version: "1.0".to_string(),
description: String::new(),
source: crate::state::Source::Official {
repo: "extra".to_string(),
arch: "x86_64".to_string(),
},
popularity: None,
out_of_date: None,
orphaned: false,
},
];
app.selected = 1;
app.search_normal_mode = true;
let (qtx, _qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('j'), KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert_eq!(app.selected, 2);
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('k'), KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert_eq!(app.selected, 1);
app.selected = 0;
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('k'), KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert_eq!(app.selected, 0);
}
#[test]
fn search_normal_mode_vote_rejects_non_aur_package() {
let mut app = new_app();
app.search_normal_mode = true;
app.results = vec![crate::state::PackageItem {
name: "ripgrep".to_string(),
version: "14.1.0".to_string(),
description: String::new(),
source: crate::state::Source::Official {
repo: "extra".to_string(),
arch: "x86_64".to_string(),
},
popularity: None,
out_of_date: None,
orphaned: false,
}];
let (qtx, _qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert!(app.pending_aur_vote_intent.is_none());
assert!(app.pending_aur_vote_request.is_none());
let toast = app
.toast_message
.as_ref()
.expect("non-AUR vote should produce toast");
assert!(toast.contains("only available for AUR packages"));
}
#[test]
fn search_normal_mode_vote_opens_confirmation_when_enabled() {
let mut app = new_app();
app.search_normal_mode = true;
let pkgbase = "pkg-vote-default-normal";
app.results = vec![crate::state::PackageItem {
name: pkgbase.to_string(),
version: "1.0.0".to_string(),
description: String::new(),
source: crate::state::Source::Aur,
popularity: None,
out_of_date: None,
orphaned: false,
}];
let (qtx, _qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert_eq!(
app.pending_aur_vote_intent,
Some((pkgbase.to_string(), crate::sources::VoteAction::Vote))
);
assert!(app.pending_aur_vote_request.is_none());
match app.modal {
crate::state::Modal::ConfirmAurVote {
pkgbase, action, ..
} => {
assert_eq!(pkgbase, "pkg-vote-default-normal");
assert_eq!(action, crate::sources::VoteAction::Vote);
}
other => panic!("expected confirm modal, got {other:?}"),
}
}
#[test]
fn search_insert_mode_vote_opens_confirmation_when_enabled() {
let mut app = new_app();
app.search_normal_mode = false;
let pkgbase = "pkg-vote-default-insert";
app.results = vec![crate::state::PackageItem {
name: pkgbase.to_string(),
version: "1.0.0".to_string(),
description: String::new(),
source: crate::state::Source::Aur,
popularity: None,
out_of_date: None,
orphaned: false,
}];
let (qtx, _qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert_eq!(
app.pending_aur_vote_intent,
Some((pkgbase.to_string(), crate::sources::VoteAction::Vote))
);
assert!(app.pending_aur_vote_request.is_none());
match app.modal {
crate::state::Modal::ConfirmAurVote {
pkgbase, action, ..
} => {
assert_eq!(pkgbase, "pkg-vote-default-insert");
assert_eq!(action, crate::sources::VoteAction::Vote);
}
other => panic!("expected confirm modal, got {other:?}"),
}
}
#[test]
fn search_normal_mode_vote_toggles_to_unvote_when_cached_voted() {
let mut app = new_app();
app.search_normal_mode = true;
app.results = vec![crate::state::PackageItem {
name: "pacsea-bin".to_string(),
version: "1.0.0".to_string(),
description: String::new(),
source: crate::state::Source::Aur,
popularity: None,
out_of_date: None,
orphaned: false,
}];
app.aur_vote_state_by_pkgbase.insert(
"pacsea-bin".to_string(),
crate::state::app_state::AurVoteStateUi::Voted,
);
let (qtx, _qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
let _ = handle_search_key(
KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert_eq!(
app.pending_aur_vote_intent,
Some(("pacsea-bin".to_string(), crate::sources::VoteAction::Unvote))
);
}
#[test]
fn search_selection_vote_state_check_queues_when_feature_enabled() {
let mut app = new_app();
app.results = vec![
crate::state::PackageItem {
name: "pkg-a".to_string(),
version: "1.0.0".to_string(),
description: String::new(),
source: crate::state::Source::Aur,
popularity: None,
out_of_date: None,
orphaned: false,
},
crate::state::PackageItem {
name: "pkg-b".to_string(),
version: "1.0.0".to_string(),
description: String::new(),
source: crate::state::Source::Aur,
popularity: None,
out_of_date: None,
orphaned: false,
},
];
app.selected = 0;
let (qtx, _qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
let _ = handle_search_key(
KeyEvent::new(KeyCode::Down, KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert_eq!(app.selected, 1);
assert_eq!(
app.pending_aur_vote_state_request,
Some("pkg-b".to_string())
);
assert!(matches!(
app.aur_vote_state_by_pkgbase.get("pkg-b"),
Some(crate::state::app_state::AurVoteStateUi::Loading)
));
}
#[test]
fn search_selection_does_not_queue_vote_state_check_for_official_package() {
let mut app = new_app();
app.results = vec![
crate::state::PackageItem {
name: "pkg-a".to_string(),
version: "1.0.0".to_string(),
description: String::new(),
source: crate::state::Source::Official {
repo: "core".to_string(),
arch: "x86_64".to_string(),
},
popularity: None,
out_of_date: None,
orphaned: false,
},
crate::state::PackageItem {
name: "pkg-b".to_string(),
version: "1.0.0".to_string(),
description: String::new(),
source: crate::state::Source::Official {
repo: "extra".to_string(),
arch: "x86_64".to_string(),
},
popularity: None,
out_of_date: None,
orphaned: false,
},
];
app.selected = 0;
let (qtx, _qrx) = mpsc::unbounded_channel::<QueryInput>();
let (dtx, _drx) = mpsc::unbounded_channel::<PackageItem>();
let (atx, _arx) = mpsc::unbounded_channel::<PackageItem>();
let (ptx, _prx) = mpsc::unbounded_channel::<PackageItem>();
let (comments_tx, _comments_rx) = mpsc::unbounded_channel::<String>();
let _ = handle_search_key(
KeyEvent::new(KeyCode::Down, KeyModifiers::empty()),
&mut app,
&qtx,
&dtx,
&atx,
&ptx,
&comments_tx,
);
assert_eq!(app.selected, 1);
assert!(app.pending_aur_vote_state_request.is_none());
}