use super::*;
#[test]
fn copy_paths_and_names_from_a_panel() {
let dir = std::env::temp_dir().join(format!("fm84-copy-names-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("a.txt"), "").unwrap();
std::fs::write(dir.join("b.txt"), "").unwrap();
let mut app_state = AppState::new();
app_state.options = crate::options::Options::default();
app_state.is_left_active = true;
app_state.open_dir(true, dir.clone(), Some("b.txt".as_ref()));
let chord = |app_state: &mut AppState, modifiers| handle_key(app_state, KeyEvent::new(KeyCode::Char('c'), modifiers));
chord(&mut app_state, KeyModifiers::CONTROL);
assert_eq!(app_state.clipboard, dir.join("b.txt").to_string_lossy());
assert!(app_state.find_note.as_deref().unwrap().starts_with("Copied path: "));
chord(&mut app_state, KeyModifiers::ALT);
assert_eq!(app_state.clipboard, "b.txt");
app_state.selected_left.insert("a.txt".into());
app_state.selected_left.insert("b.txt".into());
chord(&mut app_state, KeyModifiers::ALT);
assert_eq!(app_state.clipboard, "a.txt\nb.txt");
assert_eq!(app_state.find_note.as_deref(), Some("Copied 2 names"));
press(&mut app_state, KeyCode::Down);
assert!(app_state.find_note.is_none());
app_state.selected_left.clear();
app_state.state_left.select(Some(0));
chord(&mut app_state, KeyModifiers::CONTROL);
assert_eq!(app_state.clipboard, dir.to_string_lossy());
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn swap_the_panels_and_send_the_other_one_here() {
let dir = std::env::temp_dir().join(format!("fm84-swap-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
for name in ["one", "two", "three"] {
std::fs::create_dir_all(dir.join(name)).unwrap();
}
std::fs::write(dir.join("one").join("a.txt"), "").unwrap();
std::fs::write(dir.join("one").join("b.txt"), "").unwrap();
let mut app_state = AppState::new();
app_state.is_left_active = true;
app_state.open_dir(true, dir.join("one"), Some("b.txt".as_ref()));
app_state.selected_left.insert("a.txt".into());
app_state.open_dir(false, dir.join("two"), None);
app_state.is_left_active = false;
app_state.new_tab();
app_state.open_dir(false, dir.join("three"), None);
app_state.is_left_active = true;
let chord = |app_state: &mut AppState, c, modifiers| handle_key(app_state, KeyEvent::new(KeyCode::Char(c), modifiers));
let cursor = |app_state: &AppState, is_left: bool| {
let (children, state) = if is_left { (&app_state.children_left, &app_state.state_left) } else { (&app_state.children_right, &app_state.state_right) };
children[state.selected().unwrap()].name_full.clone()
};
chord(&mut app_state, 'u', KeyModifiers::CONTROL);
assert_eq!((app_state.dir_left.clone(), app_state.dir_right.clone()), (dir.join("three"), dir.join("one")));
assert_eq!((app_state.tabs_left.list.len(), app_state.tabs_left.active, app_state.tabs_right.list.len()), (2, 1, 1));
assert!(!app_state.is_left_active);
assert_eq!(cursor(&app_state, false), "b.txt");
assert!(app_state.selected_right.contains(std::ffi::OsStr::new("a.txt")) && app_state.selected_left.is_empty());
app_state.select_tab(true, 0);
assert_eq!(app_state.dir_left, dir.join("two"));
chord(&mut app_state, 'i', KeyModifiers::ALT);
assert_eq!(app_state.dir_left, dir.join("one"));
assert_eq!(cursor(&app_state, true), "b.txt");
assert!(!app_state.is_left_active);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn sort_from_the_keys_and_the_headings() {
use crate::options::{Options, SortKey};
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
let dir = std::env::temp_dir().join(format!("fm84-sort-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("a.txt"), "a long one").unwrap();
std::fs::write(dir.join("b.rs"), "short").unwrap();
std::fs::write(dir.join("c.md"), "mid size").unwrap();
let mut app_state = AppState::new();
app_state.options = Options::default();
app_state.is_left_active = true;
app_state.open_dir(true, dir.clone(), Some("b.rs".as_ref()));
let names = |app_state: &AppState| app_state.children_left.iter().skip(1).map(|item| item.name_full.clone()).collect::<Vec<_>>();
let control = |app_state: &mut AppState, n| handle_key(app_state, KeyEvent::new(KeyCode::F(n), KeyModifiers::CONTROL));
control(&mut app_state, 3);
assert_eq!(names(&app_state), ["a.txt", "b.rs", "c.md"]);
control(&mut app_state, 3);
assert!(app_state.options.sort_descending);
assert_eq!(names(&app_state), ["c.md", "b.rs", "a.txt"]);
control(&mut app_state, 4);
assert_eq!((app_state.options.sort_key, app_state.options.sort_descending), (SortKey::Extension, false));
assert_eq!(names(&app_state), ["c.md", "b.rs", "a.txt"]);
let cursor = app_state.state_left.selected().unwrap();
assert_eq!(app_state.children_left[cursor].name_full, "b.rs");
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(120, 30)).unwrap();
crate::ui::render_ui(&mut terminal, &mut app_state);
let (cell, _) = *app_state.sort_headers.iter().find(|(_, key)| *key == SortKey::Size).unwrap();
let click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: cell.x,
row: cell.y,
modifiers: KeyModifiers::NONE,
};
crate::input::handle_mouse(&mut app_state, click);
assert_eq!(app_state.options.sort_key, SortKey::Size);
assert_eq!(names(&app_state), ["b.rs", "c.md", "a.txt"]);
crate::ui::render_ui(&mut terminal, &mut app_state);
let heading: String = (cell.x..cell.x + cell.width).map(|x| terminal.backend().buffer()[(x, cell.y)].symbol().to_string()).collect();
assert_eq!(heading.trim_end(), "Size ↑");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn go_to_a_path_and_back_and_forward() {
let dir = std::env::temp_dir().join(format!("fm84-go-to-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("sub/deeper")).unwrap();
std::fs::write(dir.join("a.txt"), "text").unwrap();
let mut app_state = AppState::new();
app_state.is_left_active = true;
app_state.open_dir(true, dir.clone(), Some("sub".as_ref()));
let alt = |app_state: &mut AppState, code: KeyCode| handle_key(app_state, KeyEvent::new(code, KeyModifiers::ALT));
let ctrl_l = |app_state: &mut AppState| handle_key(app_state, KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL));
let cursor = |app_state: &AppState| {
let index = app_state.state_left.selected().unwrap();
app_state.children_left[index].name.clone()
};
let type_in = |app_state: &mut AppState, text: &str| {
let Some(Dialog::Prompt(PromptKind::GoTo, input)) = &mut app_state.dialog else { panic!("no prompt") };
input.set(text.to_string());
};
ctrl_l(&mut app_state);
let Some(Dialog::Prompt(PromptKind::GoTo, input)) = &app_state.dialog else { panic!("no prompt") };
assert_eq!(input.text, format!("{}{}", dir.display(), std::path::MAIN_SEPARATOR));
press(&mut app_state, KeyCode::Char('s'));
press(&mut app_state, KeyCode::Tab);
press(&mut app_state, KeyCode::Char('d'));
press(&mut app_state, KeyCode::Tab);
press(&mut app_state, KeyCode::Enter);
assert_eq!(app_state.dir_left, dir.join("sub/deeper"));
ctrl_l(&mut app_state);
type_in(&mut app_state, "../../a.txt");
press(&mut app_state, KeyCode::Enter);
assert_eq!((app_state.dir_left.clone(), cursor(&app_state)), (dir.clone(), "a".to_string()));
ctrl_l(&mut app_state);
type_in(&mut app_state, "nowhere");
press(&mut app_state, KeyCode::Enter);
assert!(app_state.error.take().unwrap().contains("nowhere"));
assert_eq!(app_state.dir_left, dir);
alt(&mut app_state, KeyCode::Left);
assert_eq!(app_state.dir_left, dir.join("sub/deeper"));
alt(&mut app_state, KeyCode::Left);
assert_eq!((app_state.dir_left.clone(), cursor(&app_state)), (dir.clone(), "sub".to_string()));
alt(&mut app_state, KeyCode::Right);
assert_eq!(app_state.dir_left, dir.join("sub/deeper"));
press(&mut app_state, KeyCode::Backspace);
assert_eq!(app_state.dir_left, dir.join("sub"));
alt(&mut app_state, KeyCode::Right);
assert_eq!(app_state.dir_left, dir.join("sub"));
alt(&mut app_state, KeyCode::Left);
assert_eq!(app_state.dir_left, dir.join("sub/deeper"));
std::fs::remove_dir(dir.join("sub/deeper")).unwrap();
alt(&mut app_state, KeyCode::Right);
assert_eq!(app_state.dir_left, dir.join("sub"));
alt(&mut app_state, KeyCode::Left);
assert_eq!(app_state.dir_left, dir, "the gone one was passed over");
alt(&mut app_state, KeyCode::Right);
assert_eq!(app_state.dir_left, dir.join("sub"));
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn alt_shift_enter_sizes_every_directory() {
let dir = std::env::temp_dir().join(format!("fm84-size-all-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let count = crate::constants::SIZE_WORKERS * 2 + 1;
for n in 0..count {
let sub = dir.join(format!("dir{n:02}"));
std::fs::create_dir_all(sub.join("inner")).unwrap();
std::fs::write(sub.join("a.bin"), vec![0u8; n]).unwrap();
std::fs::write(sub.join("inner").join("b.bin"), vec![0u8; 100]).unwrap();
}
std::fs::write(dir.join("file.txt"), "not a directory").unwrap();
std::os::unix::fs::symlink(&dir, dir.join("loop")).unwrap();
for modifiers in [KeyModifiers::ALT | KeyModifiers::SHIFT, KeyModifiers::ALT] {
let mut app_state = AppState::new();
app_state.options = crate::options::Options::default();
app_state.is_left_active = true;
app_state.open_dir(true, dir.clone(), None);
handle_key(&mut app_state, KeyEvent::new(KeyCode::Enter, modifiers));
assert_eq!(app_state.dir_left, dir, "it sizes, and does not go in");
assert_eq!(app_state.dir_sizing.len(), crate::constants::SIZE_WORKERS);
assert_eq!(app_state.dir_size_queue.len(), count - crate::constants::SIZE_WORKERS);
assert_eq!(app_state.dir_size_so_far(app_state.dir_size_queue.back().unwrap()), Some(0));
let started = std::time::Instant::now();
while !app_state.dir_sizing.is_empty() || !app_state.dir_size_queue.is_empty() {
assert!(started.elapsed() < std::time::Duration::from_secs(10), "sizing never finished");
assert!(app_state.dir_sizing.len() <= crate::constants::SIZE_WORKERS);
std::thread::sleep(std::time::Duration::from_millis(2));
app_state.poll_dir_sizes();
}
for n in 0..count {
assert_eq!(app_state.dir_sizes.get(&dir.join(format!("dir{n:02}"))), Some(&(n as u64 + 100)), "dir{n:02}");
}
assert_eq!(app_state.dir_sizes.len(), count, "neither the link nor the file");
}
let mut app_state = AppState::new();
app_state.is_left_active = true;
app_state.open_dir(true, dir.clone(), None);
handle_key(&mut app_state, KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT | KeyModifiers::SHIFT));
press(&mut app_state, KeyCode::Esc);
assert!(app_state.dir_sizing.is_empty() && app_state.dir_size_queue.is_empty());
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_directory_sized_on_its_own_goes_ahead_of_those_waiting() {
let mut app_state = AppState::new();
for n in 0..crate::constants::SIZE_WORKERS {
app_state.start_dir_size(std::path::PathBuf::from(format!("/nonexistent/fm84/busy{n}")));
}
let waiting: Vec<std::path::PathBuf> = (0..3).map(|n| std::path::PathBuf::from(format!("/nonexistent/fm84/{n}"))).collect();
app_state.dir_size_queue.extend(waiting.iter().cloned());
app_state.start_dir_size(waiting[2].clone());
assert_eq!(app_state.dir_size_queue.front(), Some(&waiting[2]));
assert_eq!(app_state.dir_size_queue.len(), 3, "moved, not added again");
app_state.cancel_dir_sizes();
}
#[test]
fn leaving_a_directory_stops_sizing_what_is_in_it() {
let dir = std::env::temp_dir().join(format!("fm84-size-leave-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
for n in 0..crate::constants::SIZE_WORKERS * 2 {
std::fs::create_dir_all(dir.join(format!("d{n}"))).unwrap();
}
let elsewhere = std::env::temp_dir();
let size_all = |app_state: &mut AppState| handle_key(app_state, KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT | KeyModifiers::SHIFT));
let mut app_state = AppState::new();
app_state.is_left_active = true;
app_state.open_dir(true, dir.clone(), None);
app_state.open_dir(false, elsewhere.clone(), None);
app_state.dir_sizes.insert(dir.join("d0"), 42);
size_all(&mut app_state);
assert!(!app_state.dir_sizing.is_empty() && !app_state.dir_size_queue.is_empty());
app_state.open_dir(true, elsewhere.clone(), None);
assert!(app_state.dir_sizing.is_empty(), "running ones stopped");
assert!(app_state.dir_size_queue.is_empty(), "waiting ones dropped");
assert_eq!(app_state.dir_sizes.get(&dir.join("d0")), Some(&42), "a size counted before is kept");
let mut app_state = AppState::new();
app_state.is_left_active = true;
app_state.open_dir(true, dir.clone(), None);
app_state.open_dir(false, dir.clone(), None);
size_all(&mut app_state);
let asked = app_state.dir_sizing.len() + app_state.dir_size_queue.len();
app_state.open_dir(true, elsewhere.clone(), None);
assert_eq!(app_state.dir_sizing.len() + app_state.dir_size_queue.len(), asked);
app_state.cancel_dir_sizes();
std::fs::remove_dir_all(&dir).unwrap();
}