use crate::common::git_test_helper::{DirGuard, GitTestRepo};
use crate::common::harness::EditorTestHarness;
use crate::common::tracing::init_tracing_from_env;
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::Config;
fn trigger_git_grep(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Git Grep").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
fn trigger_git_find_file(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness.type_text("Git Find File").unwrap();
harness.wait_for_screen_contains("Git Find File").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.wait_for_screen_contains("Find file:").unwrap();
}
#[test]
fn test_git_grep_shows_results() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_grep(&mut harness);
harness.assert_screen_contains("Git grep: ");
harness.type_text("config").unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains(".yml:") || screen.contains(".md:") || screen.contains(".rs:")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Git grep screen:\n{screen}");
assert!(
screen.contains("src/") || screen.contains("Config") || screen.contains("config"),
"Should show grep results"
);
}
#[test]
fn test_git_grep_interactive_updates() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_grep(&mut harness);
harness.type_text("Config").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("src/"))
.unwrap();
let screen_config = harness.screen_to_string();
for _ in 0..6 {
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap();
harness.sleep(std::time::Duration::from_millis(10));
}
harness.render().unwrap();
harness.type_text("println").unwrap();
harness
.wait_until(|h| {
let s = h.screen_to_string();
s.contains("println") || s.contains("main.rs")
})
.unwrap();
let screen_println = harness.screen_to_string();
println!("After 'Config' query:\n{screen_config}");
println!("After 'println' query:\n{screen_println}");
assert!(
screen_config.contains("Config") || screen_config.contains("src/"),
"Config search should show results"
);
}
#[test]
fn test_git_grep_selection_navigation() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_grep(&mut harness);
harness.type_text("config").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("src/"))
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
let screen_after_down = harness.screen_to_string();
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
let screen_after_up = harness.screen_to_string();
println!("After down:\n{screen_after_down}");
println!("After up:\n{screen_after_up}");
assert!(screen_after_down.contains("Git grep:"));
assert!(screen_after_up.contains("Git grep:"));
}
#[test]
fn test_git_grep_confirm_jumps_to_location() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_grep(&mut harness);
harness.type_text("Hello, world").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("main.rs"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.sleep(std::time::Duration::from_millis(200));
harness.render().unwrap();
let screen = harness.screen_to_string();
println!("After confirming grep result:\n{screen}");
harness.assert_screen_not_contains("Git grep:");
let has_file_content = screen.contains("Hello, world")
|| screen.contains("fn main")
|| screen.contains("println")
|| screen.contains("main.rs");
if !has_file_content {
println!(
"Note: File content not visible (likely due to relative path in test environment)"
);
}
}
#[test]
fn test_git_grep_cancel() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_grep(&mut harness);
harness.assert_screen_contains("Git grep: ");
harness.type_text("config").unwrap();
harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.assert_screen_not_contains("Git grep: ");
}
#[test]
fn test_git_find_file_shows_results() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_find_file(&mut harness);
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Find file:")
&& (screen.contains("src/")
|| screen.contains(".rs")
|| screen.contains("Cargo.toml"))
})
.unwrap();
let screen = harness.screen_to_string();
println!("Git find file screen:\n{screen}");
assert!(
screen.contains(".rs") || screen.contains("Cargo") || screen.contains("README"),
"Should show project files"
);
}
#[test]
fn test_git_find_file_interactive_filtering() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_find_file(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("src/"))
.unwrap();
harness.type_text("main").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("main"))
.unwrap();
let screen_main = harness.screen_to_string();
println!("After filtering 'main':\n{screen_main}");
assert!(
screen_main.contains("main.rs") || screen_main.contains("main"),
"Should filter to show main.rs"
);
for _ in 0..4 {
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap();
harness.sleep(std::time::Duration::from_millis(10));
}
harness.type_text("lib").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("lib"))
.unwrap();
let screen_lib = harness.screen_to_string();
println!("After filtering 'lib':\n{screen_lib}");
assert!(
screen_lib.contains("lib.rs") || screen_lib.contains("lib"),
"Should filter to show lib.rs"
);
}
#[test]
fn test_git_find_file_selection_navigation() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_find_file(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("src/"))
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
let screen = harness.screen_to_string();
println!("After navigation:\n{screen}");
assert!(screen.contains("Find file:"));
}
#[test]
#[cfg_attr(windows, ignore)] fn test_git_find_file_confirm_opens_file() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_find_file(&mut harness);
harness.type_text("main.rs").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("main.rs"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
!screen.contains("Find file:")
})
.unwrap();
let screen = harness.screen_to_string();
println!("After confirming file:\n{screen}");
harness.assert_screen_not_contains("Find file:");
let has_file_content =
screen.contains("fn main()") || screen.contains("println") || screen.contains("Hello");
if !has_file_content {
println!(
"Note: File content not visible (likely due to relative path in test environment)"
);
}
}
#[test]
fn test_git_grep_scrolling_many_results() {
let repo = GitTestRepo::new();
repo.setup_many_files(50);
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_grep(&mut harness);
harness.type_text("Searchable").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("file"))
.unwrap();
for _ in 0..10 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness.sleep(std::time::Duration::from_millis(20));
}
let screen = harness.screen_to_string();
println!("After scrolling down:\n{screen}");
assert!(screen.contains("Git grep:"));
assert!(screen.contains("file") || screen.contains("Searchable"));
}
#[test]
fn test_git_find_file_scrolling_many_files() {
let repo = GitTestRepo::new();
repo.setup_many_files(50);
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_find_file(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("file"))
.unwrap();
for _ in 0..15 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness.sleep(std::time::Duration::from_millis(20));
}
for _ in 0..5 {
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness.sleep(std::time::Duration::from_millis(20));
}
let screen = harness.screen_to_string();
println!("After scrolling:\n{screen}");
assert!(screen.contains("Find file:"));
}
#[test]
fn test_git_commands_via_command_palette() {
init_tracing_from_env();
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("Command: ");
harness.type_text("Git Grep").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Git grep:"))
.unwrap();
}
#[test]
fn test_git_grep_opens_correct_file_and_jumps_to_line() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
let initial_content = harness.get_buffer_content().unwrap();
assert!(
initial_content.is_empty() || initial_content == "\n",
"Should start with empty buffer"
);
trigger_git_grep(&mut harness);
harness.type_text("println").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("main.rs"))
.unwrap();
let screen_before = harness.screen_to_string();
println!("Screen with results:\n{screen_before}");
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let content = h.get_buffer_content().unwrap();
!content.is_empty() && content != "\n" && content.contains("println")
})
.unwrap();
let buffer_content = harness.get_buffer_content().unwrap();
println!("Buffer content after selection:\n{buffer_content}");
assert!(
buffer_content.contains("println"),
"BUG: Buffer does not contain expected file content. Expected 'println' in buffer. Buffer: {buffer_content:?}"
);
let cursor_pos = harness.cursor_position();
println!("Cursor position: {cursor_pos}");
assert!(
cursor_pos > 0,
"BUG: Cursor is at position 0! It should have jumped to the match line. Position: {cursor_pos}"
);
let screen_after = harness.screen_to_string();
println!("Screen after selection:\n{screen_after}");
assert!(
screen_after.contains("fn main") || screen_after.contains("println"),
"BUG: Screen does not show file content after selection"
);
}
#[test]
fn test_git_find_file_actually_opens_file() {
init_tracing_from_env();
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
let initial_content = harness.get_buffer_content().unwrap();
assert!(
initial_content.is_empty() || initial_content == "\n",
"Should start with empty buffer"
);
trigger_git_find_file(&mut harness);
harness.wait_for_prompt().unwrap();
harness.type_text("lib.rs").unwrap();
harness
.wait_until(|h| {
let s = h.screen_to_string();
let lines: Vec<&str> = s.lines().collect();
lines
.iter()
.take(lines.len().saturating_sub(1))
.any(|line| line.contains("src/"))
})
.unwrap();
let screen_before = harness.screen_to_string();
println!("Screen with file list:\n{screen_before}");
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let content = h.get_buffer_content().unwrap_or_default();
!content.is_empty() && content != "\n"
})
.unwrap();
let buffer_content = harness.get_buffer_content().unwrap();
println!("Buffer content after selection:\n{buffer_content}");
assert!(
!buffer_content.is_empty() && buffer_content != "\n",
"BUG: Buffer is still empty! File lib.rs was not opened. Buffer: {buffer_content:?}"
);
assert!(
buffer_content.contains("pub struct Config") || buffer_content.contains("impl Default"),
"BUG: Buffer does not contain lib.rs content. Expected 'Config' or 'impl Default'. Buffer: {buffer_content:?}"
);
let screen_after = harness.screen_to_string();
println!("Screen after selection:\n{screen_after}");
assert!(
screen_after.contains("Config") || screen_after.contains("pub struct"),
"BUG: Screen does not show lib.rs content after selection. Screen:\n{screen_after}"
);
harness.assert_screen_not_contains("Find file:");
}
#[test]
fn test_git_grep_cursor_position_accuracy() {
let repo = GitTestRepo::new();
repo.create_file(
"test.txt",
"Line 1\nLine 2\nLine 3 with MARKER\nLine 4\nLine 5\n",
);
repo.git_add(&["test.txt"]);
repo.git_commit("Add test file");
repo.setup_git_plugins();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_grep(&mut harness);
harness.type_text("MARKER").unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("test.txt"))
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.wait_until(|h| {
let content = h.get_buffer_content().unwrap();
content.contains("MARKER")
})
.unwrap();
let buffer_content = harness.get_buffer_content().unwrap();
println!("Buffer content:\n{buffer_content}");
let cursor_pos = harness.cursor_position();
println!("Cursor position: {cursor_pos}");
assert!(
cursor_pos >= 14,
"BUG: Cursor should be at line 3 (position >= 14), but is at position {cursor_pos}"
);
let screen = harness.screen_to_string();
assert!(
screen.contains("MARKER"),
"BUG: Screen should show the line with MARKER"
);
}
fn trigger_git_log(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Git Log").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
#[test]
fn test_git_log_shows_commits() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_log_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_log(&mut harness);
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Commits:") && screen.contains("Initial commit")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Git log screen:\n{screen}");
assert!(screen.contains("Commits:"), "Should show Commits: header");
}
#[test]
fn test_git_log_cursor_navigation() {
let repo = GitTestRepo::new();
repo.create_file("file1.txt", "Content 1");
repo.git_add(&["file1.txt"]);
repo.git_commit("First commit");
repo.create_file("file2.txt", "Content 2");
repo.git_add(&["file2.txt"]);
repo.git_commit("Second commit");
repo.create_file("file3.txt", "Content 3");
repo.git_add(&["file3.txt"]);
repo.git_commit("Third commit");
repo.setup_git_log_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_log(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("Commits:"))
.unwrap();
harness
.send_key(KeyCode::Char('j'), KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness
.send_key(KeyCode::Char('k'), KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
let screen = harness.screen_to_string();
println!("After navigation:\n{screen}");
assert!(screen.contains("Commits:"));
}
#[test]
fn test_git_log_show_commit_detail() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_log_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_log(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("Commits:"))
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Author:") && screen.contains("Date:")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Commit detail screen:\n{screen}");
}
#[test]
fn test_git_log_back_from_commit_detail() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_log_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_log(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("Commits:"))
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Author:"))
.unwrap();
let screen_detail = harness.screen_to_string();
println!("Commit detail:\n{screen_detail}");
harness
.send_key(KeyCode::Char('q'), KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Commits:"))
.unwrap();
let screen_log = harness.screen_to_string();
println!("Back to git log:\n{screen_log}");
}
#[test]
fn test_git_log_close() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_log_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_log(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("Commits:"))
.unwrap();
let screen_before = harness.screen_to_string();
assert!(screen_before.contains("Commits:"));
harness
.send_key(KeyCode::Char('q'), KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness.sleep(std::time::Duration::from_millis(100));
harness.render().unwrap();
let screen_after = harness.screen_to_string();
println!("After closing:\n{screen_after}");
harness.assert_screen_not_contains("Commits:");
}
#[test]
fn test_git_log_diff_coloring() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_log_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_log(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("Commits:"))
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Author:")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Commit detail with diff:\n{screen}");
assert!(
screen.contains("Author:") || screen.contains("Date:"),
"Should show commit info"
);
}
#[test]
fn test_git_log_open_different_commits_sequentially() {
let repo = GitTestRepo::new();
repo.create_file("file1.txt", "Content for first file");
repo.git_add(&["file1.txt"]);
repo.git_commit("FIRST_UNIQUE_COMMIT_AAA");
repo.create_file("file2.txt", "Content for second file");
repo.git_add(&["file2.txt"]);
repo.git_commit("SECOND_UNIQUE_COMMIT_BBB");
repo.create_file("file3.txt", "Content for third file");
repo.git_add(&["file3.txt"]);
repo.git_commit("THIRD_UNIQUE_COMMIT_CCC");
repo.setup_git_log_plugin();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
trigger_git_log(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("Commits:"))
.unwrap();
let screen_log = harness.screen_to_string();
println!("Git log with commits:\n{screen_log}");
assert!(
screen_log.contains("THIRD_UNIQUE_COMMIT_CCC"),
"Should show third commit"
);
assert!(
screen_log.contains("SECOND_UNIQUE_COMMIT_BBB"),
"Should show second commit"
);
assert!(
screen_log.contains("FIRST_UNIQUE_COMMIT_AAA"),
"Should show first commit"
);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Author:") && screen.contains("THIRD_UNIQUE_COMMIT_CCC")
})
.unwrap();
let screen_first_detail = harness.screen_to_string();
println!("First commit detail (should be THIRD):\n{screen_first_detail}");
harness
.send_key(KeyCode::Char('q'), KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("Commits:"))
.unwrap();
let screen_back_to_log = harness.screen_to_string();
println!("Back to git log:\n{screen_back_to_log}");
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
let screen_after_nav = harness.screen_to_string();
println!("After navigating down:\n{screen_after_nav}");
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Author:")
})
.unwrap();
let screen_second_detail = harness.screen_to_string();
println!("Second commit detail (should be SECOND):\n{screen_second_detail}");
assert!(
screen_second_detail.contains("SECOND_UNIQUE_COMMIT_BBB"),
"BUG: After navigating to a different commit and pressing Enter, it should open SECOND_UNIQUE_COMMIT_BBB, but got:\n{screen_second_detail}"
);
assert!(
!screen_second_detail.contains("THIRD_UNIQUE_COMMIT_CCC"),
"BUG: Should NOT show THIRD commit when SECOND was selected:\n{screen_second_detail}"
);
}
fn trigger_git_blame(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Git Blame").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_blame_shows_blocks_with_headers() {
init_tracing_from_env();
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_blame_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("src/main.rs");
harness.open_file(&file_path).unwrap();
harness
.wait_until(|h| {
let content = h.get_buffer_content().unwrap();
content.contains("fn main")
})
.unwrap();
trigger_git_blame(&mut harness);
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("──") && screen.contains("Initial commit")
})
.unwrap();
let screen = harness.screen_to_string();
println!("Git blame screen:\n{screen}");
assert!(screen.contains("──"), "Should show block header separator");
assert!(
screen.contains("Initial commit"),
"Should show commit summary in header"
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_blame_cursor_navigation() {
let repo = GitTestRepo::new();
repo.create_file("test.txt", "Line 1\nLine 2\n");
repo.git_add(&["test.txt"]);
repo.git_commit("First commit");
repo.create_file("test.txt", "Line 1\nLine 2\nLine 3\nLine 4\n");
repo.git_add(&["test.txt"]);
repo.git_commit("Second commit");
repo.setup_git_blame_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("test.txt");
harness.open_file(&file_path).unwrap();
harness
.wait_until(|h| h.get_buffer_content().unwrap().contains("Line 1"))
.unwrap();
trigger_git_blame(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("──"))
.unwrap();
harness
.send_key(KeyCode::Char('j'), KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness
.send_key(KeyCode::Char('k'), KeyModifiers::NONE)
.unwrap();
harness.process_async_and_render().unwrap();
let screen = harness.screen_to_string();
println!("After navigation:\n{screen}");
assert!(screen.contains("──"));
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_blame_close() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_blame_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("src/main.rs");
harness.open_file(&file_path).unwrap();
harness
.wait_until(|h| h.get_buffer_content().unwrap().contains("fn main"))
.unwrap();
trigger_git_blame(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("──"))
.unwrap();
let screen_before = harness.screen_to_string();
assert!(screen_before.contains("──"));
harness
.send_key(KeyCode::Char('q'), KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("fn main") && !screen.contains("──")
})
.unwrap();
let screen_after = harness.screen_to_string();
println!("After closing:\n{screen_after}");
harness.assert_screen_not_contains("──");
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_blame_go_back_in_history() {
let repo = GitTestRepo::new();
repo.create_file("test.txt", "Original line 1\nOriginal line 2\n");
repo.git_add(&["test.txt"]);
repo.git_commit("First commit");
repo.create_file("test.txt", "Original line 1\nModified line 2\nNew line 3\n");
repo.git_add(&["test.txt"]);
repo.git_commit("Second commit");
repo.setup_git_blame_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("test.txt");
harness.open_file(&file_path).unwrap();
harness
.wait_until(|h| h.get_buffer_content().unwrap().contains("line"))
.unwrap();
trigger_git_blame(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("──"))
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
let screen_before = harness.screen_to_string();
println!("Before pressing 'b':\n{screen_before}");
harness
.send_key(KeyCode::Char('b'), KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("──") && (screen.contains("depth:") || screen.contains("First commit"))
})
.unwrap();
let screen_after = harness.screen_to_string();
println!("After pressing 'b':\n{screen_after}");
assert!(
screen_after.contains("──"),
"Should still show blame block headers after going back"
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_blame_shows_different_commits() {
let repo = GitTestRepo::new();
repo.create_file("multi.txt", "Line from first commit\n");
repo.git_add(&["multi.txt"]);
repo.git_commit("First commit");
repo.create_file(
"multi.txt",
"Line from first commit\nLine from second commit\n",
);
repo.git_add(&["multi.txt"]);
repo.git_commit("Second commit");
repo.setup_git_blame_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("multi.txt");
harness.open_file(&file_path).unwrap();
harness
.wait_until(|h| h.get_buffer_content().unwrap().contains("Line from"))
.unwrap();
trigger_git_blame(&mut harness);
harness
.wait_until(|h| {
let screen = h.screen_to_string();
let header_count = screen.matches("──").count();
header_count >= 2
})
.unwrap();
let screen = harness.screen_to_string();
println!("Git blame with multiple commits:\n{screen}");
assert!(
screen.contains("First commit") || screen.contains("Second commit"),
"Should show commit summaries"
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_blame_line_numbers_correct() {
let repo = GitTestRepo::new();
repo.create_file(
"numbered.txt",
"Line 1 from first commit\nLine 2 from first commit\n",
);
repo.git_add(&["numbered.txt"]);
repo.git_commit("First commit");
repo.create_file("numbered.txt", "Line 1 from first commit\nLine 2 from first commit\nLine 3 from second commit\nLine 4 from second commit\n");
repo.git_add(&["numbered.txt"]);
repo.git_commit("Second commit");
repo.setup_git_blame_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("numbered.txt");
harness.open_file(&file_path).unwrap();
harness
.wait_until(|h| h.get_buffer_content().unwrap().contains("Line 1"))
.unwrap();
trigger_git_blame(&mut harness);
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.matches("──").count() >= 2
})
.unwrap();
let screen = harness.screen_to_string();
println!("Git blame with line numbers:\n{screen}");
assert!(
screen.contains("1")
&& screen.contains("2")
&& screen.contains("3")
&& screen.contains("4"),
"Should show line numbers 1-4 for content lines"
);
let total_lines = screen.lines().count();
let header_count = screen.matches("──").count();
assert!(
total_lines >= 6,
"Should have at least 6 lines (4 content + 2 headers), got {total_lines}"
);
assert!(
header_count >= 2,
"Should have at least 2 header lines, got {header_count}"
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_blame_scroll_to_bottom() {
let repo = GitTestRepo::new();
let mut content = String::new();
for i in 1..=50 {
content.push_str(&format!("Line {} content\n", i));
}
repo.create_file("scrolltest.txt", &content);
repo.git_add(&["scrolltest.txt"]);
repo.git_commit("Add scrollable file");
repo.setup_git_blame_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
30, Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("scrolltest.txt");
harness.open_file(&file_path).unwrap();
harness
.wait_until(|h| h.get_buffer_content().unwrap().contains("Line 1"))
.unwrap();
trigger_git_blame(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("──"))
.unwrap();
let screen_top = harness.screen_to_string();
println!("Git blame at top:\n{screen_top}");
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.process_async_and_render().unwrap();
harness.render().unwrap();
let screen_bottom = harness.screen_to_string();
println!("Git blame at bottom:\n{screen_bottom}");
assert!(
screen_bottom.contains("Line 50")
|| screen_bottom.contains("Line 49")
|| screen_bottom.contains("Line 48"),
"Should show last lines of file after scrolling to bottom"
);
assert!(
screen_bottom.contains("content"),
"Should still show file content properly after scrolling"
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_blame_scroll_with_many_virtual_lines() {
use std::time::Duration;
let repo = GitTestRepo::new();
let content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n";
repo.create_file("scroll_many_virtual.txt", content);
repo.git_add(&["scroll_many_virtual.txt"]);
repo.git_commit("Add file with few lines");
repo.setup_git_blame_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
80,
20,
Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("scroll_many_virtual.txt");
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
trigger_git_blame(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("──"))
.unwrap();
for _ in 0..40 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
harness.sleep(Duration::from_millis(5));
}
harness.render().unwrap();
let screen = harness.screen_to_string();
println!("Blame after scrolling with virtual lines:\n{screen}");
assert!(
screen.contains("Line 5") || screen.contains("Line 4"),
"Should see tail lines after scrolling with many virtual lines"
);
}
fn trigger_test_view_marker(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.type_text("Test View Marker").unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
fn trigger_test_view_marker_many_virtual_lines(harness: &mut EditorTestHarness) {
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.type_text("Test View Marker (Many Virtual Lines)")
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
#[test]
fn test_view_transform_header_at_byte_zero() {
init_tracing_from_env();
let repo = GitTestRepo::new();
repo.create_file("test.txt", "placeholder");
repo.git_add(&["test.txt"]);
repo.git_commit("Initial commit");
repo.setup_test_view_marker_plugin();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("test.txt");
harness.open_file(&file_path).unwrap();
harness
.wait_until(|h| !h.get_buffer_content().unwrap().is_empty())
.unwrap();
trigger_test_view_marker(&mut harness);
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Test view marker active") || screen.contains("*test-view-marker*")
})
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("HEADER AT BYTE 0")
})
.unwrap();
let screen_after = harness.screen_to_string();
println!("Screen after view marker:\n{screen_after}");
}
#[test]
fn test_view_transform_scroll_with_many_virtual_lines() {
init_tracing_from_env();
let repo = GitTestRepo::new();
repo.create_file("test.txt", "placeholder");
repo.git_add(&["test.txt"]);
repo.git_commit("Initial commit");
repo.setup_test_view_marker_plugin();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
20,
Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("test.txt");
harness.open_file(&file_path).unwrap();
harness
.wait_until(|h| !h.get_buffer_content().unwrap().is_empty())
.unwrap();
trigger_test_view_marker_many_virtual_lines(&mut harness);
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("Line 1") || screen.contains("Line 2") || screen.contains("Line 3")
})
.unwrap();
let initial_screen = harness.screen_to_string();
println!("Initial screen (auto-scrolled to cursor):\n{initial_screen}");
for _ in 0..150 {
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.process_async_and_render().unwrap();
}
harness.render().unwrap();
let screen_after_up = harness.screen_to_string();
println!("Screen after scrolling up through virtual lines:\n{screen_after_up}");
assert!(
screen_after_up.contains("HEADER AT BYTE 0") || screen_after_up.contains("Virtual pad"),
"Scrolling up should reveal header or virtual pad lines"
);
}
#[test]
fn test_view_transform_scroll_with_single_virtual_line() {
init_tracing_from_env();
let repo = GitTestRepo::new();
repo.create_file("test.txt", "placeholder");
repo.git_add(&["test.txt"]);
repo.git_commit("Initial commit");
repo.setup_test_view_marker_plugin();
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
20,
Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("test.txt");
harness.open_file(&file_path).unwrap();
harness
.wait_until(|h| !h.get_buffer_content().unwrap().is_empty())
.unwrap();
trigger_test_view_marker(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("HEADER AT BYTE 0"))
.unwrap();
let screen = harness.screen_to_string();
println!("Screen with single virtual header line:\n{screen}");
assert!(
screen.contains("Line 1") || screen.contains("Line 2"),
"Source content should be visible below header"
);
let content_lines: Vec<&str> = screen
.lines()
.filter(|l| l.contains("│") && !l.contains("~"))
.collect();
println!("Content lines: {content_lines:?}");
assert!(
content_lines.len() >= 4,
"Expected at least 4 content lines"
);
assert!(
content_lines[0].contains("│ == HEADER AT BYTE 0 =="),
"Line 0 should be header without line number. Got: {}",
content_lines[0]
);
assert!(
content_lines[1].contains("1 │ Line 1"),
"Line 1 should show '1 │ Line 1'. Got: {}",
content_lines[1]
);
assert!(
content_lines[2].contains("2 │ Line 2"),
"Line 2 should show '2 │ Line 2'. Got: {}",
content_lines[2]
);
assert!(
content_lines[3].contains("3 │ Line 3"),
"Line 3 should show '3 │ Line 3'. Got: {}",
content_lines[3]
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn test_git_blame_original_buffer_not_decorated() {
let repo = GitTestRepo::new();
repo.setup_typical_project();
repo.setup_git_blame_plugin();
let original_dir = repo.change_to_repo_dir();
let _guard = DirGuard::new(original_dir);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
let file_path = repo.path.join("src/main.rs");
harness.open_file(&file_path).unwrap();
harness
.wait_until(|h| h.get_buffer_content().unwrap().contains("fn main"))
.unwrap();
let screen_before_blame = harness.screen_to_string();
println!("Screen before blame:\n{screen_before_blame}");
assert!(
!screen_before_blame.contains("──"),
"Original file should NOT have blame headers before opening blame"
);
trigger_git_blame(&mut harness);
harness
.wait_until(|h| h.screen_to_string().contains("──"))
.unwrap();
let screen_with_blame = harness.screen_to_string();
println!("Screen with blame:\n{screen_with_blame}");
assert!(
screen_with_blame.contains("──"),
"Blame view should have headers"
);
harness
.send_key(KeyCode::Char('q'), KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| {
let screen = h.screen_to_string();
screen.contains("fn main") && !screen.contains("──")
})
.unwrap();
let screen_after_close = harness.screen_to_string();
println!("Screen after closing blame:\n{screen_after_close}");
assert!(
!screen_after_close.contains("──"),
"Original file should NOT have blame headers after closing blame"
);
}