use crate::common::git_test_helper::GitTestRepo;
use crate::common::harness::{copy_plugin, copy_plugin_lib, EditorTestHarness};
use crate::common::tracing::init_tracing_from_env;
use crossterm::event::{KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use fresh::config::Config;
use std::fs;
use tracing::info;
fn setup_audit_mode_plugin(repo: &GitTestRepo) {
let plugins_dir = repo.path.join("plugins");
fs::create_dir_all(&plugins_dir).expect("Failed to create plugins directory");
copy_plugin(&plugins_dir, "audit_mode");
copy_plugin_lib(&plugins_dir);
}
fn is_in_diff_view(screen: &str) -> bool {
let has_diff_tab = screen.contains("*Diff:");
let has_old_header = screen.contains("OLD (HEAD)");
let has_full_status = screen.contains("Side-by-side diff:");
(has_diff_tab && has_old_header) || has_full_status
}
fn open_side_by_side_diff(harness: &mut EditorTestHarness) {
info!("open_side_by_side_diff: sending Ctrl+p");
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
info!("open_side_by_side_diff: waiting for prompt");
harness.wait_for_prompt().unwrap();
info!("open_side_by_side_diff: typing 'Side-by-Side Diff'");
harness.type_text("Side-by-Side Diff").unwrap();
harness.render().unwrap();
info!("open_side_by_side_diff: sending Enter");
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
info!("open_side_by_side_diff: waiting for prompt closed");
harness.wait_for_prompt_closed().unwrap();
info!("open_side_by_side_diff: prompt closed, waiting for diff view to load");
harness
.wait_until(|h| {
let screen = h.screen_to_string();
if screen.contains("TypeError")
|| screen.contains("Error:")
|| screen.contains("Failed")
{
panic!("Error loading side-by-side diff. Screen:\n{}", screen);
}
let still_loading = screen.contains("Loading side-by-side diff");
!still_loading && is_in_diff_view(&screen)
})
.unwrap();
info!("open_side_by_side_diff: diff view loaded");
}
fn create_repo_with_varied_lines(repo: &GitTestRepo) {
let file_path = repo.path.join("test.rs");
let original_content = r#"
short
this is a medium length line for testing
this is a very long line that extends well beyond the visible viewport width and requires horizontal scrolling to see the entire content of this particular line which is intentionally made very long for testing purposes
another short
medium line here with some content
"#;
fs::write(&file_path, original_content).expect("Failed to create file");
repo.git_add_all();
repo.git_commit("Initial commit");
let modified_content = r#"
short_modified
this is a MODIFIED medium length line for testing
this is a very long MODIFIED line that extends well beyond the visible viewport width and requires horizontal scrolling to see the entire content of this particular line which is intentionally made very long for testing purposes and now even longer
another short MOD
medium line here with some MODIFIED content
added new line
"#;
fs::write(&file_path, modified_content).expect("Failed to modify file");
}
fn create_repo_with_long_file(repo: &GitTestRepo) {
let file_path = repo.path.join("long.rs");
let mut original = String::new();
for i in 0..100 {
original.push_str(&format!("// Original line {}\n", i));
}
fs::write(&file_path, &original).expect("Failed to create file");
repo.git_add_all();
repo.git_commit("Initial commit");
let mut modified = String::new();
for i in 0..100 {
if i == 10 || i == 50 || i == 90 {
modified.push_str(&format!("// MODIFIED line {} with extra content\n", i));
} else if i == 25 {
modified.push_str("// Inserted line A\n");
modified.push_str("// Inserted line B\n");
modified.push_str(&format!("// Original line {}\n", i));
} else {
modified.push_str(&format!("// Original line {}\n", i));
}
}
fs::write(&file_path, modified).expect("Failed to modify file");
}
fn create_repo_with_empty_lines(repo: &GitTestRepo) {
let file_path = repo.path.join("empty.rs");
let original = "first line\n\n\nfourth line\n";
fs::write(&file_path, original).expect("Failed to create file");
repo.git_add_all();
repo.git_commit("Initial commit");
let modified = "first line\nsecond line added\n\nfourth line modified\n";
fs::write(&file_path, modified).expect("Failed to modify file");
}
fn create_repo_short_file(repo: &GitTestRepo) {
let file_path = repo.path.join("short.rs");
let original = "line 1\nline 2\nline 3\n";
fs::write(&file_path, original).expect("Failed to create file");
repo.git_add_all();
repo.git_commit("Initial commit");
let modified = "line 1 modified\nline 2\nline 3 changed\n";
fs::write(&file_path, modified).expect("Failed to modify file");
}
#[test]
fn test_diff_cursor_empty_lines() {
let repo = GitTestRepo::new();
create_repo_with_empty_lines(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("empty.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
is_in_diff_view(&screen),
"Should still be in diff view after empty line navigation"
);
}
#[test]
fn test_diff_cursor_at_line_start() {
let repo = GitTestRepo::new();
create_repo_with_varied_lines(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("test.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("short"))
.unwrap();
open_side_by_side_diff(&mut harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_cursor_at_line_end() {
let repo = GitTestRepo::new();
create_repo_with_varied_lines(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("test.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("short"))
.unwrap();
open_side_by_side_diff(&mut harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
for _ in 0..5 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_cursor_at_line_middle() {
let repo = GitTestRepo::new();
create_repo_with_varied_lines(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("test.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("medium"))
.unwrap();
open_side_by_side_diff(&mut harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
for _ in 0..10 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
for _ in 0..3 {
harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
}
harness.render().unwrap();
for _ in 0..5 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_cursor_at_buffer_start() {
let repo = GitTestRepo::new();
create_repo_short_file(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("short.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_cursor_at_buffer_end() {
let repo = GitTestRepo::new();
create_repo_short_file(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("short.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
for _ in 0..5 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
}
harness.render().unwrap();
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_horizontal_scroll_long_line() {
init_tracing_from_env();
info!("Starting test_diff_horizontal_scroll_long_line");
let repo = GitTestRepo::new();
info!("Created git test repo");
create_repo_with_varied_lines(&repo);
info!("Created repo with varied lines");
setup_audit_mode_plugin(&repo);
info!("Set up audit_mode plugin");
let file_path = repo.path.join("test.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
80, 40,
Config::default(),
repo.path.clone(),
)
.unwrap();
info!("Created editor test harness");
harness.open_file(&file_path).unwrap();
info!("Opened file");
harness.render().unwrap();
info!("Rendered");
harness
.wait_until(|h| h.screen_to_string().contains("long"))
.unwrap();
info!("File content visible with 'long'");
open_side_by_side_diff(&mut harness);
info!("Opened side-by-side diff");
for _ in 0..3 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
}
harness.render().unwrap();
let screen_before = harness.screen_to_string();
println!("Before horizontal scroll:\n{}", screen_before);
for _ in 0..50 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
let screen_after_right = harness.screen_to_string();
println!("After scrolling right:\n{}", screen_after_right);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen_at_end = harness.screen_to_string();
println!("At end of line:\n{}", screen_at_end);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen_at_home = harness.screen_to_string();
println!("Back at home:\n{}", screen_at_home);
assert!(
is_in_diff_view(&screen_at_home),
"Should still be in diff view"
);
}
#[test]
fn test_diff_vertical_scroll_long_file() {
let repo = GitTestRepo::new();
create_repo_with_long_file(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("long.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
30, Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
let screen_initial = harness.screen_to_string();
println!("Initial view:\n{}", screen_initial);
harness
.send_key(KeyCode::PageDown, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let screen_after_pagedown = harness.screen_to_string();
println!("After PageDown:\n{}", screen_after_pagedown);
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen_at_end = harness.screen_to_string();
println!("At buffer end:\n{}", screen_at_end);
harness
.send_key(KeyCode::PageUp, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen_at_start = harness.screen_to_string();
println!("Back at buffer start:\n{}", screen_at_start);
assert!(
is_in_diff_view(&screen_at_start),
"Should still be in diff view"
);
}
#[test]
fn test_diff_cursor_viewport_middle() {
let repo = GitTestRepo::new();
create_repo_with_long_file(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("long.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
for _ in 0..10 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
}
harness.render().unwrap();
for _ in 0..3 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
}
harness.render().unwrap();
for _ in 0..2 {
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
}
harness.render().unwrap();
for _ in 0..5 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
for _ in 0..3 {
harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_word_movement_comprehensive() {
let repo = GitTestRepo::new();
create_repo_with_varied_lines(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("test.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
harness
.wait_until(|h| h.screen_to_string().contains("medium"))
.unwrap();
open_side_by_side_diff(&mut harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
for _ in 0..4 {
harness
.send_key(KeyCode::Right, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
}
for _ in 0..2 {
harness
.send_key(KeyCode::Left, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
}
for _ in 0..20 {
harness
.send_key(KeyCode::Right, KeyModifiers::CONTROL)
.unwrap();
}
harness.render().unwrap();
harness
.send_key(KeyCode::Left, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
for _ in 0..20 {
harness
.send_key(KeyCode::Left, KeyModifiers::CONTROL)
.unwrap();
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_pane_switching_with_tab() {
let repo = GitTestRepo::new();
create_repo_with_varied_lines(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("test.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_mouse_click_both_panes() {
let repo = GitTestRepo::new();
create_repo_with_varied_lines(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("test.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 5,
modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 10,
row: 5,
modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 100,
row: 5,
modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 100,
row: 5,
modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 70, row: 3,
modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 70,
row: 3,
modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 20,
row: 2, modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 20,
row: 2,
modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_mouse_click_moves_cursor() {
init_tracing_from_env();
let repo = GitTestRepo::new();
create_repo_for_wrap_test(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("wrap.txt");
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 1,") && screen.contains("Col 1"),
"Initial cursor should be at Ln 1, Col 1, got: {}",
screen
);
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 5,
modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 10,
row: 5,
modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 2,"),
"Cursor should move to Ln 2 after clicking row 5, got: {}",
screen
);
}
#[test]
fn test_diff_keyboard_scroll_to_cursor() {
init_tracing_from_env();
let repo = GitTestRepo::new();
let file_path = repo.path.join("long.txt");
let mut content = String::new();
for i in 1..=50 {
content.push_str(&format!("Line number {}\n", i));
}
fs::write(&file_path, &content).unwrap();
repo.git_add(&["long.txt"]);
repo.git_commit("Initial");
let mut new_content = String::new();
for i in 1..=50 {
if i == 25 {
new_content.push_str("MODIFIED line 25\n");
} else {
new_content.push_str(&format!("Line number {}\n", i));
}
}
fs::write(&file_path, &new_content).unwrap();
setup_audit_mode_plugin(&repo);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
30, Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 50")
|| screen.contains("Ln 49")
|| screen.contains("Ln 51")
|| screen.contains("Ln 52"),
"Cursor should be near end of file after Ctrl+End, got: {}",
screen
);
assert!(
screen.contains("Line number 50") || screen.contains("Line number 49"),
"View should scroll to show cursor at end of file, got: {}",
screen
);
}
#[test]
fn test_diff_scrollbar_click() {
init_tracing_from_env();
let repo = GitTestRepo::new();
let file_path = repo.path.join("scrollable.txt");
let mut content = String::new();
for i in 1..=100 {
content.push_str(&format!("Line {}: some content here\n", i));
}
fs::write(&file_path, &content).unwrap();
repo.git_add(&["scrollable.txt"]);
repo.git_commit("Initial");
let mut new_content = String::new();
for i in 1..=100 {
if i >= 45 && i <= 55 {
new_content.push_str(&format!("MODIFIED Line {}\n", i));
} else {
new_content.push_str(&format!("Line {}: some content here\n", i));
}
}
fs::write(&file_path, &new_content).unwrap();
setup_audit_mode_plugin(&repo);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
30,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
let screen = harness.screen_to_string();
assert!(
screen.contains("Line 1:") || screen.contains(" 1 Line"),
"Should start at top of file, got: {}",
screen
);
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 119,
row: 20, modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
harness
.send_mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 119,
row: 20,
modifiers: KeyModifiers::NONE,
})
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
let has_higher_lines =
screen.contains("Line 4") || screen.contains("Line 5") || screen.contains("MODIFIED");
assert!(
has_higher_lines || !screen.contains("Line 1:"),
"Scrollbar click should scroll the view, got: {}",
screen
);
}
#[test]
fn test_diff_selection_comprehensive() {
let repo = GitTestRepo::new();
create_repo_with_varied_lines(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("test.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
for _ in 0..5 {
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
}
harness.render().unwrap();
harness
.send_key(KeyCode::Down, KeyModifiers::SHIFT)
.unwrap();
harness.render().unwrap();
for _ in 0..3 {
harness
.send_key(KeyCode::Left, KeyModifiers::SHIFT)
.unwrap();
}
harness.render().unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT | KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::End, KeyModifiers::SHIFT).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::SHIFT)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_combined_movement() {
let repo = GitTestRepo::new();
create_repo_with_varied_lines(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("test.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
for _ in 0..5 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
for _ in 0..3 {
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
}
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_no_scroll_needed() {
let repo = GitTestRepo::new();
create_repo_short_file(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("short.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
160,
40, Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::PageDown, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::PageUp, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_home_end_all_line_types() {
init_tracing_from_env();
info!("Starting test_diff_home_end_all_line_types");
let repo = GitTestRepo::new();
info!("Created git test repo");
create_repo_with_varied_lines(&repo);
info!("Created repo with varied lines");
setup_audit_mode_plugin(&repo);
info!("Set up audit_mode plugin");
let file_path = repo.path.join("test.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
80, 40,
Config::default(),
repo.path.clone(),
)
.unwrap();
info!("Created editor test harness");
harness.open_file(&file_path).unwrap();
info!("Opened file");
harness.render().unwrap();
info!("Rendered");
open_side_by_side_diff(&mut harness);
info!("Opened side-by-side diff");
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen_at_start = harness.screen_to_string();
println!("Long line at start:\n{}", screen_at_start);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen_at_end = harness.screen_to_string();
println!("Long line at end:\n{}", screen_at_end);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen_back_home = harness.screen_to_string();
println!("Long line back at home:\n{}", screen_back_home);
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
#[test]
fn test_diff_cursor_always_visible() {
init_tracing_from_env();
info!("Starting test_diff_cursor_always_visible");
let repo = GitTestRepo::new();
info!("Created git test repo");
create_repo_with_long_file(&repo);
info!("Created repo with long file");
setup_audit_mode_plugin(&repo);
info!("Set up audit_mode plugin");
let file_path = repo.path.join("long.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
80, 20, Config::default(),
repo.path.clone(),
)
.unwrap();
info!("Created editor test harness");
harness.open_file(&file_path).unwrap();
info!("Opened file");
harness.render().unwrap();
info!("Rendered");
open_side_by_side_diff(&mut harness);
info!("Opened side-by-side diff");
harness
.send_key(KeyCode::End, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
is_in_diff_view(&screen),
"Should still be in diff view at buffer end"
);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
for _ in 0..30 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
is_in_diff_view(&screen),
"Should still be in diff view after horizontal scroll"
);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(is_in_diff_view(&screen), "Should still be in diff view");
}
fn create_repo_for_wrap_test(repo: &GitTestRepo) {
let file_path = repo.path.join("wrap.txt");
let old_content = "first\nsecond\nthird\n";
fs::write(&file_path, old_content).unwrap();
repo.git_add(&["wrap.txt"]);
repo.git_commit("Initial");
let new_content = "first_mod\nsecond_mod\nthird_mod\n";
fs::write(&file_path, new_content).unwrap();
}
#[test]
fn test_diff_cursor_wrap_right_to_next_line() {
init_tracing_from_env();
let repo = GitTestRepo::new();
create_repo_for_wrap_test(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("wrap.txt");
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 1,") || screen.contains("Ln 1 "),
"Should be on line 1, got: {}",
screen
);
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 2,") || screen.contains("Ln 2 "),
"Should be on line 2 after wrapping, got: {}",
screen
);
assert!(
screen.contains("Col 1") || screen.contains(", Col 1"),
"Should be at column 1 after wrapping, got: {}",
screen
);
}
#[test]
fn test_diff_cursor_wrap_left_to_prev_line() {
init_tracing_from_env();
let repo = GitTestRepo::new();
create_repo_for_wrap_test(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("wrap.txt");
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 2,") || screen.contains("Ln 2 "),
"Should be on line 2, got: {}",
screen
);
harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 1,") || screen.contains("Ln 1 "),
"Should be on line 1 after wrapping, got: {}",
screen
);
assert!(
screen.contains("Col 6"),
"Should be at end of OLD pane line (col 6 for 'first'), got: {}",
screen
);
}
#[test]
fn test_diff_cursor_word_wrap_at_boundaries() {
init_tracing_from_env();
let repo = GitTestRepo::new();
create_repo_for_wrap_test(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("wrap.txt");
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 1,") || screen.contains("Ln 1 "),
"Should be on line 1, got: {}",
screen
);
harness
.send_key(KeyCode::Right, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 2,") || screen.contains("Ln 2 "),
"Ctrl+Right at end should go to line 2, got: {}",
screen
);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Left, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 1,") || screen.contains("Ln 1 "),
"Ctrl+Left at start should go to line 1, got: {}",
screen
);
}
#[test]
fn test_diff_horizontal_scroll_keeps_cursor_visible() {
init_tracing_from_env();
let repo = GitTestRepo::new();
create_repo_with_varied_lines(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("test.rs");
let mut harness = EditorTestHarness::with_config_and_working_dir(
80,
30,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
for _ in 0..4 {
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 4,") || screen.contains("Ln 4 "),
"Should be on line 4 (the long line). Screen:\n{}",
screen
);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 4,") || screen.contains("Ln 4 "),
"After Home, should still be on line 4. Screen:\n{}",
screen
);
assert!(
screen.contains("this"),
"At line start, should see 'this'. Screen:\n{}",
screen
);
for _ in 0..40 {
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("Ln 4,"),
"After 40 right moves, should still be on line 4. Screen:\n{}",
screen
);
assert!(
is_in_diff_view(&screen),
"Should still be in diff view after right moves. Screen:\n{}",
screen
);
let screen = harness.screen_to_string();
assert!(
screen.contains("MODIFIED"),
"After scrolling right, should see 'MODIFIED'. Screen:\n{}",
screen
);
assert!(
!screen.contains("this is a very"),
"After scrolling right, 'this is a very' should have scrolled off. Screen:\n{}",
screen
);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("testing purposes") || screen.contains("testing"),
"At line end, should see 'testing purposes' (near end of line). Screen:\n{}",
screen
);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("this"),
"Back at line start, should see 'this'. Screen:\n{}",
screen
);
}
#[test]
fn test_diff_move_without_shift_clears_selection() {
init_tracing_from_env();
let repo = GitTestRepo::new();
create_repo_for_wrap_test(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("wrap.txt");
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.editor_mut().set_clipboard_for_test("".to_string());
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
for _ in 0..3 {
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
}
harness.render().unwrap();
harness
.send_key(KeyCode::Char('c'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.editor_mut().set_clipboard_for_test("".to_string());
harness
.send_key(KeyCode::Char('c'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('q'), KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| !is_in_diff_view(&h.screen_to_string()))
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness
.send_key(KeyCode::Char('v'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
let prompt_line = screen
.lines()
.find(|l| l.contains(">command"))
.unwrap_or("");
assert!(
!prompt_line.contains("fir"),
"After move without shift, selection should be cleared. Prompt: {}",
prompt_line
);
}
#[test]
fn test_diff_copy_no_empty_lines() {
init_tracing_from_env();
let repo = GitTestRepo::new();
let file_path = repo.path.join("multiline.txt");
let original_content = "line one\nline two\nline three\n";
fs::write(&file_path, original_content).expect("Failed to create file");
repo.git_add_all();
repo.git_commit("Initial commit");
let modified_content = "line one modified\nline two modified\nline three modified\n";
fs::write(&file_path, modified_content).expect("Failed to modify file");
setup_audit_mode_plugin(&repo);
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.editor_mut().set_clipboard_for_test("".to_string());
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Down, KeyModifiers::SHIFT)
.unwrap();
harness
.send_key(KeyCode::Down, KeyModifiers::SHIFT)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('c'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('q'), KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| !is_in_diff_view(&h.screen_to_string()))
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness
.send_key(KeyCode::Char('v'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
let prompt_line = screen
.lines()
.find(|l| l.starts_with(">") && !l.contains(">command"))
.unwrap_or("");
assert!(
prompt_line.contains("line"),
"Should contain copied line content. Prompt: {}",
prompt_line
);
}
#[test]
fn test_diff_copy_preserves_selection() {
init_tracing_from_env();
let repo = GitTestRepo::new();
create_repo_for_wrap_test(&repo);
setup_audit_mode_plugin(&repo);
let file_path = repo.path.join("wrap.txt");
let mut harness = EditorTestHarness::with_config_and_working_dir(
120,
40,
Config::default(),
repo.path.clone(),
)
.unwrap();
harness.editor_mut().set_clipboard_for_test("".to_string());
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
open_side_by_side_diff(&mut harness);
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
harness.render().unwrap();
for _ in 0..3 {
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
}
harness.render().unwrap();
harness
.send_key(KeyCode::Char('c'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Right, KeyModifiers::SHIFT)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('c'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('q'), KeyModifiers::NONE)
.unwrap();
harness
.wait_until(|h| !is_in_diff_view(&h.screen_to_string()))
.unwrap();
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();
harness.wait_for_prompt().unwrap();
harness
.send_key(KeyCode::Char('v'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
let prompt_line = screen
.lines()
.find(|l| l.starts_with(">") && !l.contains(">command"))
.unwrap_or("");
assert!(
prompt_line.contains("firs"),
"Should have 4 chars after extending selection post-copy. Prompt: {}",
prompt_line
);
}