#[cfg(test)]
use super::display::{
build_file_display_items, compute_display_items_len, compute_file_display_items_len,
};
#[cfg(test)]
use super::keys::handle_preflight_key;
#[cfg(test)]
use crate::state::modal::{
CascadeMode, DependencyInfo, DependencySource, DependencyStatus, FileChange, FileChangeType,
PackageFileInfo, ServiceImpact, ServiceRestartDecision,
};
#[cfg(test)]
use crate::state::{AppState, Modal, PackageItem, PreflightAction, PreflightTab, Source};
#[cfg(test)]
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[cfg(test)]
use std::collections::HashSet;
fn pkg(name: &str) -> PackageItem {
PackageItem {
name: name.into(),
version: "1.0.0".into(),
description: "pkg".into(),
source: Source::Official {
repo: "extra".into(),
arch: "x86_64".into(),
},
popularity: None,
out_of_date: None,
orphaned: false,
}
}
fn dep(name: &str, required_by: &[&str]) -> DependencyInfo {
DependencyInfo {
name: name.into(),
version: ">=1".into(),
status: DependencyStatus::ToInstall,
source: DependencySource::Official {
repo: "extra".into(),
},
required_by: required_by.iter().map(|s| (*s).into()).collect(),
depends_on: Vec::new(),
is_core: false,
is_system: false,
}
}
fn dep_with_status(name: &str, required_by: &[&str], status: DependencyStatus) -> DependencyInfo {
DependencyInfo {
name: name.into(),
version: ">=1".into(),
status,
source: DependencySource::Official {
repo: "extra".into(),
},
required_by: required_by.iter().map(|s| (*s).into()).collect(),
depends_on: Vec::new(),
is_core: false,
is_system: false,
}
}
fn file_info(name: &str, file_count: usize) -> PackageFileInfo {
let mut files = Vec::new();
for idx in 0..file_count {
files.push(FileChange {
path: format!("/tmp/{name}_{idx}"),
change_type: FileChangeType::New,
package: name.into(),
is_config: false,
predicted_pacnew: false,
predicted_pacsave: false,
});
}
PackageFileInfo {
name: name.into(),
files,
total_count: file_count,
new_count: file_count,
changed_count: 0,
removed_count: 0,
config_count: 0,
pacnew_candidates: 0,
pacsave_candidates: 0,
}
}
fn svc(unit: &str, decision: ServiceRestartDecision) -> ServiceImpact {
ServiceImpact {
unit_name: unit.into(),
providers: vec!["target".into()],
is_active: true,
needs_restart: true,
recommended_decision: ServiceRestartDecision::Restart,
restart_decision: decision,
}
}
#[test]
fn deps_display_len_counts_unique_expanded_dependencies() {
let items = vec![pkg("app"), pkg("tool")];
let deps = vec![
dep("libfoo", &["app"]),
dep("libbar", &["app", "tool"]),
dep("libbar", &["app"]),
];
let mut expanded = HashSet::new();
expanded.insert("app".to_string());
let len = compute_display_items_len(&items, &deps, &expanded);
assert_eq!(len, 4);
}
#[test]
fn deps_display_len_collapsed_counts_only_headers() {
let items = vec![pkg("app"), pkg("tool")];
let deps = vec![dep("libfoo", &["app"]), dep("libbar", &["tool"])];
let expanded = HashSet::new();
let len = compute_display_items_len(&items, &deps, &expanded);
assert_eq!(len, 2);
}
#[test]
fn file_display_len_respects_expansion_state() {
let items = vec![pkg("pkg"), pkg("empty")];
let info = vec![file_info("pkg", 2), file_info("empty", 0)];
let mut expanded = HashSet::new();
let collapsed = compute_file_display_items_len(&items, &info, &expanded);
assert_eq!(collapsed, 2); expanded.insert("pkg".to_string());
let expanded_len = compute_file_display_items_len(&items, &info, &expanded);
assert_eq!(expanded_len, 4); }
#[test]
fn build_file_items_match_expansion() {
let items = vec![pkg("pkg")];
let info = vec![file_info("pkg", 2)];
let collapsed = build_file_display_items(&items, &info, &HashSet::new());
assert_eq!(collapsed, vec![(true, "pkg".into())]);
let mut expanded = HashSet::new();
expanded.insert("pkg".to_string());
let expanded_items = build_file_display_items(&items, &info, &expanded);
assert_eq!(
expanded_items,
vec![
(true, "pkg".into()),
(false, String::new()),
(false, String::new())
]
);
}
fn setup_preflight_app(
tab: PreflightTab,
dependency_info: Vec<DependencyInfo>,
dep_selected: usize,
dep_tree_expanded: HashSet<String>,
) -> AppState {
let mut app = AppState::default();
let items = vec![pkg("target")];
app.modal = Modal::Preflight {
items,
action: PreflightAction::Install,
tab,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info,
dep_selected,
dep_tree_expanded,
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: std::collections::HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
};
app
}
fn setup_preflight_app_with_services(
tab: PreflightTab,
service_info: Vec<ServiceImpact>,
service_selected: usize,
) -> AppState {
let mut app = AppState::default();
let items = vec![pkg("target")];
app.modal = Modal::Preflight {
items,
action: PreflightAction::Install,
tab,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info,
service_selected,
services_loaded: true,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: std::collections::HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
};
app
}
#[test]
fn handle_enter_toggles_dependency_group() {
let deps = vec![dep("libfoo", &["target"])];
let mut app = setup_preflight_app(PreflightTab::Deps, deps, 0, HashSet::new());
let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
handle_preflight_key(enter, &mut app);
if let Modal::Preflight {
dep_tree_expanded, ..
} = &app.modal
{
assert!(dep_tree_expanded.contains("target"));
} else {
panic!("expected Preflight modal");
}
let enter_again = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
handle_preflight_key(enter_again, &mut app);
if let Modal::Preflight {
dep_tree_expanded, ..
} = &app.modal
{
assert!(!dep_tree_expanded.contains("target"));
} else {
panic!("expected Preflight modal");
}
}
#[test]
fn handle_down_stops_at_last_visible_dependency_row() {
let deps = vec![dep("libfoo", &["target"]), dep("libbar", &["target"])];
let mut expanded = HashSet::new();
expanded.insert("target".to_string());
let mut app = setup_preflight_app(PreflightTab::Deps, deps, 0, expanded);
handle_preflight_key(
KeyEvent::new(KeyCode::Down, KeyModifiers::empty()),
&mut app,
);
handle_preflight_key(
KeyEvent::new(KeyCode::Down, KeyModifiers::empty()),
&mut app,
);
handle_preflight_key(
KeyEvent::new(KeyCode::Down, KeyModifiers::empty()),
&mut app,
);
if let Modal::Preflight { dep_selected, .. } = &app.modal {
assert_eq!(*dep_selected, 2);
} else {
panic!("expected Preflight modal");
}
}
#[test]
fn handle_space_toggles_service_restart_decision() {
let services = vec![svc("nginx.service", ServiceRestartDecision::Restart)];
let mut app = setup_preflight_app_with_services(PreflightTab::Services, services, 0);
handle_preflight_key(
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty()),
&mut app,
);
if let Modal::Preflight { service_info, .. } = &app.modal {
assert_eq!(
service_info[0].restart_decision,
ServiceRestartDecision::Defer
);
} else {
panic!("expected Preflight modal");
}
handle_preflight_key(
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty()),
&mut app,
);
if let Modal::Preflight { service_info, .. } = &app.modal {
assert_eq!(
service_info[0].restart_decision,
ServiceRestartDecision::Restart
);
} else {
panic!("expected Preflight modal");
}
}
#[test]
fn handle_service_restart_shortcuts_force_decisions() {
let services = vec![svc("postgresql.service", ServiceRestartDecision::Defer)];
let mut app = setup_preflight_app_with_services(PreflightTab::Services, services, 0);
handle_preflight_key(
KeyEvent::new(KeyCode::Char('r'), KeyModifiers::empty()),
&mut app,
);
if let Modal::Preflight { service_info, .. } = &app.modal {
assert_eq!(
service_info[0].restart_decision,
ServiceRestartDecision::Restart
);
} else {
panic!("expected Preflight modal");
}
handle_preflight_key(
KeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT),
&mut app,
);
if let Modal::Preflight { service_info, .. } = &app.modal {
assert_eq!(
service_info[0].restart_decision,
ServiceRestartDecision::Defer
);
} else {
panic!("expected Preflight modal");
}
}
#[test]
fn installed_dependencies_are_included_in_list() {
let installed_dep = dep_with_status(
"libinstalled",
&["target"],
DependencyStatus::Installed {
version: "1.2.3".into(),
},
);
let to_install_dep = dep("libnew", &["target"]);
let deps = vec![installed_dep, to_install_dep];
let app = setup_preflight_app(PreflightTab::Deps, deps, 0, HashSet::new());
if let Modal::Preflight {
dependency_info, ..
} = &app.modal
{
assert_eq!(dependency_info.len(), 2);
assert!(
dependency_info.iter().any(|d| d.name == "libinstalled"
&& matches!(d.status, DependencyStatus::Installed { .. }))
);
assert!(
dependency_info
.iter()
.any(|d| d.name == "libnew" && matches!(d.status, DependencyStatus::ToInstall))
);
} else {
panic!("expected Preflight modal");
}
}
#[test]
fn cached_dependencies_load_on_tab_switch() {
let mut app = AppState::default();
let items = vec![pkg("target")];
let cached_deps = vec![dep("libcached", &["target"])];
app.install_list_deps = cached_deps;
app.modal = Modal::Preflight {
items,
action: PreflightAction::Install,
tab: PreflightTab::Summary,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: std::collections::HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
};
handle_preflight_key(
KeyEvent::new(KeyCode::Right, KeyModifiers::empty()),
&mut app,
);
if let Modal::Preflight {
tab,
dependency_info,
..
} = &app.modal
{
assert_eq!(*tab, PreflightTab::Deps);
assert_eq!(dependency_info.len(), 1);
assert_eq!(dependency_info[0].name, "libcached");
} else {
panic!("expected Preflight modal");
}
}
#[test]
fn cached_files_load_on_tab_switch() {
let mut app = AppState::default();
let items = vec![pkg("target")];
let cached_files = vec![file_info("target", 3)];
app.install_list_files = cached_files;
app.modal = Modal::Preflight {
items,
action: PreflightAction::Install,
tab: PreflightTab::Summary,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: std::collections::HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
};
handle_preflight_key(
KeyEvent::new(KeyCode::Right, KeyModifiers::empty()),
&mut app,
);
handle_preflight_key(
KeyEvent::new(KeyCode::Right, KeyModifiers::empty()),
&mut app,
);
if let Modal::Preflight { tab, file_info, .. } = &app.modal {
assert_eq!(*tab, PreflightTab::Files);
assert_eq!(file_info.len(), 1);
assert_eq!(file_info[0].name, "target");
assert_eq!(file_info[0].files.len(), 3);
} else {
panic!("expected Preflight modal");
}
}
#[test]
fn installed_dependencies_counted_in_display_length() {
let items = vec![pkg("app")];
let deps = vec![
dep_with_status(
"libinstalled",
&["app"],
DependencyStatus::Installed {
version: "1.0.0".into(),
},
),
dep("libnew", &["app"]),
];
let mut expanded = HashSet::new();
expanded.insert("app".to_string());
let len = compute_display_items_len(&items, &deps, &expanded);
assert_eq!(len, 3);
}
#[test]
fn files_displayed_when_file_info_populated() {
let items = vec![pkg("pkg1"), pkg("pkg2")];
let file_infos = vec![file_info("pkg1", 5), file_info("pkg2", 3)];
let mut expanded = HashSet::new();
expanded.insert("pkg1".to_string());
let len = compute_file_display_items_len(&items, &file_infos, &expanded);
assert_eq!(len, 7);
expanded.insert("pkg2".to_string());
let len_expanded = compute_file_display_items_len(&items, &file_infos, &expanded);
assert_eq!(len_expanded, 10);
}
#[test]
fn empty_file_info_handled_correctly() {
let items = vec![pkg("pkg1"), pkg("pkg2")];
let file_infos = Vec::<PackageFileInfo>::new();
let expanded = HashSet::new();
let len = compute_file_display_items_len(&items, &file_infos, &expanded);
assert_eq!(len, 2);
}
#[test]
fn dependencies_filtered_by_required_by_on_cache_load() {
let mut app = AppState::default();
let items = vec![pkg("target")];
let deps_for_target = dep("libtarget", &["target"]);
let deps_for_other = dep("libother", &["other"]);
app.install_list_deps = vec![deps_for_target, deps_for_other];
app.modal = Modal::Preflight {
items,
action: PreflightAction::Install,
tab: PreflightTab::Summary,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: std::collections::HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
};
handle_preflight_key(
KeyEvent::new(KeyCode::Right, KeyModifiers::empty()),
&mut app,
);
if let Modal::Preflight {
dependency_info, ..
} = &app.modal
{
assert_eq!(dependency_info.len(), 1);
assert_eq!(dependency_info[0].name, "libtarget");
assert!(
dependency_info[0]
.required_by
.iter()
.any(|req| req == "target")
);
} else {
panic!("expected Preflight modal");
}
}
#[test]
fn files_filtered_by_package_name_on_cache_load() {
let mut app = AppState::default();
let items = vec![pkg("target")];
let files_for_target = file_info("target", 2);
let files_for_other = file_info("other", 3);
app.install_list_files = vec![files_for_target, files_for_other];
app.modal = Modal::Preflight {
items,
action: PreflightAction::Install,
tab: PreflightTab::Summary,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: std::collections::HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
};
handle_preflight_key(
KeyEvent::new(KeyCode::Right, KeyModifiers::empty()),
&mut app,
);
handle_preflight_key(
KeyEvent::new(KeyCode::Right, KeyModifiers::empty()),
&mut app,
);
if let Modal::Preflight { file_info, .. } = &app.modal {
assert_eq!(file_info.len(), 1);
assert_eq!(file_info[0].name, "target");
assert_eq!(file_info[0].files.len(), 2);
} else {
panic!("expected Preflight modal");
}
}
#[test]
fn test_f_key_file_sync_doesnt_close_tui() {
let mut app = AppState {
modal: Modal::Preflight {
items: vec![pkg("test-package")],
action: PreflightAction::Install,
tab: PreflightTab::Files,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
},
..Default::default()
};
let key_event = KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty());
let should_exit = handle_preflight_key(key_event, &mut app);
assert!(
!should_exit,
"Pressing 'f' for file sync should not close the TUI"
);
}
#[test]
fn test_p_key_proceed_install_doesnt_close_tui() {
let mut app = AppState {
modal: Modal::Preflight {
items: vec![pkg("test-package")],
action: PreflightAction::Install,
tab: PreflightTab::Summary,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
},
..Default::default()
};
let key_event = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::empty());
let should_exit = handle_preflight_key(key_event, &mut app);
assert!(
!should_exit,
"Pressing 'p' to proceed should not close the TUI"
);
}
#[test]
fn test_p_key_proceed_remove_doesnt_close_tui() {
let mut app = AppState {
modal: Modal::Preflight {
items: vec![pkg("test-package")],
action: PreflightAction::Remove,
tab: PreflightTab::Summary,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Cascade,
cached_reverse_deps_report: None, },
..Default::default()
};
let key_event = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::empty());
let should_exit = handle_preflight_key(key_event, &mut app);
assert!(
!should_exit,
"Pressing 'p' to proceed with remove should not close the TUI"
);
}
#[test]
fn test_q_key_quit_modal_doesnt_close_tui() {
let mut app = AppState {
modal: Modal::Preflight {
items: vec![pkg("test-package")],
action: PreflightAction::Install,
tab: PreflightTab::Summary,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
},
..Default::default()
};
let key_event = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty());
let should_exit = handle_preflight_key(key_event, &mut app);
assert!(
!should_exit,
"Pressing 'q' to quit modal should not close the TUI"
);
assert!(
matches!(app.modal, Modal::None),
"Modal should be closed after pressing 'q'"
);
}
#[test]
fn test_esc_key_close_modal_doesnt_close_tui() {
let mut app = AppState {
modal: Modal::Preflight {
items: vec![pkg("test-package")],
action: PreflightAction::Install,
tab: PreflightTab::Summary,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
},
..Default::default()
};
let key_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
let should_exit = handle_preflight_key(key_event, &mut app);
assert!(
!should_exit,
"Pressing Esc to close modal should not close the TUI"
);
assert!(
matches!(app.modal, Modal::None),
"Modal should be closed after pressing Esc"
);
}
#[test]
fn test_enter_key_doesnt_close_tui() {
let mut app = AppState {
modal: Modal::Preflight {
items: vec![pkg("test-package")],
action: PreflightAction::Install,
tab: PreflightTab::Summary,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
},
..Default::default()
};
let key_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
let should_exit = handle_preflight_key(key_event, &mut app);
assert!(!should_exit, "Pressing Enter should not close the TUI");
}
#[test]
fn test_all_preflight_keys_return_false() {
let keys_to_test = vec![
(KeyCode::Char('f'), "f key"),
(KeyCode::Char('F'), "F key"),
(KeyCode::Char('p'), "p key"),
(KeyCode::Char('P'), "P key"),
(KeyCode::Char('q'), "q key"),
(KeyCode::Char('Q'), "Q key"),
(KeyCode::Char('s'), "s key"),
(KeyCode::Char('S'), "S key"),
(KeyCode::Char('c'), "c key"),
(KeyCode::Char('m'), "m key"),
(KeyCode::Char('d'), "d key"),
(KeyCode::Char('?'), "? key"),
(KeyCode::Esc, "Esc key"),
(KeyCode::Enter, "Enter key"),
];
for (key_code, key_name) in keys_to_test {
let mut app = AppState::default();
let tab = if matches!(key_code, KeyCode::Char('f' | 'F')) {
PreflightTab::Files
} else {
PreflightTab::Summary
};
app.modal = Modal::Preflight {
items: vec![pkg("test-package")],
action: PreflightAction::Install,
tab,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
};
let key_event = KeyEvent::new(key_code, KeyModifiers::empty());
let should_exit = handle_preflight_key(key_event, &mut app);
assert!(
!should_exit,
"{key_name} should not close the TUI (returned true, expected false)"
);
}
}
#[test]
fn test_keys_return_false_on_all_tabs() {
let tabs = vec![
PreflightTab::Summary,
PreflightTab::Deps,
PreflightTab::Files,
PreflightTab::Services,
PreflightTab::Sandbox,
];
for tab in tabs {
let mut app = AppState {
modal: Modal::Preflight {
items: vec![pkg("test-package")],
action: PreflightAction::Install,
tab,
summary: None,
summary_scroll: 0,
header_chips: crate::state::modal::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: CascadeMode::Basic,
cached_reverse_deps_report: None,
},
..Default::default()
};
let key_event = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::empty());
let should_exit = handle_preflight_key(key_event, &mut app);
assert!(
!should_exit,
"Pressing 'p' on {tab:?} tab should not close the TUI"
);
}
}