use anyhow::Result;
use cleansys_core::{CleanedItemType, CleanerCategory, CleanerItem, CleaningResult, RunOptions};
use cleansys_tui::app::{App, ChartType, FilterMode, SortMode, ViewMode};
fn noop(_opts: RunOptions) -> Result<CleaningResult> {
Ok(CleaningResult::new())
}
fn sample_item(name: &str, requires_root: bool) -> CleanerItem {
CleanerItem {
name: name.to_string(),
description: format!("{name} description"),
requires_root,
selected: false,
function: noop,
bytes_cleaned: 0,
last_result: None,
status: None,
}
}
fn seed_detailed_items(app: &mut App) {
let entries = [
(
"/home/user/.cache/pip/wheels/abc123.whl",
15_728_640u64,
"User Land Cleaners",
"pip cache",
CleanedItemType::File,
),
(
"/home/user/.mozilla/firefox/abc.default/cache2",
104_857_600,
"User Land Cleaners",
"firefox cache",
CleanedItemType::Directory,
),
(
"/home/user/.npm/_cacache",
8_388_608,
"User Land Cleaners",
"npm cache",
CleanedItemType::Directory,
),
(
"/var/cache/apt/archives",
52_428_800,
"System Cleaners",
"APT cache",
CleanedItemType::Directory,
),
];
for (path, size, category, cleaner_name, item_type) in entries {
app.add_detailed_cleaned_item(
path.to_string(),
size,
category.to_string(),
cleaner_name.to_string(),
item_type.into(),
);
}
}
fn app_with_categories() -> App {
let mut app = App::new();
app.categories = vec![
CleanerCategory {
name: "User".to_string(),
description: "User cleaners".to_string(),
items: vec![
sample_item("Browser Caches", false),
sample_item("Trash", false),
sample_item("Temp Files", false),
],
},
CleanerCategory {
name: "System".to_string(),
description: "System cleaners".to_string(),
items: vec![
sample_item("Package Cache", true),
sample_item("Logs", true),
],
},
];
app.category_index = 0;
app.item_list_state.select(Some(0));
app
}
#[test]
fn next_item_wraps_around() {
let mut app = app_with_categories();
assert_eq!(app.item_list_state.selected(), Some(0));
app.next_item();
assert_eq!(app.item_list_state.selected(), Some(1));
app.next_item();
assert_eq!(app.item_list_state.selected(), Some(2));
app.next_item();
assert_eq!(app.item_list_state.selected(), Some(0));
}
#[test]
fn previous_item_wraps_around() {
let mut app = app_with_categories();
app.item_list_state.select(Some(0));
app.previous_item();
assert_eq!(app.item_list_state.selected(), Some(2));
app.previous_item();
assert_eq!(app.item_list_state.selected(), Some(1));
}
#[test]
fn next_and_previous_category_wrap_and_reset_selection() {
let mut app = app_with_categories();
app.item_list_state.select(Some(2));
app.next_category();
assert_eq!(app.category_index, 1);
assert_eq!(app.item_list_state.selected(), Some(0));
app.next_category();
assert_eq!(app.category_index, 0);
app.previous_category();
assert_eq!(app.category_index, 1);
}
#[test]
fn toggle_selected_flips_current_item() {
let mut app = app_with_categories();
app.item_list_state.select(Some(0));
assert!(!app.categories[0].items[0].selected);
app.toggle_selected();
assert!(app.categories[0].items[0].selected);
app.toggle_selected();
assert!(!app.categories[0].items[0].selected);
}
#[test]
fn select_all_and_deselect_all_affect_current_category_only() {
let mut app = app_with_categories();
app.select_all();
assert!(app.categories[0].items.iter().all(|i| i.selected));
assert!(app.categories[1].items.iter().all(|i| !i.selected));
app.deselect_all();
assert!(app.categories[0].items.iter().all(|i| !i.selected));
}
#[test]
fn cycle_view_mode_goes_through_all_variants() {
let mut app = app_with_categories();
app.view_mode = ViewMode::Standard;
app.cycle_view_mode();
assert_eq!(app.view_mode, ViewMode::Compact);
app.cycle_view_mode();
assert_eq!(app.view_mode, ViewMode::Detailed);
app.cycle_view_mode();
assert_eq!(app.view_mode, ViewMode::Performance);
app.cycle_view_mode();
assert_eq!(app.view_mode, ViewMode::Standard);
}
#[test]
fn cycle_sort_mode_goes_through_all_variants() {
let mut app = app_with_categories();
app.sort_mode = SortMode::Name;
app.cycle_sort_mode();
assert_eq!(app.sort_mode, SortMode::Size);
app.cycle_sort_mode();
assert_eq!(app.sort_mode, SortMode::Status);
app.cycle_sort_mode();
assert_eq!(app.sort_mode, SortMode::Category);
app.cycle_sort_mode();
assert_eq!(app.sort_mode, SortMode::Name);
}
#[test]
fn cycle_filter_mode_goes_through_all_variants() {
let mut app = app_with_categories();
app.filter_mode = FilterMode::All;
app.cycle_filter_mode();
assert_eq!(app.filter_mode, FilterMode::Selected);
app.cycle_filter_mode();
assert_eq!(app.filter_mode, FilterMode::Completed);
app.cycle_filter_mode();
assert_eq!(app.filter_mode, FilterMode::Errors);
app.cycle_filter_mode();
assert_eq!(app.filter_mode, FilterMode::UserOnly);
app.cycle_filter_mode();
assert_eq!(app.filter_mode, FilterMode::SystemOnly);
app.cycle_filter_mode();
assert_eq!(app.filter_mode, FilterMode::All);
}
#[test]
fn toggle_chart_type_cycles_bar_pie_count_pie_size() {
let mut app = app_with_categories();
app.chart_type = ChartType::Bar;
app.toggle_chart_type();
assert_eq!(app.chart_type, ChartType::PieCount);
app.toggle_chart_type();
assert_eq!(app.chart_type, ChartType::PieSize);
app.toggle_chart_type();
assert_eq!(app.chart_type, ChartType::Bar);
}
#[test]
fn toggle_help_and_compact_mode() {
let mut app = app_with_categories();
assert!(!app.show_help);
app.toggle_help();
assert!(app.show_help);
app.toggle_help();
assert!(!app.show_help);
let was_compact = app.compact_mode;
app.toggle_compact_mode();
assert_eq!(app.compact_mode, !was_compact);
}
#[test]
fn search_toggle_and_input() {
let mut app = app_with_categories();
assert!(!app.search_active);
app.toggle_search();
assert!(app.search_active);
app.add_search_char('a');
app.add_search_char('b');
assert_eq!(app.search_query, "ab");
app.remove_search_char();
assert_eq!(app.search_query, "a");
app.clear_search();
assert!(!app.search_active);
assert!(app.search_query.is_empty());
}
#[test]
fn get_category_distribution_groups_by_cleaner_name() {
let mut app = App::new();
seed_detailed_items(&mut app);
let distribution = app.get_category_distribution();
assert!(!distribution.is_empty());
for pair in distribution.windows(2) {
assert!(pair[0].2 >= pair[1].2);
}
}
#[test]
fn get_filtered_detailed_items_respects_search_query() {
let mut app = App::new();
seed_detailed_items(&mut app);
app.search_query = "firefox".to_string();
let filtered = app.get_filtered_detailed_items();
assert!(!filtered.is_empty());
assert!(filtered
.iter()
.all(|item| item.path.to_lowercase().contains("firefox")
|| item.category.to_lowercase().contains("firefox")
|| item.cleaner_name.to_lowercase().contains("firefox")));
}
#[test]
fn get_filtered_detailed_items_sort_by_size_is_descending() {
let mut app = App::new();
seed_detailed_items(&mut app);
app.sort_mode = SortMode::Size;
let filtered = app.get_filtered_detailed_items();
for pair in filtered.windows(2) {
assert!(pair[0].size >= pair[1].size);
}
}
#[test]
fn clear_errors_resets_error_status_and_counter() {
let mut app = app_with_categories();
app.categories[0].items[0].status = Some(cleansys_core::Status::Error("boom".to_string()));
app.errors_count = 1;
app.clear_errors();
assert!(app.categories[0].items[0].status.is_none());
assert_eq!(app.errors_count, 0);
}
#[test]
fn update_counters_counts_selected_errors_and_operations() {
let mut app = app_with_categories();
app.categories[0].items[0].selected = true;
app.categories[0].items[1].status = Some(cleansys_core::Status::Error("x".to_string()));
app.categories[0].items[2].status = Some(cleansys_core::Status::Success("ok".to_string()));
app.update_counters();
assert_eq!(app.selected_cleaners_count, 1);
assert_eq!(app.errors_count, 1);
assert_eq!(app.operation_count, 2);
}
#[test]
fn request_run_with_nothing_selected_logs_message() {
let mut app = app_with_categories();
app.request_run().unwrap();
assert!(!app.awaiting_run_confirmation);
assert!(app
.result_messages
.iter()
.any(|m| m.contains("No items selected")));
}
#[test]
fn request_run_with_confirmation_mode_shows_confirmation_overlay() {
let mut app = app_with_categories();
app.confirmation_mode = true;
app.categories[0].items[0].selected = true;
app.request_run().unwrap();
assert!(app.awaiting_run_confirmation);
assert!(!app.is_running);
assert_eq!(app.pending_run_selection.len(), 1);
}
#[test]
fn cancel_run_confirmation_clears_pending_selection() {
let mut app = app_with_categories();
app.confirmation_mode = true;
app.categories[0].items[0].selected = true;
app.request_run().unwrap();
app.cancel_run_confirmation();
assert!(!app.awaiting_run_confirmation);
assert!(app.pending_run_selection.is_empty());
assert!(!app.is_running);
}
#[test]
fn confirm_pending_run_starts_execution_for_user_cleaners() {
let mut app = app_with_categories();
app.confirmation_mode = true;
app.categories[0].items[0].selected = true;
app.request_run().unwrap();
app.confirm_pending_run().unwrap();
assert!(!app.awaiting_run_confirmation);
assert!(app.is_running);
assert!(matches!(
app.categories[0].items[0].status,
Some(cleansys_core::Status::Pending)
));
}
#[test]
fn request_run_without_confirmation_mode_starts_immediately() {
let mut app = app_with_categories();
app.confirmation_mode = false;
app.categories[0].items[0].selected = true;
app.request_run().unwrap();
assert!(!app.awaiting_run_confirmation);
assert!(app.is_running);
}
#[test]
fn request_run_with_root_item_and_no_root_needs_elevation() {
let mut app = app_with_categories();
app.confirmation_mode = false;
app.is_root = false;
app.categories[1].items[0].selected = true;
app.request_run().unwrap();
assert!(!app.is_running);
if cleansys_core::utils::supports_sudo_prompt() {
assert!(app.needs_sudo);
} else {
assert!(app.needs_admin_notice);
}
}
#[test]
fn run_preview_with_nothing_selected_logs_message() {
let mut app = app_with_categories();
app.run_preview();
assert!(!app.preview_open);
assert!(app
.result_messages
.iter()
.any(|m| m.contains("No items selected")));
}
#[test]
fn run_preview_with_selection_populates_results_and_opens_overlay() {
let mut app = app_with_categories();
app.categories[0].items[0].selected = true;
app.run_preview();
assert!(app.preview_open);
assert_eq!(app.preview_results.len(), 1);
assert!(!app.is_running);
assert!(app.categories[0].items[0].status.is_none());
}
#[test]
fn close_preview_clears_state() {
let mut app = app_with_categories();
app.categories[0].items[0].selected = true;
app.run_preview();
app.close_preview();
assert!(!app.preview_open);
assert!(app.preview_results.is_empty());
}
#[test]
fn select_all_everywhere_selects_every_category() {
let mut app = app_with_categories();
app.select_all_everywhere();
assert!(app
.categories
.iter()
.all(|c| c.items.iter().all(|i| i.selected)));
}
#[test]
fn deselect_all_everywhere_deselects_every_category() {
let mut app = app_with_categories();
app.select_all_everywhere();
app.deselect_all_everywhere();
assert!(app
.categories
.iter()
.all(|c| c.items.iter().all(|i| !i.selected)));
}
#[test]
fn begin_execution_skips_reprompt_when_already_authenticated() {
let mut app = app_with_categories();
app.confirmation_mode = false;
app.is_root = false;
app.password_prompt.mark_authenticated_for_tests();
app.categories[1].items[0].selected = true;
app.request_run().unwrap();
assert!(
!app.needs_sudo,
"should not re-show the password prompt once already authenticated"
);
assert!(app.is_running, "run should start immediately");
}
#[test]
fn begin_execution_still_prompts_when_not_yet_authenticated() {
let mut app = app_with_categories();
app.confirmation_mode = false;
app.is_root = false;
app.categories[1].items[0].selected = true;
app.request_run().unwrap();
if cleansys_core::utils::supports_sudo_prompt() {
assert!(app.needs_sudo);
}
assert!(!app.is_running);
}
#[test]
fn password_authentication_success_path_resets_all_item_status_and_bytes() {
let mut app = app_with_categories();
app.confirmation_mode = false;
app.categories[0].items[1].status = Some(cleansys_core::Status::Error(
"stale from last run".to_string(),
));
app.categories[0].items[1].bytes_cleaned = 1234;
app.is_root = false;
app.categories[1].items[0].selected = true; app.request_run().unwrap();
assert!(app.needs_sudo || app.needs_admin_notice);
if cleansys_core::utils::supports_sudo_prompt() {
app.password_prompt.mark_authenticated_for_tests();
app.needs_sudo = false;
app.password_prompt.hide();
let selected_cleaners = app.pending_operations.clone();
app.pending_operations.clear();
assert!(!selected_cleaners.is_empty());
app.start_operations_for_tests(&selected_cleaners);
assert!(app.categories[0].items[1].status.is_none());
assert_eq!(app.categories[0].items[1].bytes_cleaned, 0);
assert!(matches!(
app.categories[1].items[0].status,
Some(cleansys_core::Status::Pending)
));
}
}