#![allow(clippy::unwrap_used, clippy::panic)]
use super::*;
use crate::config::{FzfConfig, SortOrder, ThemeConfig};
use std::collections::HashMap;
struct TestFileGuard {
path: PathBuf,
}
impl Drop for TestFileGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
#[test]
fn test_stash_creation_flow() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_stash.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.start_stash_create();
assert_eq!(app.mode, Mode::StashCreateInput);
assert!(app.input_buffer.is_empty());
app.input_buffer = "my_custom_stash".to_string();
let esc_key = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
let consumed = crate::input::handle_key(&mut app, esc_key, 0);
assert!(consumed);
assert_eq!(app.mode, Mode::Detail);
app.start_stash_create();
app.input_buffer = "my_custom_stash".to_string();
let backspace_key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty());
crate::input::handle_key(&mut app, backspace_key, 0);
assert_eq!(app.input_buffer, "my_custom_stas");
let char_key = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::empty());
crate::input::handle_key(&mut app, char_key, 0);
assert_eq!(app.input_buffer, "my_custom_stash");
let enter_key = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
crate::input::handle_key(&mut app, enter_key, 0);
assert_eq!(app.mode, Mode::Detail);
app.mode = Mode::Detail;
app.detail_focus = DetailSection::Commits;
let mut mock_info = repo::RepoInfo::default();
mock_info.changes.unstaged = vec![repo::FileEntry { path: "dirty.rs".to_string(), label: "M" }];
app.current_detail = Some(repo::ItemDetail::Repo {
resolved: PathBuf::from("a_repo"),
info: Box::new(mock_info),
});
assert!(app.has_uncommitted_changes());
let s_key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::empty());
let consumed = crate::input::handle_key(&mut app, s_key, 0);
assert!(consumed);
assert_eq!(app.mode, Mode::StashingUI);
}
#[test]
fn test_network_action_progress_and_error_handling() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_network.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.fetching = true;
app.status_message = Some("Pushing...".to_string());
assert!(app.fetching);
assert_eq!(app.status_message.as_deref(), Some("Pushing..."));
app.tx.send("Push failed: git push rejected".to_string()).unwrap();
while let Ok(msg) = app.rx.try_recv() {
let is_err = msg.starts_with("Fetch failed:")
|| msg.starts_with("Pull failed:")
|| msg.starts_with("Push failed:")
|| msg.starts_with("Failed to")
|| msg.contains("failed");
if is_err {
app.error_message = Some(msg);
} else {
app.status_message = Some(msg);
}
app.fetching = false;
}
assert!(!app.fetching);
assert_eq!(app.error_message.as_deref(), Some("Push failed: git push rejected"));
let esc_key = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
let consumed = crate::input::handle_key(&mut app, esc_key, 0);
assert!(consumed);
assert!(app.error_message.is_none());
}
#[test]
fn test_remote_tags_progress_and_error_handling() {
let config = Config {
items: vec![".".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_remote_tags_progress.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
let mock_info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
remotes: crate::repo::TabData::Loaded(vec![crate::repo::RemoteInfo {
name: "origin".to_string(),
url: "git@github.com:tareqmy/gitwig.git".to_string(),
push_url: None,
refspecs: vec![],
}]),
..crate::repo::RepoInfo::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("."),
info: Box::new(mock_info),
});
app.fetch_remote_tags(true);
assert!(app.fetching);
assert_eq!(app.status_message.as_deref(), Some("Fetching tags from 'origin'..."));
app.tx.send("REMOTE_TAGS_ERR:Failed to get remote tags: custom error".to_string()).unwrap();
if let Ok(msg) = app.rx.try_recv() {
if let Some(err_msg) = msg.strip_prefix("REMOTE_TAGS_ERR:") {
app.set_error(err_msg.to_string());
app.fetching = false;
}
}
assert!(!app.fetching);
assert_eq!(app.error_message.as_deref(), Some("Failed to get remote tags: custom error"));
}
#[test]
fn test_remote_fetch_progress_and_error_handling() {
let config = Config {
items: vec![".".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_remote_fetch_progress.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
let mock_info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
remotes: crate::repo::TabData::Loaded(vec![crate::repo::RemoteInfo {
name: "origin".to_string(),
url: "git@github.com:tareqmy/gitwig.git".to_string(),
push_url: None,
refspecs: vec![],
}]),
..crate::repo::RepoInfo::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("."),
info: Box::new(mock_info),
});
app.fetch_remote("origin");
assert!(app.fetching);
assert_eq!(app.status_message.as_deref(), Some("Fetching remote 'origin'..."));
app.tx.send("Fetch failed: custom fetch error".to_string()).unwrap();
if let Ok(msg) = app.rx.try_recv() {
let is_err = msg.starts_with("Fetch failed:")
|| msg.starts_with("Pull failed:")
|| msg.starts_with("Push failed:")
|| msg.starts_with("Failed to")
|| msg.contains("failed");
if is_err {
app.set_error(msg);
}
app.fetching = false;
}
assert!(!app.fetching);
assert_eq!(app.error_message.as_deref(), Some("Fetch failed: custom fetch error"));
}
#[test]
fn test_set_error_logging() {
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_set_error.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
let test_error_msg = "Test error message for debugging".to_string();
app.set_error(test_error_msg.clone());
assert_eq!(app.error_message.as_ref(), Some(&test_error_msg));
let logs = crate::debug_log::get_logs();
assert!(logs.iter().any(|log| log.contains("ERROR") && log.contains(&test_error_msg)));
}
#[test]
fn test_sorting_logic() {
let config = Config {
items: vec!["z_repo".to_string(), "a_repo".to_string(), "m_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_sort.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
assert_eq!(app.config.items[0], "z_repo");
assert_eq!(app.config.items[1], "a_repo");
app.cycle_sort_order();
assert_eq!(app.config.sort_by, SortOrder::Alphabetical);
assert_eq!(app.config.items[0], "a_repo");
assert_eq!(app.config.items[1], "m_repo");
assert_eq!(app.config.items[2], "z_repo");
app.toggle_sort_reverse();
assert!(app.config.sort_reverse);
assert_eq!(app.config.items[0], "z_repo");
assert_eq!(app.config.items[1], "m_repo");
assert_eq!(app.config.items[2], "a_repo");
app.toggle_sort_reverse();
assert!(!app.config.sort_reverse);
app.config.visits.insert("a_repo".to_string(), 10);
app.config.visits.insert("z_repo".to_string(), 20);
app.config.visits.insert("m_repo".to_string(), 5);
app.cycle_sort_order();
assert_eq!(app.config.sort_by, SortOrder::RecentVisit);
assert_eq!(app.config.items[0], "z_repo");
assert_eq!(app.config.items[1], "a_repo");
assert_eq!(app.config.items[2], "m_repo");
}
#[test]
fn test_duplicate_prevention() {
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_duplicate.toml");
let _ = std::fs::remove_file(&temp_path);
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.input_buffer = " /path/to/repo ".to_string(); app.commit_add();
assert_eq!(app.config.items.len(), 1);
assert_eq!(app.config.items[0], "/path/to/repo");
assert_eq!(app.status_message, Some("Saved".to_string()));
app.status_message = None;
app.input_buffer = "/path/to/repo".to_string();
app.commit_add();
assert_eq!(app.config.items.len(), 1);
assert_eq!(app.status_message, Some("Repository already added".to_string()));
app.status_message = None;
if let Some(home) = dirs::home_dir() {
let home_str = home.to_string_lossy().to_string();
app.input_buffer = "~/my_cool_repo".to_string();
app.commit_add();
assert_eq!(app.config.items.len(), 2);
assert_eq!(app.config.items[1], "~/my_cool_repo");
assert_eq!(app.status_message, Some("Saved".to_string()));
app.status_message = None;
let expanded_path = format!("{}/my_cool_repo", home_str);
app.input_buffer = expanded_path;
app.commit_add();
assert_eq!(app.config.items.len(), 2);
assert_eq!(app.status_message, Some("Repository already added".to_string()));
app.status_message = None;
let new_abs = format!("{}/another_cool_repo", home_str);
app.input_buffer = new_abs;
app.commit_add();
assert_eq!(app.config.items.len(), 3);
assert_eq!(app.config.items[2], format!("{}/another_cool_repo", home_str));
assert_eq!(app.status_message, Some("Saved".to_string()));
app.status_message = None;
app.input_buffer = "~/another_cool_repo".to_string();
app.commit_add();
assert_eq!(app.config.items.len(), 3);
assert_eq!(app.status_message, Some("Repository already added".to_string()));
app.status_message = None; }
let len_before = app.config.items.len();
app.add_repo_path(" /another/path ".to_string());
assert_eq!(app.config.items.len(), len_before + 1);
assert_eq!(app.config.items.last().unwrap(), "/another/path");
assert_eq!(app.status_message, Some("Added repository".to_string()));
app.status_message = None;
app.add_repo_path("/another/path".to_string());
assert_eq!(app.config.items.len(), len_before + 1);
assert_eq!(app.status_message, Some("Repository already added".to_string()));
}
#[test]
fn test_bulk_add_folders() {
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_dir = std::env::temp_dir().join("gitwig_test_bulk_add_dir");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let repo_a = temp_dir.join("repo_a");
let repo_b = temp_dir.join("repo_b");
let repo_c = temp_dir.join("repo_c");
std::fs::create_dir_all(repo_a.join(".git")).unwrap();
std::fs::create_dir_all(&repo_b).unwrap();
std::fs::create_dir_all(repo_c.join(".git")).unwrap();
let config_path = temp_dir.join("config_bulk.toml");
let _ = std::fs::remove_file(&config_path);
let _guard = TestFileGuard { path: config_path.clone() };
let mut app = App::new(config, config_path);
app.config.fzf.git_only = true;
app.input_buffer = temp_dir.to_string_lossy().to_string();
app.commit_bulk_add();
assert_eq!(app.config.items.len(), 2);
assert!(app.config.items.iter().any(|item| item.ends_with("repo_a")));
assert!(app.config.items.iter().any(|item| item.ends_with("repo_c")));
assert!(!app.config.items.iter().any(|item| item.ends_with("repo_b")));
app.config.items.clear();
app.original_items.clear();
app.statuses.clear();
app.config.fzf.git_only = false;
app.input_buffer = temp_dir.to_string_lossy().to_string();
app.commit_bulk_add();
assert_eq!(app.config.items.len(), 3);
assert!(app.config.items.iter().any(|item| item.ends_with("repo_a")));
assert!(app.config.items.iter().any(|item| item.ends_with("repo_b")));
assert!(app.config.items.iter().any(|item| item.ends_with("repo_c")));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_pinning_and_sorting() {
let config = Config {
items: vec!["z_repo".to_string(), "a_repo".to_string(), "m_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Alphabetical,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_pin.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.sort_items_in_place();
assert_eq!(app.config.items[0], "a_repo");
assert_eq!(app.config.items[1], "m_repo");
assert_eq!(app.config.items[2], "z_repo");
app.selected_index = 2;
app.toggle_pin_selected();
assert!(app.config.pinned.contains("z_repo"));
assert_eq!(app.config.items[0], "z_repo");
assert_eq!(app.config.items[1], "a_repo");
assert_eq!(app.config.items[2], "m_repo");
assert_eq!(app.selected_index, 0);
app.toggle_sort_reverse();
assert_eq!(app.config.items[0], "z_repo");
assert_eq!(app.config.items[1], "m_repo");
assert_eq!(app.config.items[2], "a_repo");
assert_eq!(app.selected_index, 0);
app.toggle_sort_reverse();
app.selected_index = 2; app.toggle_pin_selected();
assert_eq!(app.config.items[0], "m_repo");
assert_eq!(app.config.items[1], "z_repo");
assert_eq!(app.config.items[2], "a_repo");
assert_eq!(app.selected_index, 0);
app.selected_index = 0;
app.toggle_pin_selected();
assert_eq!(app.config.items[0], "z_repo");
assert_eq!(app.config.items[1], "a_repo");
assert_eq!(app.config.items[2], "m_repo");
}
#[test]
fn test_commit_input_scroll() {
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_commit_scroll.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
assert_eq!(app.commit_input_scroll, 0);
app.commit_input_scroll_down();
assert_eq!(app.commit_input_scroll, 1);
app.commit_input_scroll_down();
assert_eq!(app.commit_input_scroll, 2);
app.commit_input_scroll_up();
assert_eq!(app.commit_input_scroll, 1);
app.cancel_commit();
assert_eq!(app.commit_input_scroll, 0);
}
#[test]
fn test_commit_popup_maximized_toggle() {
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_commit_maximize.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
assert!(!app.commit_popup.maximized);
app.toggle_commit_popup_maximized();
assert!(app.commit_popup.maximized);
app.toggle_commit_popup_maximized();
assert!(!app.commit_popup.maximized);
app.toggle_commit_popup_maximized();
assert!(app.commit_popup.maximized);
app.cancel_commit();
assert!(!app.commit_popup.maximized);
}
#[test]
fn test_cherry_pick_and_revert_flow() {
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_cherry_pick.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
let mock_info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
commits: vec![crate::repo::CommitEntry {
id: "1234567".to_string(),
oid: "1234567890abcdef1234567890abcdef12345678".to_string(),
summary: "test commit".to_string(),
author: "author".to_string(),
when: "today".to_string(),
date: "today".to_string(),
refs: vec![],
message: "msg".to_string(),
files: vec![],
signature_status: "N".to_string(),
}],
..Default::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("/mock/repo"),
info: Box::new(mock_info),
});
app.commit_list.selection = 0;
app.request_cherry_pick();
assert_eq!(app.mode, Mode::CherryPickConfirm);
assert!(app.cherry_pick_target.is_some());
assert_eq!(
app.cherry_pick_target.as_ref().unwrap().0,
"1234567890abcdef1234567890abcdef12345678"
);
app.cancel_cherry_pick();
assert_eq!(app.mode, Mode::Detail);
assert!(app.cherry_pick_target.is_none());
app.commit_list.selection = 0;
app.request_revert();
assert_eq!(app.mode, Mode::RevertConfirm);
assert!(app.revert_target.is_some());
assert_eq!(app.revert_target.as_ref().unwrap().0, "1234567890abcdef1234567890abcdef12345678");
app.cancel_revert();
assert_eq!(app.mode, Mode::Detail);
assert!(app.revert_target.is_none());
}
#[test]
fn test_commit_amend_flow() {
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_commit_amend.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
assert!(!app.commit_popup.amend);
app.toggle_commit_amend();
assert!(app.commit_popup.amend);
app.toggle_commit_amend();
assert!(!app.commit_popup.amend);
app.start_commit_amend();
assert_eq!(app.status_message.as_deref(), Some("No commit to amend"));
assert_eq!(app.mode, Mode::Normal);
let info = crate::repo::RepoInfo {
head: Some(crate::repo::HeadInfo {
short_id: "dummy_sha".to_string(),
summary: "dummy message".to_string(),
author: "author".to_string(),
when: "now".to_string(),
}),
..Default::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: PathBuf::from("/dummy"),
info: Box::new(info),
});
app.start_commit_amend();
assert!(app.commit_popup.amend);
assert!(app.commit_popup.editing);
assert_eq!(app.mode, Mode::CommitInput);
}
#[test]
fn test_splitter_dragging() {
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_splitter.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.detail_areas.bottom_left = Some(Rect::new(0, 0, 40, 50));
app.detail_areas.bottom_right = Some(Rect::new(40, 0, 60, 50));
app.detail_areas.inspect_horizontal_splitter = Some(Rect::new(39, 0, 2, 50));
let down_event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 39,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, down_event);
assert_eq!(app.active_drag_splitter, Some(Splitter::InspectHorizontal));
let drag_event = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 30,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, drag_event);
assert_eq!(app.inspect_horizontal_split_pct, 30);
let up_event = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 30,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, up_event);
assert_eq!(app.active_drag_splitter, None);
app.detail_areas.commits = Some(Rect::new(0, 0, 100, 20));
app.detail_areas.bottom_right = Some(Rect::new(0, 20, 100, 30));
app.detail_areas.workspace_main_splitter = Some(Rect::new(0, 19, 100, 2));
let down_event_main = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 19,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, down_event_main);
assert_eq!(app.active_drag_splitter, Some(Splitter::WorkspaceMain));
let drag_event_main = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 10,
row: 25,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, drag_event_main);
assert_eq!(app.workspace_main_split_pct, 50);
let drag_event_main_2 = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 10,
row: 15,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, drag_event_main_2);
assert_eq!(app.workspace_main_split_pct, 30);
let up_event_main = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 10,
row: 15,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, up_event_main);
assert_eq!(app.active_drag_splitter, None);
app.detail_areas.files = Some(Rect::new(0, 0, 45, 50));
app.detail_areas.file_content = Some(Rect::new(45, 0, 55, 50));
app.detail_areas.files_horizontal_splitter = Some(Rect::new(44, 0, 2, 50));
let down_event_files = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 44,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, down_event_files);
assert_eq!(app.active_drag_splitter, Some(Splitter::FilesHorizontal));
let drag_event_files = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 60,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, drag_event_files);
assert_eq!(app.files_horizontal_split_pct, 60);
let up_event_files = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 60,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, up_event_files);
assert_eq!(app.active_drag_splitter, None);
app.detail_areas = DetailAreas::default();
app.detail_areas.local_branches = Some(Rect::new(0, 0, 50, 50));
app.detail_areas.remote_branches = Some(Rect::new(50, 0, 50, 50));
app.detail_areas.branches_horizontal_splitter = Some(Rect::new(49, 0, 2, 50));
let down_event_branches = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 49,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, down_event_branches);
assert_eq!(app.active_drag_splitter, Some(Splitter::BranchesHorizontal));
let drag_event_branches = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 35,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, drag_event_branches);
assert_eq!(app.branches_horizontal_split_pct, 35);
let up_event_branches = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 35,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, up_event_branches);
assert_eq!(app.active_drag_splitter, None);
app.detail_areas = DetailAreas::default();
app.detail_areas.stashes = Some(Rect::new(0, 0, 35, 25));
app.detail_areas.stashed_files = Some(Rect::new(0, 25, 35, 25));
app.detail_areas.bottom_right = Some(Rect::new(35, 0, 65, 50));
app.detail_areas.stashes_horizontal_splitter = Some(Rect::new(34, 0, 2, 50));
app.detail_areas.stashes_vertical_splitter = Some(Rect::new(0, 24, 35, 2));
let down_stashes_h = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 34,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, down_stashes_h);
assert_eq!(app.active_drag_splitter, Some(Splitter::StashesHorizontal));
let drag_stashes_h = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 40,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, drag_stashes_h);
assert_eq!(app.stashes_horizontal_split_pct, 40);
let up_stashes_h = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 40,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, up_stashes_h);
let down_stashes_v = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 24,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, down_stashes_v);
assert_eq!(app.active_drag_splitter, Some(Splitter::StashesVertical));
let drag_stashes_v = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 10,
row: 30,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, drag_stashes_v);
assert_eq!(app.stashes_vertical_split_pct, 60);
let up_stashes_v = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 10,
row: 30,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, up_stashes_v);
app.detail_areas = DetailAreas::default();
app.detail_areas.tab_bar = Some(Rect::new(0, 0, 100, 2));
app.detail_areas.overview_horizontal_splitter = Some(Rect::new(49, 2, 2, 48));
let down_overview = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 49,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, down_overview);
assert_eq!(app.active_drag_splitter, Some(Splitter::OverviewHorizontal));
let drag_overview = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 30,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, drag_overview);
assert_eq!(app.overview_horizontal_split_pct, 30);
let up_overview = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 30,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, up_overview);
assert_eq!(app.active_drag_splitter, None);
}
#[test]
fn test_mouse_row_selection_in_detail_panels() {
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_mouse_select.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.detail_areas = crate::ui_detail::DetailAreas::default();
app.detail_areas.commits = Some(Rect::new(0, 0, 100, 20));
app.detail_areas.commits_inner = Some(Rect::new(1, 1, 98, 18));
let mock_info = repo::RepoInfo {
branch: Some("main".to_string()),
commits: vec![
repo::CommitEntry {
id: "1".to_string(),
oid: "1111111111111111111111111111111111111111".to_string(),
summary: "C1".to_string(),
author: "A".to_string(),
when: "now".to_string(),
date: "now".to_string(),
refs: vec![],
message: "msg".to_string(),
files: vec![],
signature_status: "N".to_string(),
},
repo::CommitEntry {
id: "2".to_string(),
oid: "2222222222222222222222222222222222222222".to_string(),
summary: "C2".to_string(),
author: "B".to_string(),
when: "now".to_string(),
date: "now".to_string(),
refs: vec![],
message: "msg".to_string(),
files: vec![],
signature_status: "N".to_string(),
},
repo::CommitEntry {
id: "3".to_string(),
oid: "3333333333333333333333333333333333333333".to_string(),
summary: "C3".to_string(),
author: "C".to_string(),
when: "now".to_string(),
date: "now".to_string(),
refs: vec![],
message: "msg".to_string(),
files: vec![],
signature_status: "N".to_string(),
},
],
..repo::RepoInfo::default()
};
app.current_detail = Some(repo::ItemDetail::Repo {
resolved: PathBuf::from("a_repo"),
info: Box::new(mock_info),
});
let commit_click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 3,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, commit_click);
assert_eq!(app.commit_list.selection, 1);
assert_eq!(app.detail_focus, DetailSection::Commits);
app.detail_areas = crate::ui_detail::DetailAreas::default();
let mut mock_info_2 = repo::RepoInfo::default();
mock_info_2.changes.staged = vec![
repo::FileEntry { path: "s1.rs".to_string(), label: "M" },
repo::FileEntry { path: "s2.rs".to_string(), label: "M" },
];
mock_info_2.changes.unstaged = vec![repo::FileEntry { path: "u1.rs".to_string(), label: "M" }];
app.current_detail = Some(repo::ItemDetail::Repo {
resolved: PathBuf::from("a_repo"),
info: Box::new(mock_info_2),
});
app.detail_areas.staged_sub = Some(Rect::new(0, 20, 50, 10));
app.detail_areas.staged_sub_inner = Some(Rect::new(1, 21, 48, 8));
let staged_click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 5,
row: 22,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, staged_click);
assert_eq!(app.status_list.staging_file_selection, 1);
assert_eq!(app.detail_focus, DetailSection::Staged);
app.detail_areas = crate::ui_detail::DetailAreas::default();
let mut mock_info_2_unstaged = repo::RepoInfo::default();
mock_info_2_unstaged.changes.unstaged =
vec![repo::FileEntry { path: "u1.rs".to_string(), label: "M" }];
app.current_detail = Some(repo::ItemDetail::Repo {
resolved: PathBuf::from("a_repo"),
info: Box::new(mock_info_2_unstaged),
});
app.detail_areas.unstaged_sub = Some(Rect::new(0, 30, 50, 10));
app.detail_areas.unstaged_sub_inner = Some(Rect::new(1, 31, 48, 8));
let unstaged_click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 5,
row: 31,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, unstaged_click);
assert_eq!(app.status_list.staging_file_selection, 0);
assert_eq!(app.detail_focus, DetailSection::Unstaged);
app.detail_areas = crate::ui_detail::DetailAreas::default();
let mock_info_3 = repo::RepoInfo {
local_branches: repo::TabData::Loaded(vec![
repo::BranchInfo {
name: "b1".to_string(),
is_head: true,
short_sha: "123".to_string(),
short_message: "msg".to_string(),
},
repo::BranchInfo {
name: "b2".to_string(),
is_head: false,
short_sha: "456".to_string(),
short_message: "msg".to_string(),
},
]),
remote_branches: repo::TabData::Loaded(vec![repo::BranchInfo {
name: "origin/b1".to_string(),
is_head: false,
short_sha: "123".to_string(),
short_message: "msg".to_string(),
}]),
..Default::default()
};
app.current_detail = Some(repo::ItemDetail::Repo {
resolved: PathBuf::from("a_repo"),
info: Box::new(mock_info_3),
});
app.detail_areas.local_branches = Some(Rect::new(0, 0, 50, 20));
app.detail_areas.local_branches_inner = Some(Rect::new(1, 1, 48, 18));
let local_branch_click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 5,
row: 2, modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, local_branch_click);
assert_eq!(app.branch_list.local_branch_selection, 1);
assert_eq!(app.detail_focus, DetailSection::LocalBranches);
app.detail_areas = crate::ui_detail::DetailAreas::default();
let mock_info_3_remote = repo::RepoInfo {
remote_branches: repo::TabData::Loaded(vec![repo::BranchInfo {
name: "origin/b1".to_string(),
is_head: false,
short_sha: "123".to_string(),
short_message: "msg".to_string(),
}]),
..Default::default()
};
app.current_detail = Some(repo::ItemDetail::Repo {
resolved: PathBuf::from("a_repo"),
info: Box::new(mock_info_3_remote),
});
app.detail_areas.remote_branches = Some(Rect::new(50, 0, 50, 20));
app.detail_areas.remote_branches_inner = Some(Rect::new(51, 1, 48, 18));
let remote_branch_click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 55,
row: 1, modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, remote_branch_click);
assert_eq!(app.branch_list.remote_branch_selection, 0);
assert_eq!(app.detail_focus, DetailSection::RemoteBranches);
app.detail_areas = crate::ui_detail::DetailAreas::default();
let mock_info_4 = repo::RepoInfo {
local_tags: repo::TabData::Loaded(vec![
repo::BranchInfo {
name: "t1".to_string(),
is_head: false,
short_sha: "123".to_string(),
short_message: "msg".to_string(),
},
repo::BranchInfo {
name: "t2".to_string(),
is_head: false,
short_sha: "456".to_string(),
short_message: "msg".to_string(),
},
]),
..Default::default()
};
app.current_detail = Some(repo::ItemDetail::Repo {
resolved: PathBuf::from("a_repo"),
info: Box::new(mock_info_4),
});
app.detail_areas.local_tags = Some(Rect::new(0, 0, 100, 20));
app.detail_areas.local_tags_inner = Some(Rect::new(1, 1, 98, 18));
let tag_click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 2, modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, tag_click);
assert_eq!(app.tag_list.local_tag_selection, 1);
assert_eq!(app.detail_focus, DetailSection::LocalTags);
app.detail_areas = crate::ui_detail::DetailAreas::default();
let mock_info_5 = repo::RepoInfo {
remotes: repo::TabData::Loaded(vec![
repo::RemoteInfo {
name: "r1".to_string(),
url: "url1".to_string(),
push_url: None,
refspecs: vec![],
},
repo::RemoteInfo {
name: "r2".to_string(),
url: "url2".to_string(),
push_url: None,
refspecs: vec![],
},
]),
..Default::default()
};
app.current_detail = Some(repo::ItemDetail::Repo {
resolved: PathBuf::from("a_repo"),
info: Box::new(mock_info_5),
});
app.detail_areas.remotes = Some(Rect::new(0, 0, 100, 20));
app.detail_areas.remotes_inner = Some(Rect::new(1, 1, 98, 18));
let remote_click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 2, modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, remote_click);
assert_eq!(app.branch_list.remote_selection, 1);
assert_eq!(app.detail_focus, DetailSection::Remotes);
app.detail_areas = crate::ui_detail::DetailAreas::default();
let mock_info_6 = repo::RepoInfo {
stashes: repo::TabData::Loaded(vec![
repo::StashInfo {
index: 0,
commit_id: "123".to_string(),
message: "s1".to_string(),
files: vec![
repo::FileEntry { path: "f1.rs".to_string(), label: "M" },
repo::FileEntry { path: "f2.rs".to_string(), label: "M" },
],
},
repo::StashInfo {
index: 1,
commit_id: "456".to_string(),
message: "s2".to_string(),
files: vec![],
},
]),
..Default::default()
};
app.current_detail = Some(repo::ItemDetail::Repo {
resolved: PathBuf::from("a_repo"),
info: Box::new(mock_info_6),
});
app.detail_areas.stashes = Some(Rect::new(0, 0, 100, 20));
app.detail_areas.stashes_inner = Some(Rect::new(1, 1, 98, 18));
let stash_click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 2, modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, stash_click);
assert_eq!(app.stash_list.stash_selection, 1);
assert_eq!(app.detail_focus, DetailSection::Stashes);
app.detail_areas = crate::ui_detail::DetailAreas::default();
app.stash_list.stash_selection = 0;
app.detail_areas.stashed_files = Some(Rect::new(0, 20, 100, 20));
app.detail_areas.stashed_files_inner = Some(Rect::new(1, 21, 98, 18));
let stash_file_click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 22, modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, stash_file_click);
assert_eq!(app.stash_list.stash_file_selection, 1);
assert_eq!(app.detail_focus, DetailSection::StashedFiles);
app.mode = Mode::Inspect;
app.detail_areas = crate::ui_detail::DetailAreas::default();
let mut mock_info_inspect = repo::RepoInfo::default();
let mock_commit = repo::CommitEntry {
id: "1234567".to_string(),
oid: "1234567890abcdef1234567890abcdef12345678".to_string(),
summary: "test summary".to_string(),
author: "author".to_string(),
when: "now".to_string(),
date: "now".to_string(),
refs: vec![],
message: "message".to_string(),
files: vec![
repo::FileEntry { path: "file1.rs".to_string(), label: "M" },
repo::FileEntry { path: "file2.rs".to_string(), label: "M" },
],
signature_status: "N".to_string(),
};
mock_info_inspect.commits = vec![mock_commit];
app.current_detail = Some(repo::ItemDetail::Repo {
resolved: PathBuf::from("a_repo"),
info: Box::new(mock_info_inspect),
});
app.commit_list.selection = 0;
app.detail_areas.bottom_left = Some(Rect::new(0, 10, 50, 10));
app.detail_areas.changed_files_inner = Some(Rect::new(1, 11, 48, 8));
let inspect_file_click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 5,
row: 12, modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, inspect_file_click);
assert_eq!(app.status_list.file_selection, 1);
assert_eq!(app.detail_focus, DetailSection::Staged);
app.mode = Mode::Detail;
app.detail_tab = 1; app.detail_areas = crate::ui_detail::DetailAreas::default();
app.detail_areas.files = Some(Rect::new(0, 0, 100, 20));
app.detail_areas.files_inner = Some(Rect::new(1, 1, 98, 18));
app.file_tree.visible_files = vec![
crate::app::FileTreeItem {
name: "f1.rs".to_string(),
full_path: "f1.rs".to_string(),
is_dir: false,
depth: 0,
is_expanded: false,
},
crate::app::FileTreeItem {
name: "f2.rs".to_string(),
full_path: "f2.rs".to_string(),
is_dir: false,
depth: 0,
is_expanded: false,
},
];
let files_click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 2, modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, files_click);
assert_eq!(app.file_tree.file_list_selection, 1);
assert_eq!(app.detail_focus, DetailSection::Files);
}
#[test]
fn test_settings_mode_navigation_and_editing() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_settings.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
assert_eq!(app.mode, Mode::Normal);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('s')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Settings);
assert_eq!(app.settings_selected_index, 0);
assert!(!app.settings_editing);
assert!(app.settings_focus_sidebar);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('w')), 10);
assert!(handled);
assert!(!app.settings_focus_sidebar);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(handled);
assert!(app.settings_editing);
assert_eq!(app.input_buffer, "100");
crate::input::handle_key(&mut app, key_event(KeyCode::Backspace), 10);
crate::input::handle_key(&mut app, key_event(KeyCode::Char('5')), 10);
assert_eq!(app.input_buffer, "105");
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.settings_editing);
assert_eq!(app.config.poll_interval_ms, 105);
crate::input::handle_key(&mut app, key_event(KeyCode::Left), 10);
assert!(app.settings_focus_sidebar);
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.settings_selected_index, 1);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.settings_focus_sidebar);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert_eq!(app.config.sort_by, SortOrder::Alphabetical);
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.settings_selected_index, 2);
crate::input::handle_key(&mut app, key_event(KeyCode::Char(' ')), 10);
assert!(app.config.sort_reverse);
crate::input::handle_key(&mut app, key_event(KeyCode::Left), 10);
assert!(app.settings_focus_sidebar);
crate::input::handle_key(&mut app, key_event(KeyCode::Char('4')), 10);
assert_eq!(app.settings_selected_index, 3);
assert!(!app.settings_focus_sidebar);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(app.settings_editing);
assert!(app.settings_theme_list.contains(&"default".to_string()));
let prev_idx = app.settings_theme_index;
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
if app.settings_theme_list.len() > 1 {
assert_eq!(app.settings_theme_index, prev_idx + 1);
}
crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert!(!app.settings_editing);
crate::input::handle_key(&mut app, key_event(KeyCode::Char('3')), 10);
assert_eq!(app.settings_selected_index, 11);
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.settings_selected_index, 5);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(app.settings_editing);
app.input_buffer = "/some/path".to_string();
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.settings_editing);
assert_eq!(app.config.fzf.start_dir, "/some/path");
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.settings_selected_index, 4);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(app.settings_editing);
app.input_buffer = "3".to_string();
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.settings_editing);
assert_eq!(app.config.fzf.max_depth, 3);
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.settings_selected_index, 10);
assert!(app.config.fzf.git_only);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.config.fzf.git_only);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(app.config.fzf.git_only);
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.settings_selected_index, 8);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(app.settings_editing);
app.input_buffer = "target, node_modules ,.git".to_string();
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.settings_editing);
assert_eq!(
app.config.fzf.excludes,
vec!["target".to_string(), "node_modules".to_string(), ".git".to_string()]
);
crate::input::handle_key(&mut app, key_event(KeyCode::Char('1')), 10);
assert_eq!(app.settings_selected_index, 0);
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.settings_selected_index, 7);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(app.settings_editing);
app.input_buffer = "15".to_string();
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.settings_editing);
assert_eq!(app.config.page_size, 15);
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.settings_selected_index, 9);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(app.settings_editing);
app.input_buffer = "lazygit".to_string();
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.settings_editing);
assert_eq!(app.config.git_app, "lazygit");
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.settings_selected_index, 12);
assert!(!app.config.compatibility_mode);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(app.config.compatibility_mode);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.config.compatibility_mode);
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.settings_selected_index, 13);
assert!(!app.config.resync_on_tab_change);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(app.config.resync_on_tab_change);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.config.resync_on_tab_change);
crate::input::handle_key(&mut app, key_event(KeyCode::Char('2')), 10);
assert_eq!(app.settings_selected_index, 1);
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10); crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10); assert_eq!(app.settings_selected_index, 6);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(app.settings_editing);
app.input_buffer = "100".to_string();
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.settings_editing);
assert_eq!(app.config.max_commits, 100);
crate::input::handle_key(&mut app, key_event(KeyCode::Char('3')), 10);
assert_eq!(app.settings_selected_index, 11);
assert!(app.config.fzf.enabled);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(!app.config.fzf.enabled);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(app.config.fzf.enabled);
crate::input::handle_key(&mut app, key_event(KeyCode::PageUp), 10);
assert_eq!(app.settings_selected_index, 11);
crate::input::handle_key(&mut app, key_event(KeyCode::Home), 10);
assert_eq!(app.settings_selected_index, 11);
crate::input::handle_key(&mut app, key_event(KeyCode::End), 10);
assert_eq!(app.settings_selected_index, 8);
crate::input::handle_key(&mut app, key_event(KeyCode::PageDown), 10);
assert_eq!(app.settings_selected_index, 8);
crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert!(app.settings_focus_sidebar);
crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_remote_add_delete_flow() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_remotes.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.detail_tab = 5;
app.detail_focus = DetailSection::Remotes;
app.current_detail = Some(repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("."),
info: Box::new(repo::RepoInfo {
remotes: repo::TabData::Loaded(vec![repo::RemoteInfo {
name: "origin".to_string(),
url: "https://github.com/example/repo.git".to_string(),
push_url: None,
refspecs: vec![],
}]),
..Default::default()
}),
});
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('a')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::RemoteAddNameInput);
app.input_buffer = "upstream".to_string();
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(handled);
assert_eq!(app.mode, Mode::RemoteAddUrlInput);
assert_eq!(app.remote_add_name, "upstream");
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Detail);
app.branch_list.remote_selection = 0;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('d')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::RemoteDeleteConfirm);
assert_eq!(app.remote_action_target.as_deref(), Some("origin"));
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('n')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Detail);
assert!(app.remote_action_target.is_none());
}
#[test]
fn test_workspace_tab_right_arrow_inspect() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_inspect.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.detail_tab = 0;
app.detail_focus = DetailSection::Staged;
let mut changes = crate::repo::WorktreeChanges::default();
changes.staged.push(crate::repo::FileEntry { path: "dummy.txt".to_string(), label: "M" });
let info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
changes,
..crate::repo::RepoInfo::default()
};
app.current_detail =
Some(crate::repo::ItemDetail::Repo { resolved: PathBuf::from("."), info: Box::new(info) });
app.commit_list.selection = 0;
assert_ne!(app.mode, Mode::Inspect);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Right), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Inspect);
assert_eq!(app.detail_focus, DetailSection::StagingDetails);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Left), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Inspect);
assert_eq!(app.detail_focus, DetailSection::Staged);
app.mode = Mode::Detail;
app.detail_focus = DetailSection::StagingDetails;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Right), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Inspect);
assert_eq!(app.detail_focus, DetailSection::StagingDetails);
}
#[test]
fn test_commit_enter_key_inspect() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_inspect_enter.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.detail_tab = 0;
app.detail_focus = DetailSection::Commits;
let mut changes = crate::repo::WorktreeChanges::default();
changes.staged.push(crate::repo::FileEntry { path: "dummy.txt".to_string(), label: "M" });
let info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
changes,
..crate::repo::RepoInfo::default()
};
app.current_detail =
Some(crate::repo::ItemDetail::Repo { resolved: PathBuf::from("."), info: Box::new(info) });
app.commit_list.selection = 0;
assert_ne!(app.mode, Mode::Inspect);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Inspect);
assert_eq!(app.detail_focus, DetailSection::Staged);
}
#[test]
fn test_inspect_commit_shortcut() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_inspect_commit.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Inspect;
app.detail_tab = 0;
app.detail_focus = DetailSection::Staged;
let mut changes = crate::repo::WorktreeChanges::default();
changes.staged.push(crate::repo::FileEntry { path: "dummy.txt".to_string(), label: "M" });
let info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
summary: crate::repo::RepoSummary { staged: 1, ..Default::default() },
changes,
..crate::repo::RepoInfo::default()
};
app.current_detail =
Some(crate::repo::ItemDetail::Repo { resolved: PathBuf::from("."), info: Box::new(info) });
app.commit_list.selection = 0;
assert_eq!(app.mode, Mode::Inspect);
assert!(app.is_uncommitted_selected());
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('c')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::CommitInput);
}
#[test]
fn test_workspace_all_changes_shortcuts() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_workspace_all.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.detail_tab = 0;
app.detail_focus = DetailSection::Unstaged;
let mut changes = crate::repo::WorktreeChanges::default();
changes.unstaged.push(crate::repo::FileEntry { path: "dummy.txt".to_string(), label: "M" });
let info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
summary: crate::repo::RepoSummary { modified: 1, ..Default::default() },
changes,
..crate::repo::RepoInfo::default()
};
app.current_detail =
Some(crate::repo::ItemDetail::Repo { resolved: PathBuf::from("."), info: Box::new(info) });
app.commit_list.selection = 0;
assert!(app.is_uncommitted_selected());
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('X')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::DiscardChangesConfirm);
assert_eq!(app.discard_target.as_ref().unwrap().0, "All Changes");
app.cancel_discard_changes();
assert_eq!(app.mode, Mode::Detail);
}
#[test]
fn test_inspect_workspace_all_changes_shortcuts() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_inspect_workspace_all.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Inspect;
app.detail_tab = 0;
app.detail_focus = DetailSection::Unstaged;
let mut changes = crate::repo::WorktreeChanges::default();
changes.unstaged.push(crate::repo::FileEntry { path: "dummy.txt".to_string(), label: "M" });
let info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
summary: crate::repo::RepoSummary { modified: 1, ..Default::default() },
changes,
..crate::repo::RepoInfo::default()
};
app.current_detail =
Some(crate::repo::ItemDetail::Repo { resolved: PathBuf::from("."), info: Box::new(info) });
app.commit_list.selection = 0;
assert!(app.is_uncommitted_selected());
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('X')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::DiscardChangesConfirm);
assert_eq!(app.discard_target.as_ref().unwrap().0, "All Changes");
app.cancel_discard_changes();
app.mode = Mode::Inspect;
app.detail_focus = DetailSection::Unstaged;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('a')), 10);
assert!(handled);
app.detail_focus = DetailSection::Staged;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('a')), 10);
assert!(handled);
}
#[test]
fn test_workspace_all_changes_focus_transitions() {
let mut temp_path = std::env::temp_dir();
temp_path.push(format!(
"gitwig_test_app_all_{}",
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
));
std::fs::create_dir_all(&temp_path).unwrap();
let repo = git2::Repository::init(&temp_path).unwrap();
let mut config_git = repo.config().unwrap();
config_git.set_str("user.name", "Test User").unwrap();
config_git.set_str("user.email", "test@example.com").unwrap();
let file_path = temp_path.join("file.txt");
std::fs::write(&file_path, "initial").unwrap();
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let mut app = App::new(config, temp_path.join("config.toml"));
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: temp_path.clone(),
info: Box::new(crate::repo::RepoInfo::default()),
});
app.detail_focus = DetailSection::Unstaged;
app.stage_all_changes();
assert_eq!(app.detail_focus, DetailSection::Staged);
app.detail_focus = DetailSection::Staged;
app.unstage_all_changes();
assert_eq!(app.detail_focus, DetailSection::Unstaged);
let _ = std::fs::remove_dir_all(&temp_path);
}
#[test]
fn test_workspace_tab_focus_cycle_skips_empty_panels() {
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_cycle.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.detail_tab = 0;
app.detail_focus = DetailSection::Commits;
let mut changes = crate::repo::WorktreeChanges::default();
changes.staged.push(crate::repo::FileEntry { path: "staged_file.txt".to_string(), label: "M" });
let info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
changes,
..crate::repo::RepoInfo::default()
};
app.current_detail =
Some(crate::repo::ItemDetail::Repo { resolved: PathBuf::from("."), info: Box::new(info) });
app.commit_list.selection = 0;
app.cycle_detail_focus(false);
assert_eq!(app.detail_focus, DetailSection::Staged);
app.cycle_detail_focus(false);
assert_eq!(app.detail_focus, DetailSection::StagingDetails);
app.cycle_detail_focus(false);
assert_eq!(app.detail_focus, DetailSection::Commits);
app.cycle_detail_focus(true);
assert_eq!(app.detail_focus, DetailSection::StagingDetails);
app.cycle_detail_focus(true);
assert_eq!(app.detail_focus, DetailSection::Staged);
app.cycle_detail_focus(true);
assert_eq!(app.detail_focus, DetailSection::Commits);
app.commit_list.selection = 1;
let empty_info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
..crate::repo::RepoInfo::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: PathBuf::from("."),
info: Box::new(empty_info),
});
app.cycle_detail_focus(false);
assert_eq!(app.detail_focus, DetailSection::CommitDetails);
app.cycle_detail_focus(false);
assert_eq!(app.detail_focus, DetailSection::Commits);
app.cycle_detail_focus(true);
assert_eq!(app.detail_focus, DetailSection::CommitDetails);
app.cycle_detail_focus(true);
assert_eq!(app.detail_focus, DetailSection::Commits);
}
#[test]
fn test_git_app_shortcut_triggers_pending() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_git_app.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
assert!(!app.pending_git_app);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('g')), 10);
assert!(handled);
assert!(app.pending_git_app);
}
#[test]
fn test_files_fzf_shortcut_triggers_pending() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_files_fzf.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.detail_tab = 1; app.detail_focus = DetailSection::Files;
assert!(!app.pending_files_fzf);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('f')), 10);
assert!(handled);
assert!(app.pending_files_fzf);
}
#[test]
fn test_logs_search_picker_flow() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_logs_search.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.detail_tab = 0; app.detail_focus = DetailSection::Commits;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('f')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::SearchColumnPicker);
assert_eq!(app.search_column_selection, 0);
crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.search_column_selection, 1);
assert!(app.search_columns_message);
crate::input::handle_key(&mut app, key_event(KeyCode::Char(' ')), 10);
assert!(!app.search_columns_message);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert_eq!(app.mode, Mode::LogsSearchInput);
assert!(app.in_logs_ui);
let mock_info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
commits: vec![
crate::repo::CommitEntry {
id: "1234567".to_string(),
oid: "1234567890abcdef1234567890abcdef12345678".to_string(),
summary: "first test".to_string(),
author: "test author 1".to_string(),
when: "today".to_string(),
date: "today".to_string(),
refs: vec![],
message: "msg".to_string(),
files: vec![],
signature_status: "N".to_string(),
},
crate::repo::CommitEntry {
id: "2234567".to_string(),
oid: "2234567890abcdef1234567890abcdef12345678".to_string(),
summary: "no match".to_string(),
author: "author 1".to_string(),
when: "today".to_string(),
date: "today".to_string(),
refs: vec![],
message: "msg".to_string(),
files: vec![],
signature_status: "N".to_string(),
},
crate::repo::CommitEntry {
id: "2345678".to_string(),
oid: "234567890abcdef1234567890abcdef12345678a".to_string(),
summary: "second test".to_string(),
author: "test author 2".to_string(),
when: "today".to_string(),
date: "today".to_string(),
refs: vec![],
message: "msg".to_string(),
files: vec![],
signature_status: "N".to_string(),
},
crate::repo::CommitEntry {
id: "3234567".to_string(),
oid: "3234567890abcdef1234567890abcdef12345678".to_string(),
summary: "no match".to_string(),
author: "author 1".to_string(),
when: "today".to_string(),
date: "today".to_string(),
refs: vec![],
message: "msg".to_string(),
files: vec![],
signature_status: "N".to_string(),
},
crate::repo::CommitEntry {
id: "4234567".to_string(),
oid: "4234567890abcdef1234567890abcdef12345678".to_string(),
summary: "third test".to_string(),
author: "test author 1".to_string(),
when: "today".to_string(),
date: "today".to_string(),
refs: vec![],
message: "msg".to_string(),
files: vec![],
signature_status: "N".to_string(),
},
],
..crate::repo::RepoInfo::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("."),
info: Box::new(mock_info),
});
crate::input::handle_key(&mut app, key_event(KeyCode::Char('t')), 10);
crate::input::handle_key(&mut app, key_event(KeyCode::Char('e')), 10);
crate::input::handle_key(&mut app, key_event(KeyCode::Char('s')), 10);
crate::input::handle_key(&mut app, key_event(KeyCode::Char('t')), 10);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert_eq!(app.mode, Mode::Logs);
assert_eq!(app.commit_list.search_query.as_deref(), Some("test"));
assert_eq!(app.commit_total(), 5);
assert_eq!(app.commit_list.selection, 0); crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.commit_list.selection, 2); crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.commit_list.selection, 4); crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert_eq!(app.commit_list.selection, 4);
crate::input::handle_key(&mut app, key_event(KeyCode::PageUp), 10);
assert_eq!(app.commit_list.selection, 0); crate::input::handle_key(&mut app, key_event(KeyCode::PageDown), 10);
assert_eq!(app.commit_list.selection, 4);
crate::input::handle_key(&mut app, key_event(KeyCode::Up), 10);
assert_eq!(app.commit_list.selection, 2);
crate::input::handle_key(&mut app, key_event(KeyCode::Up), 10);
assert_eq!(app.commit_list.selection, 0);
let matching_commit = crate::repo::CommitEntry {
id: "1234567".to_string(),
oid: "1234567890abcdef1234567890abcdef12345678".to_string(),
summary: "a test message".to_string(), author: "test author".to_string(), when: "today".to_string(),
date: "today".to_string(),
refs: vec![],
message: "message body".to_string(),
files: vec![],
signature_status: "N".to_string(),
};
assert!(app.commit_matches_query(&matching_commit));
let non_matching_commit = crate::repo::CommitEntry {
id: "1234567".to_string(),
oid: "1234567890abcdef1234567890abcdef12345678".to_string(),
summary: "a test message".to_string(), author: "other author".to_string(), when: "today".to_string(),
date: "today".to_string(),
refs: vec![],
message: "message body".to_string(),
files: vec![],
signature_status: "N".to_string(),
};
assert!(!app.commit_matches_query(&non_matching_commit));
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert_eq!(app.mode, Mode::Inspect);
assert!(app.in_logs_ui);
crate::input::handle_key(&mut app, key_event(KeyCode::Char('q')), 10);
assert_eq!(app.mode, Mode::Logs);
assert!(app.in_logs_ui);
crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert_eq!(app.mode, Mode::Inspect);
assert!(app.in_logs_ui);
crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert_eq!(app.mode, Mode::Logs);
assert!(app.in_logs_ui);
crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert_eq!(app.mode, Mode::Detail);
assert!(!app.in_logs_ui);
assert!(app.commit_list.search_query.is_none());
}
#[test]
fn test_detail_view_sync_on_tab_change_and_refresh() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec![".".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_sync.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.detail_tab = 0;
let mock_info = crate::repo::RepoInfo {
branch: Some("mock_branch_name_test_xyz".to_string()),
..crate::repo::RepoInfo::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("."),
info: Box::new(mock_info),
});
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('2')), 10);
assert!(handled);
assert_eq!(app.detail_tab, 1);
assert!(app.current_detail.is_some());
if let Some(crate::repo::ItemDetail::Repo { info, .. }) = &app.current_detail {
assert_eq!(info.branch.as_deref(), Some("mock_branch_name_test_xyz"));
} else {
panic!("Expected Repo detail");
}
app.config.resync_on_tab_change = true;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('3')), 10);
assert!(handled);
assert_eq!(app.detail_tab, 2);
assert!(app.current_detail.is_some());
let (path, detail) = app.detail_rx.recv().unwrap();
assert_eq!(Some(&path), app.loading_repo_path.as_ref());
app.apply_detail_snapshot(detail);
app.loading_repo_path = None;
if let Some(crate::repo::ItemDetail::Repo { info, .. }) = &app.current_detail {
assert_ne!(info.branch.as_deref(), Some("mock_branch_name_test_xyz"));
} else {
panic!("Expected Repo detail");
}
let mock_info_2 = crate::repo::RepoInfo {
branch: Some("mock_branch_name_test_xyz".to_string()),
..crate::repo::RepoInfo::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("."),
info: Box::new(mock_info_2),
});
app.config.resync_on_tab_change = false;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('R')), 10);
assert!(handled);
assert_eq!(app.status_message.as_deref(), Some("Refreshed"));
let (path, detail) = app.detail_rx.recv().unwrap();
assert_eq!(Some(&path), app.loading_repo_path.as_ref());
app.apply_detail_snapshot(detail);
app.loading_repo_path = None;
if let Some(crate::repo::ItemDetail::Repo { info, .. }) = &app.current_detail {
assert_ne!(info.branch.as_deref(), Some("mock_branch_name_test_xyz"));
} else {
panic!("Expected Repo detail");
}
}
#[test]
fn test_branch_and_tag_checkout_confirmation() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec![".gitwig".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_checkout.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.detail_tab = 3; app.detail_focus = DetailSection::LocalBranches;
let mock_info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
local_branches: crate::repo::TabData::Loaded(vec![
crate::repo::BranchInfo {
name: "main".to_string(),
is_head: true,
short_sha: "".to_string(),
short_message: "".to_string(),
},
crate::repo::BranchInfo {
name: "feature-branch".to_string(),
is_head: false,
short_sha: "".to_string(),
short_message: "".to_string(),
},
]),
remote_branches: crate::repo::TabData::Loaded(vec![crate::repo::BranchInfo {
name: "origin/feature-branch".to_string(),
is_head: false,
short_sha: "".to_string(),
short_message: "".to_string(),
}]),
local_tags: crate::repo::TabData::Loaded(vec![crate::repo::BranchInfo {
name: "v1.0.0".to_string(),
is_head: false,
short_sha: "".to_string(),
short_message: "".to_string(),
}]),
..crate::repo::RepoInfo::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("."),
info: Box::new(mock_info),
});
app.branch_list.local_branch_selection = 1;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(handled);
assert_eq!(app.mode, Mode::BranchCheckoutConfirm);
assert_eq!(app.branch_action_target, Some(("feature-branch".to_string(), false)));
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('n')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Detail);
assert_eq!(app.branch_action_target, None);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(handled);
assert_eq!(app.mode, Mode::BranchCheckoutConfirm);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('y')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Detail);
assert_eq!(app.branch_action_target, None);
app.detail_tab = 4;
app.tag_list.local_tag_selection = 0;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(handled);
assert_eq!(app.mode, Mode::TagCheckoutConfirm);
assert_eq!(app.tag_checkout_target, Some("v1.0.0".to_string()));
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Detail);
assert_eq!(app.tag_checkout_target, None);
}
#[test]
fn test_repo_search_filtering() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["z_repo".to_string(), "a_repo".to_string(), "m_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_search.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
assert_eq!(app.get_items_len(), 3);
assert_eq!(app.get_filtered_items().len(), 3);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('f')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::RepoSearchInput);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('a')), 10);
assert!(handled);
assert_eq!(app.repo_search_query.as_deref(), Some("a"));
assert_eq!(app.get_items_len(), 1);
assert_eq!(app.get_filtered_items()[0].1, &"a_repo".to_string());
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Normal);
assert_eq!(app.repo_search_query.as_deref(), Some("a"));
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert!(handled);
assert_eq!(app.repo_search_query, None);
assert_eq!(app.get_items_len(), 3);
}
#[test]
fn test_normal_mode_right_arrow_detail() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_right_arrow.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
assert_eq!(app.mode, Mode::Normal);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Right), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Detail);
assert_eq!(app.loading_repo_path.as_deref(), Some("a_repo"));
let (path, detail) = app.detail_rx.recv().unwrap();
assert_eq!(path, "a_repo");
app.current_detail = Some(detail);
app.loading_repo_path = None;
assert_eq!(app.loading_repo_path, None);
assert!(app.current_detail.is_some());
}
#[test]
fn test_inspect_full_screen_diff_toggle() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_full_diff.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Inspect;
app.detail_focus = DetailSection::StagingDetails;
app.inspect_full_diff = false;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Right), 10);
assert!(handled);
assert!(app.inspect_full_diff);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Left), 10);
assert!(handled);
assert!(!app.inspect_full_diff);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Right), 10);
assert!(handled);
assert!(app.inspect_full_diff);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert!(handled);
assert!(!app.inspect_full_diff);
assert_eq!(app.mode, Mode::Inspect); }
#[test]
fn test_files_tab_full_screen_toggle() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_files_full.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Detail;
app.detail_tab = 1;
app.detail_focus = DetailSection::FileContent;
app.inspect_full_diff = false;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Right), 10);
assert!(handled);
assert!(app.inspect_full_diff);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Left), 10);
assert!(handled);
assert!(!app.inspect_full_diff);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Right), 10);
assert!(handled);
assert!(app.inspect_full_diff);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert!(handled);
assert!(!app.inspect_full_diff);
assert_eq!(app.mode, Mode::Detail); }
#[test]
fn test_fzf_missing_flow() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_fzf_missing.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.force_fzf_missing = Some(true);
app.mode = Mode::Normal;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('a')), 10);
assert!(handled);
assert!(!app.pending_fzf);
assert_eq!(app.mode, Mode::Adding);
assert!(app.error_message.is_none());
let handled_dismiss = crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert!(handled_dismiss);
assert_eq!(app.mode, Mode::Normal);
let handled_bulk = crate::input::handle_key(&mut app, key_event(KeyCode::Char('A')), 10);
assert!(handled_bulk);
assert!(!app.pending_bulk_fzf);
assert_eq!(app.mode, Mode::BulkAddInput);
assert!(app.error_message.is_none());
app.force_fzf_missing = Some(false);
app.mode = Mode::Normal;
let handled_add = crate::input::handle_key(&mut app, key_event(KeyCode::Char('a')), 10);
assert!(handled_add);
assert!(app.pending_fzf);
assert!(app.error_message.is_none());
app.mode = Mode::Normal;
let handled_bulk_add = crate::input::handle_key(&mut app, key_event(KeyCode::Char('A')), 10);
assert!(handled_bulk_add);
assert!(app.pending_bulk_fzf);
assert!(app.error_message.is_none());
}
#[test]
fn test_initial_setup_and_migration() {
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let unique_id =
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let temp_dir = std::env::temp_dir().join(format!("gitwig_test_migration_{}", unique_id));
std::fs::create_dir_all(&temp_dir).unwrap();
let temp_path = temp_dir.join("config.toml");
crate::config::save_config(&config, &temp_path).unwrap();
{
let app = App::new(config.clone(), temp_path.clone());
let version_path = temp_dir.join(".version");
assert!(version_path.exists());
let written_version = std::fs::read_to_string(&version_path).unwrap();
assert_eq!(written_version.trim(), env!("CARGO_PKG_VERSION"));
assert_eq!(
app.status_message,
Some(format!("Welcome to Gitwig v{}!", env!("CARGO_PKG_VERSION")))
);
}
{
let app = App::new(config.clone(), temp_path.clone());
assert!(app.status_message.is_none());
}
{
let version_path = temp_dir.join(".version");
std::fs::write(&version_path, "0.1.0").unwrap();
let app = App::new(config.clone(), temp_path.clone());
assert_eq!(
app.status_message,
Some(format!(
"Gitwig updated to v{}! Configuration verified and backed up.",
env!("CARGO_PKG_VERSION")
))
);
let backup_path = temp_path.with_extension("toml.bak");
assert!(backup_path.exists());
let written_version = std::fs::read_to_string(&version_path).unwrap();
assert_eq!(written_version.trim(), env!("CARGO_PKG_VERSION"));
}
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_about_popup_flow() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_about.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
assert_eq!(app.mode, Mode::Normal);
app.open_about();
assert_eq!(app.mode, Mode::About);
app.close_dialog();
assert_eq!(app.mode, Mode::Normal);
app.mode = Mode::Normal;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('v')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::About);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('v')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Normal);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('V')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::About);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Normal);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('v')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::About);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('q')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_tag_fetch_attempt_and_dismiss_flow() {
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_tag_fetch.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
let mock_info = crate::repo::RepoInfo {
branch: Some("main".to_string()),
summary: crate::repo::RepoSummary {
branch: Some("main".to_string()),
staged: 0,
modified: 0,
untracked: 0,
conflicted: 0,
ahead: 0,
behind: 0,
},
changes: crate::repo::WorktreeChanges {
staged: vec![],
unstaged: vec![],
conflicted: vec![],
untracked: vec![],
},
remotes: crate::repo::TabData::Loaded(vec![crate::repo::RemoteInfo {
name: "origin".to_string(),
url: "git@github.com:tareqmy/gitwig.git".to_string(),
push_url: None,
refspecs: vec![],
}]),
..crate::repo::RepoInfo::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("."),
info: Box::new(mock_info),
});
if let Some(crate::repo::ItemDetail::Repo { info, .. }) = &app.current_detail {
assert!(!info.remote_tags_attempted);
}
app.detail_tab = 4;
app.set_default_focus_for_tab();
assert!(!app.fetching);
app.fetch_remote_tags(true);
assert!(app.fetching);
if let Some(crate::repo::ItemDetail::Repo { info, .. }) = &app.current_detail {
assert!(info.remote_tags_attempted);
}
app.tx.send("REMOTE_TAGS_ERR:Failed to get remote tags: network timeout".to_string()).unwrap();
if let Ok(msg) = app.rx.try_recv() {
if let Some(err_msg) = msg.strip_prefix("REMOTE_TAGS_ERR:") {
app.set_error(err_msg.to_string());
app.fetching = false;
}
}
assert!(!app.fetching);
assert_eq!(app.error_message.as_deref(), Some("Failed to get remote tags: network timeout"));
app.set_default_focus_for_tab();
assert!(!app.fetching);
let mouse_event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 10,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, mouse_event);
assert_eq!(app.error_message, None);
}
#[test]
fn test_tag_push_all_confirmation_flow() {
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_tag_push_all.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
let mock_info_single = crate::repo::RepoInfo {
remotes: crate::repo::TabData::Loaded(vec![crate::repo::RemoteInfo {
name: "origin".to_string(),
url: "git@github.com:tareqmy/gitwig.git".to_string(),
push_url: None,
refspecs: vec![],
}]),
..crate::repo::RepoInfo::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("."),
info: Box::new(mock_info_single),
});
app.request_tag_push_all();
assert_eq!(app.mode, Mode::TagPushAllConfirm);
assert_eq!(app.remote_action_target.as_deref(), Some("origin"));
app.cancel_tag_push_all();
assert_eq!(app.mode, Mode::Detail);
assert_eq!(app.remote_action_target, None);
let mock_info_multi = crate::repo::RepoInfo {
remotes: crate::repo::TabData::Loaded(vec![
crate::repo::RemoteInfo {
name: "origin".to_string(),
url: "git@github.com:tareqmy/gitwig.git".to_string(),
push_url: None,
refspecs: vec![],
},
crate::repo::RemoteInfo {
name: "upstream".to_string(),
url: "git@github.com:parent/gitwig.git".to_string(),
push_url: None,
refspecs: vec![],
},
]),
..crate::repo::RepoInfo::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("."),
info: Box::new(mock_info_multi),
});
app.request_tag_push_all();
assert_eq!(app.mode, Mode::RemotePicker);
assert_eq!(app.remote_picker_action, Some(RemotePickerAction::PushAllTags));
app.remote_picker_selection = 1;
app.confirm_remote_picker();
assert_eq!(app.mode, Mode::TagPushAllConfirm);
assert_eq!(app.remote_action_target.as_deref(), Some("upstream"));
app.confirm_tag_push_all();
assert_eq!(app.mode, Mode::Detail);
assert_eq!(app.remote_action_target, None);
}
#[test]
fn test_detail_cache_ttl_behavior() {
let temp_dir = std::env::temp_dir();
let repo_path = temp_dir.join("test_cache_repo");
let _ = std::fs::remove_dir_all(&repo_path);
std::fs::create_dir_all(&repo_path).unwrap();
let config = Config {
items: vec![repo_path.to_string_lossy().to_string()],
poll_interval_ms: 100,
max_commits: 200,
graph_max_commits: 1000,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme_name: "default".to_string(),
theme: ThemeConfig::default(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: true,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
};
let mut app = App::new(config, PathBuf::from(""));
let mock_detail = crate::repo::ItemDetail::Repo {
resolved: repo_path.clone(),
info: Box::new(crate::repo::RepoInfo {
commits: vec![],
files: crate::repo::TabData::Loaded(vec!["file1.txt".to_string()]),
..crate::repo::RepoInfo::default()
}),
};
app.detail_cache.insert(
repo_path.to_string_lossy().to_string(),
DetailCache { detail: mock_detail.clone(), loaded_at: std::time::Instant::now() },
);
app.open_detail();
assert!(app.loading_repo_path.is_none());
assert!(app.current_detail.is_some());
if let Some(crate::repo::ItemDetail::Repo { info, .. }) = &app.current_detail {
assert_eq!(info.files.as_slice(), &["file1.txt".to_string()]);
}
let _ = std::fs::remove_dir_all(&repo_path);
}
#[test]
fn test_tab_ttl_behavior() {
let temp_dir = std::env::temp_dir();
let repo_path = temp_dir.join("test_tab_ttl_repo");
let _ = std::fs::remove_dir_all(&repo_path);
std::fs::create_dir_all(&repo_path).unwrap();
let config = Config {
items: vec![repo_path.to_string_lossy().to_string()],
poll_interval_ms: 100,
max_commits: 200,
graph_max_commits: 1000,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 1, page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme_name: "default".to_string(),
theme: ThemeConfig::default(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: true,
resync_on_tab_change: false,
};
let mut app = App::new(config, PathBuf::from(""));
let mock_info = crate::repo::RepoInfo {
commits: vec![],
files: crate::repo::TabData::Loaded(vec!["file1.txt".to_string()]),
tab_loaded_at: [None; 8],
tab_loading: [false; 8],
..crate::repo::RepoInfo::default()
};
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: repo_path.clone(),
info: Box::new(mock_info),
});
if let Some(crate::repo::ItemDetail::Repo { info, .. }) = &mut app.current_detail {
info.files = crate::repo::TabData::NotLoaded;
}
app.trigger_tab_load_if_needed(1);
if let Some(crate::repo::ItemDetail::Repo { info, .. }) = &app.current_detail {
assert!(info.tab_loading[1]);
assert!(info.files.is_loading());
}
if let Some(crate::repo::ItemDetail::Repo { info, .. }) = &mut app.current_detail {
info.tab_loading[1] = false;
info.tab_loaded_at[1] = Some(std::time::Instant::now() - std::time::Duration::from_secs(5)); info.files = crate::repo::TabData::Loaded(vec!["file_refreshed.txt".to_string()]);
}
app.trigger_tab_load_if_needed(1);
if let Some(crate::repo::ItemDetail::Repo { info, .. }) = &app.current_detail {
assert!(info.tab_loading[1]);
assert!(matches!(info.files, crate::repo::TabData::Loaded(_)));
assert_eq!(info.files.as_slice(), &["file_refreshed.txt".to_string()]);
}
let _ = std::fs::remove_dir_all(&repo_path);
}
#[test]
fn test_commit_popup_mouse_resize() {
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_commit_resize.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::CommitInput;
app.commit_popup_width_pct = 80;
app.commit_popup_height_pct = 45;
app.detail_areas.commit_popup_parent = Some(Rect::new(0, 0, 100, 100));
app.detail_areas.commit_popup = Some(Rect::new(10, 27, 80, 45));
let down_event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 89,
row: 50,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, down_event);
assert_eq!(app.active_drag_splitter, Some(Splitter::CommitPopupWidth));
let drag_event = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 95,
row: 50,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, drag_event);
assert_eq!(app.commit_popup_width_pct, 90);
let up_event = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 95,
row: 50,
modifiers: crossterm::event::KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, up_event);
assert_eq!(app.active_drag_splitter, None);
}
#[test]
fn test_yank_selected_commit_hash() {
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let mut app = App::new(config, PathBuf::from("dummy_path.toml"));
let mut info = repo::RepoInfo::default();
info.commits.push(repo::CommitEntry {
id: "abc1234".to_string(),
oid: "abc123456789".to_string(),
author: "Tester".to_string(),
when: "".to_string(),
date: "".to_string(),
summary: "Initial commit".to_string(),
message: "Initial commit".to_string(),
refs: vec![],
files: vec![],
signature_status: "".to_string(),
});
app.current_detail =
Some(repo::ItemDetail::Repo { resolved: PathBuf::from("/dummy"), info: Box::new(info) });
app.commit_list.selection = 0;
app.detail_tab = 0;
app.yank_selected_commit_hash();
assert!(app.status_message.is_some());
let msg = app.status_message.as_ref().unwrap();
assert!(msg.contains("Copied hash abc1234") || msg.contains("Failed to copy"));
}
#[test]
fn test_cherry_pick_destination_branches() {
let config = Config {
items: vec![],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let mut app = App::new(config, PathBuf::from("dummy_path.toml"));
let mut info = repo::RepoInfo { branch: Some("main".to_string()), ..Default::default() };
info.commits.push(repo::CommitEntry {
id: "abc1234".to_string(),
oid: "abc123456789".to_string(),
author: "Tester".to_string(),
when: "".to_string(),
date: "".to_string(),
summary: "Initial commit".to_string(),
message: "Initial commit".to_string(),
refs: vec![],
files: vec![],
signature_status: "".to_string(),
});
info.local_branches = repo::TabData::Loaded(vec![
repo::BranchInfo {
name: "main".to_string(),
is_head: true,
short_sha: "abc1234".to_string(),
short_message: "msg".to_string(),
},
repo::BranchInfo {
name: "feature-1".to_string(),
is_head: false,
short_sha: "def5678".to_string(),
short_message: "msg2".to_string(),
},
repo::BranchInfo {
name: "feature-2".to_string(),
is_head: false,
short_sha: "9999999".to_string(),
short_message: "msg3".to_string(),
},
]);
app.current_detail =
Some(repo::ItemDetail::Repo { resolved: PathBuf::from("/dummy"), info: Box::new(info) });
app.commit_list.selection = 0;
app.request_cherry_pick();
assert_eq!(app.mode, Mode::CherryPickConfirm);
assert_eq!(app.cherry_pick_dest_branches.len(), 2);
assert_eq!(app.cherry_pick_dest_branches[0], "feature-1");
assert_eq!(app.cherry_pick_dest_branches[1], "feature-2");
assert_eq!(app.cherry_pick_dest_selection, 0);
let event_down = crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Down,
crossterm::event::KeyModifiers::empty(),
);
crate::input::handle_key(&mut app, event_down, 0);
assert_eq!(app.cherry_pick_dest_selection, 1);
let event_down_again = crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Down,
crossterm::event::KeyModifiers::empty(),
);
crate::input::handle_key(&mut app, event_down_again, 0);
assert_eq!(app.cherry_pick_dest_selection, 1);
let event_up = crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Up,
crossterm::event::KeyModifiers::empty(),
);
crate::input::handle_key(&mut app, event_up, 0);
assert_eq!(app.cherry_pick_dest_selection, 0);
}
#[test]
fn test_graph_tab_scrolling() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_graph.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
let info = repo::RepoInfo {
branch: Some("main".to_string()),
graph_lines: repo::TabData::Loaded(
(1..=15)
.map(|i| repo::GraphLine { graph: format!("line {}", i), commit: None })
.collect(),
),
..Default::default()
};
app.current_detail =
Some(repo::ItemDetail::Repo { resolved: PathBuf::from("/dummy"), info: Box::new(info) });
app.mode = Mode::Detail;
app.detail_tab = 2; app.graph_scroll = 0;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert!(handled);
assert_eq!(app.graph_scroll, 1);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::PageDown), 10);
assert!(handled);
assert_eq!(app.graph_scroll, 11);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::End), 10);
assert!(handled);
assert_eq!(app.graph_scroll, 14);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Up), 10);
assert!(handled);
assert_eq!(app.graph_scroll, 13);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::PageUp), 10);
assert!(handled);
assert_eq!(app.graph_scroll, 3);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Home), 10);
assert!(handled);
assert_eq!(app.graph_scroll, 0);
}
#[test]
fn test_commit_popup_custom_keys() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_commit_keys.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::CommitInput;
app.commit_popup.editing = true;
app.commit_popup.maximized = false;
let ctrl_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL);
let handled = crate::input::handle_key(&mut app, ctrl_d, 0);
assert!(handled);
assert!(app.commit_popup.maximized);
let handled = crate::input::handle_key(&mut app, ctrl_d, 0);
assert!(handled);
assert!(!app.commit_popup.maximized);
let ctrl_c = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
let handled = crate::input::handle_key(&mut app, ctrl_c, 0);
assert!(handled);
assert!(!app.commit_popup.editing);
assert_eq!(app.mode, Mode::CommitInput);
let key_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::empty());
let handled = crate::input::handle_key(&mut app, key_d, 0);
assert!(handled);
assert!(app.commit_popup.maximized);
let handled = crate::input::handle_key(&mut app, key_d, 0);
assert!(handled);
assert!(!app.commit_popup.maximized);
let key_e = KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty());
let handled = crate::input::handle_key(&mut app, key_e, 0);
assert!(handled);
assert!(app.commit_popup.editing);
let key_esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
let handled = crate::input::handle_key(&mut app, key_esc, 0);
assert!(handled);
assert_eq!(app.mode, Mode::Detail);
}
#[test]
fn test_settings_panel_organization() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_settings_org.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::Settings;
app.settings_selected_index = 0;
app.settings_editing = false;
app.settings_focus_sidebar = false;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Left), 10);
assert!(handled);
assert!(app.settings_focus_sidebar);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert!(handled);
assert_eq!(app.settings_selected_index, 1);
assert!(app.settings_focus_sidebar);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Right), 10);
assert!(handled);
assert!(!app.settings_focus_sidebar);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('3')), 10);
assert!(handled);
assert_eq!(app.settings_selected_index, 11);
assert!(!app.settings_focus_sidebar);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('4')), 10);
assert!(handled);
assert_eq!(app.settings_selected_index, 3);
assert!(!app.settings_focus_sidebar);
}
#[test]
fn test_help_popup_flow() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_help.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
assert_eq!(app.mode, Mode::Normal);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Char('?')), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Help);
assert_eq!(app.help_scroll, 0);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert!(handled);
assert_eq!(app.help_scroll, 1);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Up), 10);
assert!(handled);
assert_eq!(app.help_scroll, 0);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::End), 10);
assert!(handled);
let max_scroll = app.help_scroll;
assert!(max_scroll > 0);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Home), 10);
assert!(handled);
assert_eq!(app.help_scroll, 0);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::PageDown), 10);
assert!(handled);
assert_eq!(app.help_scroll, 10);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::PageUp), 10);
assert!(handled);
assert_eq!(app.help_scroll, 0);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn test_detail_help_popup_flow() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 0,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_detail_help.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.mode = Mode::DetailHelp;
app.help_scroll = 0;
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Down), 10);
assert!(handled);
assert_eq!(app.help_scroll, 1);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Up), 10);
assert!(handled);
assert_eq!(app.help_scroll, 0);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::End), 10);
assert!(handled);
let max_scroll = app.help_scroll;
assert!(max_scroll > 0);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Home), 10);
assert!(handled);
assert_eq!(app.help_scroll, 0);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::PageDown), 10);
assert!(handled);
assert_eq!(app.help_scroll, 10);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::PageUp), 10);
assert!(handled);
assert_eq!(app.help_scroll, 0);
let handled = crate::input::handle_key(&mut app, key_event(KeyCode::Esc), 10);
assert!(handled);
assert_eq!(app.mode, Mode::Detail);
}
#[test]
fn test_max_commits_limit_setting() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["a_repo".to_string()],
poll_interval_ms: 100,
max_commits: 45, page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_max_commits.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path.clone());
assert_eq!(app.commit_list.limit, 45);
app.detail_cache.clear();
app.detail_focus = DetailSection::Commits;
let _ = crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert_eq!(app.commit_list.limit, 45);
app.queue.push(crate::queue::InternalEvent::LoadMoreCommits);
app.drain_queue();
assert_eq!(app.commit_list.limit, 90);
}
#[test]
fn test_file_history_view_flow() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec![".".to_string()],
poll_interval_ms: 100,
max_commits: 10,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_file_history.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
app.detail_tab = 1; app.detail_focus = DetailSection::Files;
app.file_tree.visible_files.push(crate::app::FileTreeItem {
name: "Cargo.toml".to_string(),
full_path: "Cargo.toml".to_string(),
is_dir: false,
depth: 0,
is_expanded: false,
});
app.file_tree.file_list_selection = 0;
let mock_info = crate::repo::RepoInfo { ..crate::repo::RepoInfo::default() };
app.current_detail = Some(crate::repo::ItemDetail::Repo {
resolved: std::path::PathBuf::from("."),
info: Box::new(mock_info),
});
app.open_file_history();
assert_eq!(app.mode, Mode::FileHistory);
assert_eq!(app.file_history_path, "Cargo.toml");
assert_eq!(app.file_history_selection, 0);
assert_eq!(app.file_history_focus, 0);
let consumed = crate::tabs::FileHistoryTab::handle_event(&mut app, key_event(KeyCode::Tab));
assert!(consumed);
assert_eq!(app.file_history_focus, 1);
let consumed = crate::tabs::FileHistoryTab::handle_event(&mut app, key_event(KeyCode::Tab));
assert!(consumed);
assert_eq!(app.file_history_focus, 0);
let consumed = crate::tabs::FileHistoryTab::handle_event(&mut app, key_event(KeyCode::Esc));
assert!(consumed);
assert_eq!(app.mode, Mode::Detail);
}
#[test]
fn test_repository_labels_flow() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let key_event = |code: KeyCode| KeyEvent::new(code, KeyModifiers::empty());
let config = Config {
items: vec!["/path/to/repo1".to_string(), "/path/to/repo2".to_string()],
poll_interval_ms: 100,
max_commits: 10,
page_size: 10,
sort_by: SortOrder::Custom,
visits: HashMap::new(),
labels: std::collections::HashMap::new(),
sort_reverse: false,
pinned: std::collections::HashSet::new(),
theme: ThemeConfig::default(),
theme_name: "default".to_string(),
fzf: FzfConfig::default(),
git_app: "gitui".to_string(),
compatibility_mode: false,
detail_cache_ttl_secs: 30,
enable_commit_signatures: false,
tab_ttl_secs: 60,
resync_on_tab_change: false,
graph_max_commits: 1000,
};
let temp_path = std::env::temp_dir().join("gitwig_test_config_labels.toml");
let _guard = TestFileGuard { path: temp_path.clone() };
let mut app = App::new(config, temp_path);
assert_eq!(app.mode, Mode::Normal);
assert_eq!(app.selected_index, 0);
let _ = crate::input::handle_key(&mut app, key_event(KeyCode::Char('l')), 10);
assert_eq!(app.mode, Mode::LabelInput);
assert_eq!(app.input_buffer, "");
for c in "work, rust".chars() {
let _ = crate::input::handle_key(&mut app, key_event(KeyCode::Char(c)), 10);
}
assert_eq!(app.input_buffer, "work, rust");
let _ = crate::input::handle_key(&mut app, key_event(KeyCode::Enter), 10);
assert_eq!(app.mode, Mode::Normal);
let saved_labels = app.config.labels.get("/path/to/repo1").unwrap();
assert_eq!(saved_labels, &vec!["work".to_string(), "rust".to_string()]);
app.repo_search_query = Some("rust".to_string());
let filtered = app.get_filtered_items();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].1, &"/path/to/repo1".to_string());
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
app.repo_search_query = None;
let rect = ratatui::layout::Rect::new(0, 0, 50, 4);
app.main_areas = vec![rect];
let click_event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 19,
row: 1,
modifiers: KeyModifiers::empty(),
};
crate::mouse::handle_mouse(&mut app, click_event);
assert_eq!(app.repo_search_query.as_deref(), Some("rust"));
}