use crate::common::harness::EditorTestHarness;
use tempfile::TempDir;
#[test]
fn test_png_file_detected_as_binary() {
let temp_dir = TempDir::new().unwrap();
let png_path = temp_dir.path().join("test.png");
let png_data: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, ];
std::fs::write(&png_path, png_data).unwrap();
let mut harness = EditorTestHarness::new(120, 24).unwrap();
harness.open_file(&png_path).unwrap();
harness.render().unwrap();
assert!(
harness.editor().is_editing_disabled(),
"Binary file should have editing disabled"
);
harness.assert_screen_contains("[BIN]");
harness.assert_screen_contains("[BIN]");
}
#[test]
fn test_jpeg_file_detected_as_binary() {
let temp_dir = TempDir::new().unwrap();
let jpeg_path = temp_dir.path().join("test.jpg");
let jpeg_data: &[u8] = &[
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, ];
std::fs::write(&jpeg_path, jpeg_data).unwrap();
let mut harness = EditorTestHarness::new(120, 24).unwrap();
harness.open_file(&jpeg_path).unwrap();
harness.render().unwrap();
assert!(
harness.editor().is_editing_disabled(),
"JPEG file should have editing disabled"
);
harness.assert_screen_contains("[BIN]");
}
#[test]
fn test_elf_executable_detected_as_binary() {
let temp_dir = TempDir::new().unwrap();
let elf_path = temp_dir.path().join("test_binary");
let elf_data: &[u8] = &[
0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
std::fs::write(&elf_path, elf_data).unwrap();
let mut harness = EditorTestHarness::new(120, 24).unwrap();
harness.open_file(&elf_path).unwrap();
harness.render().unwrap();
assert!(
harness.editor().is_editing_disabled(),
"ELF binary should have editing disabled"
);
harness.assert_screen_contains("[BIN]");
}
#[test]
fn test_text_file_not_detected_as_binary() {
let temp_dir = TempDir::new().unwrap();
let text_path = temp_dir.path().join("test.txt");
std::fs::write(&text_path, "Hello, world!\nThis is a text file.\n").unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&text_path).unwrap();
harness.render().unwrap();
assert!(
!harness.editor().is_editing_disabled(),
"Text file should allow editing"
);
harness.assert_screen_not_contains("binary");
}
#[test]
fn test_ansi_escape_sequences_not_binary() {
let temp_dir = TempDir::new().unwrap();
let ansi_path = temp_dir.path().join("colored.txt");
let ansi_content = "\x1b[31mRed text\x1b[0m\n\x1b[32mGreen text\x1b[0m\n";
std::fs::write(&ansi_path, ansi_content).unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&ansi_path).unwrap();
harness.render().unwrap();
assert!(
!harness.editor().is_editing_disabled(),
"File with ANSI escape sequences should allow editing"
);
}
#[test]
fn test_typing_blocked_in_binary_file() {
use crossterm::event::{KeyCode, KeyModifiers};
let temp_dir = TempDir::new().unwrap();
let png_path = temp_dir.path().join("test.png");
let png_data: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x00,
];
std::fs::write(&png_path, png_data).unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&png_path).unwrap();
let initial_len = harness.buffer_len();
harness
.send_key(KeyCode::Char('a'), KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Char('b'), KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Char('c'), KeyModifiers::NONE)
.unwrap();
assert_eq!(
harness.buffer_len(),
initial_len,
"Typing should be blocked in binary files"
);
}
#[test]
fn test_binary_bytes_rendered_as_hex() {
let temp_dir = TempDir::new().unwrap();
let bin_path = temp_dir.path().join("test.bin");
let bin_data: &[u8] = &[0x89, 0x50, 0x4E, 0x47, 0x00, 0x01, 0x7F];
std::fs::write(&bin_path, bin_data).unwrap();
let mut harness = EditorTestHarness::new(120, 24).unwrap();
harness.open_file(&bin_path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("<89>");
harness.assert_screen_contains("PNG");
harness.assert_screen_contains("<00>");
harness.assert_screen_contains("<01>");
harness.assert_screen_contains("<7F>");
}
#[test]
fn test_binary_file_scrolling_no_artifacts() {
use crossterm::event::{KeyCode, KeyModifiers};
let temp_dir = TempDir::new().unwrap();
let png_path = temp_dir.path().join("test.png");
let mut png_data = Vec::new();
png_data.extend_from_slice(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
for i in 0..100 {
for j in 0..20 {
png_data.push(((i * 20 + j) % 256) as u8);
}
png_data.push(0x0A);
}
std::fs::write(&png_path, &png_data).unwrap();
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.open_file(&png_path).unwrap();
harness.render_real().unwrap();
let initial_screen = harness.vt100_screen_to_string();
validate_gutter_format(&initial_screen, "initial");
harness.assert_test_matches_real();
for i in 0..5 {
harness
.send_key(KeyCode::PageDown, KeyModifiers::NONE)
.unwrap();
harness.render_real().unwrap();
let screen = harness.vt100_screen_to_string();
validate_gutter_format(&screen, &format!("after PageDown #{}", i + 1));
harness.assert_test_matches_real();
}
for i in 0..5 {
harness
.send_key(KeyCode::PageUp, KeyModifiers::NONE)
.unwrap();
harness.render_real().unwrap();
let screen = harness.vt100_screen_to_string();
validate_gutter_format(&screen, &format!("after PageUp #{}", i + 1));
harness.assert_test_matches_real();
}
harness
.send_key(KeyCode::Home, KeyModifiers::CONTROL)
.unwrap();
harness.render_real().unwrap();
let final_screen = harness.vt100_screen_to_string();
validate_gutter_format(&final_screen, "final");
harness.assert_test_matches_real();
compare_screens_cell_by_cell(&initial_screen, &final_screen, 80, 24);
}
fn compare_screens_cell_by_cell(initial: &str, final_screen: &str, width: usize, height: usize) {
let initial_lines: Vec<&str> = initial.lines().collect();
let final_lines: Vec<&str> = final_screen.lines().collect();
assert_eq!(
initial_lines.len(),
final_lines.len(),
"Screen height mismatch: initial has {} lines, final has {} lines",
initial_lines.len(),
final_lines.len()
);
let mut differences = Vec::new();
for row in 0..height.min(initial_lines.len()) {
let initial_line = initial_lines.get(row).unwrap_or(&"");
let final_line = final_lines.get(row).unwrap_or(&"");
if initial_line != final_line {
let initial_chars: Vec<char> = initial_line.chars().collect();
let final_chars: Vec<char> = final_line.chars().collect();
for col in 0..width.max(initial_chars.len()).max(final_chars.len()) {
let ic = initial_chars.get(col).copied().unwrap_or(' ');
let fc = final_chars.get(col).copied().unwrap_or(' ');
if ic != fc {
differences.push(format!(
" Row {}, Col {}: initial '{}' (U+{:04X}) vs final '{}' (U+{:04X})",
row, col, ic, ic as u32, fc, fc as u32
));
}
}
}
}
if !differences.is_empty() {
panic!(
"Screen differs after scrolling!\n\nDifferences:\n{}\n\nInitial screen:\n{}\n\nFinal screen:\n{}",
differences.join("\n"),
initial,
final_screen
);
}
}
fn validate_gutter_format(screen: &str, context: &str) {
let lines: Vec<&str> = screen.lines().collect();
let content_start = 2;
let content_end = lines.len().saturating_sub(2);
for (i, line) in lines.iter().enumerate() {
if i < content_start || i >= content_end {
continue;
}
if line.trim().is_empty() || line.trim().starts_with('~') {
continue;
}
let bar_pos = line.find('│');
assert!(
bar_pos.is_some(),
"{}: Line {} is missing gutter separator │.\nLine: '{}'\n\nFull screen:\n{}",
context,
i,
line,
screen
);
let bar_pos = bar_pos.unwrap();
let before_bar = &line[..bar_pos];
let invalid_chars: Vec<char> = before_bar
.chars()
.filter(|c| !c.is_ascii_whitespace() && !c.is_ascii_digit())
.collect();
assert!(
invalid_chars.is_empty(),
"{}: Line {} has content overflowing into gutter.\nGutter area: '{}'\nInvalid chars: {:?}\nLine: '{}'\n\nFull screen:\n{}",
context, i, before_bar, invalid_chars, line, screen
);
}
}