use crate::common::harness::EditorTestHarness;
use crossterm::event::{KeyCode, KeyModifiers};
#[test]
fn test_save_unnamed_buffer_shows_save_as_prompt() {
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.new_buffer().unwrap();
harness.type_text("Hello world").unwrap();
harness.render().unwrap();
harness.assert_screen_contains("*");
harness
.send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("Save as:");
}
#[test]
fn test_quit_with_modified_buffers_shows_confirmation() {
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("Modified content").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('q'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
assert!(
!harness.should_quit(),
"Editor should not quit immediately with unsaved changes"
);
}
#[test]
fn test_quit_without_modified_buffers() {
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness
.send_key(KeyCode::Char('q'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
assert!(
harness.should_quit(),
"Editor should quit when no modified buffers"
);
}
#[test]
fn test_quit_with_confirmation_discard() {
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("Modified").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('q'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('d'), KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert!(harness.should_quit(), "Editor should quit after confirming");
}
#[test]
fn test_quit_with_confirmation_cancel() {
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.type_text("Modified").unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('q'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness
.send_key(KeyCode::Char('c'), KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
assert!(
!harness.should_quit(),
"Editor should not quit after canceling"
);
}
#[test]
fn test_undo_restores_non_dirty_status() {
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.render().unwrap();
let screen_before = harness.screen_to_string();
let tab_row: String = screen_before.lines().nth(1).unwrap_or("").to_string();
assert!(
!tab_row.contains('*'),
"New buffer should not show modified indicator"
);
harness.type_text("abc").unwrap();
harness.render().unwrap();
harness.assert_screen_contains("*");
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen_after = harness.screen_to_string();
let tab_row_after: String = screen_after.lines().nth(1).unwrap_or("").to_string();
assert!(
!tab_row_after.contains('*'),
"Buffer should not show modified indicator after undoing all changes"
);
}
#[test]
fn test_undo_after_save_modified_status() {
let mut harness = EditorTestHarness::with_temp_project(80, 24).unwrap();
let project_dir = harness.project_dir().unwrap();
let file_path = project_dir.join("test.txt");
std::fs::write(&file_path, "initial").unwrap();
harness.open_file(&file_path).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
let tab_row: String = screen.lines().nth(1).unwrap_or("").to_string();
assert!(
!tab_row.contains('*'),
"Buffer should not be modified after opening"
);
harness.type_text("X").unwrap();
harness.render().unwrap();
harness.assert_screen_contains("*");
harness
.send_key(KeyCode::Char('s'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("Saved");
let screen_after_save = harness.screen_to_string();
let tab_row_after_save: String = screen_after_save.lines().nth(1).unwrap_or("").to_string();
assert!(
!tab_row_after_save.contains('*'),
"Buffer should not be modified after save"
);
harness.type_text("Y").unwrap();
harness.render().unwrap();
harness.assert_screen_contains("*");
harness
.send_key(KeyCode::Char('z'), KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen_after_undo = harness.screen_to_string();
let tab_row_after_undo: String = screen_after_undo.lines().nth(1).unwrap_or("").to_string();
assert!(
!tab_row_after_undo.contains('*'),
"Buffer should not be modified after undoing to saved state"
);
}
#[test]
fn test_tabs_show_close_button() {
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(screen.contains('×'), "Tab bar should show close button (×)");
}
#[test]
fn test_click_tab_close_button() {
use crate::common::harness::layout;
let mut harness = EditorTestHarness::new(80, 24).unwrap();
let temp_dir = tempfile::TempDir::new().unwrap();
let file1_path = temp_dir.path().join("first.txt");
let file2_path = temp_dir.path().join("to_close.txt");
std::fs::write(&file1_path, "First file content").unwrap();
std::fs::write(&file2_path, "UNIQUE_CONTENT_TO_CLOSE").unwrap();
harness.open_file(&file1_path).unwrap();
harness.render().unwrap();
harness.open_file(&file2_path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("UNIQUE_CONTENT_TO_CLOSE");
let screen = harness.screen_to_string();
let tab_row: String = screen
.lines()
.nth(layout::TAB_BAR_ROW)
.unwrap_or("")
.to_string();
let tabs_before = tab_row.matches('×').count();
assert_eq!(tabs_before, 2, "Should have 2 tabs before close");
let x_positions: Vec<usize> = tab_row.match_indices('×').map(|(i, _)| i).collect();
let x_pos = x_positions[1];
harness
.mouse_click(x_pos as u16, layout::TAB_BAR_ROW as u16)
.unwrap();
harness.render().unwrap();
let screen_after = harness.screen_to_string();
assert!(
!screen_after.contains("UNIQUE_CONTENT_TO_CLOSE"),
"Content should no longer be visible after closing tab"
);
let tab_row_after: String = screen_after
.lines()
.nth(layout::TAB_BAR_ROW)
.unwrap_or("")
.to_string();
let tabs_after = tab_row_after.matches('×').count();
assert_eq!(tabs_after, 1, "Should have 1 tab after close");
}
#[test]
fn test_click_tab_close_button_modified_buffer() {
use crate::common::harness::layout;
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.new_buffer().unwrap();
harness.type_text("Modified content").unwrap();
harness.render().unwrap();
harness.assert_screen_contains("*");
let screen = harness.screen_to_string();
let tab_row: String = screen
.lines()
.nth(layout::TAB_BAR_ROW)
.unwrap_or("")
.to_string();
if let Some(star_pos) = tab_row.find('*') {
if let Some(x_pos) = tab_row[star_pos..].find('×') {
let actual_x_pos = star_pos + x_pos;
harness
.mouse_click(actual_x_pos as u16, layout::TAB_BAR_ROW as u16)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("modified. (s)ave, (d)iscard, (C)ancel");
} else {
panic!("Could not find × close button after * in tab bar");
}
} else {
panic!("Could not find * modified indicator in tab bar");
}
}
#[test]
fn test_click_tab_close_modified_discard() {
use crate::common::harness::layout;
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.new_buffer().unwrap();
harness.type_text("Will discard").unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
let tab_row: String = screen
.lines()
.nth(layout::TAB_BAR_ROW)
.unwrap_or("")
.to_string();
if let Some(star_pos) = tab_row.find('*') {
if let Some(x_pos) = tab_row[star_pos..].find('×') {
let actual_x_pos = star_pos + x_pos;
harness
.mouse_click(actual_x_pos as u16, layout::TAB_BAR_ROW as u16)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("modified. (s)ave, (d)iscard, (C)ancel");
harness
.send_key(KeyCode::Char('d'), KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("Buffer closed");
} else {
panic!("Could not find × close button after * in tab bar");
}
} else {
panic!("Could not find * modified indicator in tab bar");
}
}
#[test]
fn test_click_tab_close_modified_cancel() {
use crate::common::harness::layout;
let mut harness = EditorTestHarness::new(80, 24).unwrap();
harness.new_buffer().unwrap();
harness.type_text("Keep this").unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
let tab_row: String = screen
.lines()
.nth(layout::TAB_BAR_ROW)
.unwrap_or("")
.to_string();
if let Some(star_pos) = tab_row.find('*') {
if let Some(x_pos) = tab_row[star_pos..].find('×') {
let actual_x_pos = star_pos + x_pos;
harness
.mouse_click(actual_x_pos as u16, layout::TAB_BAR_ROW as u16)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("modified. (s)ave, (d)iscard, (C)ancel");
harness
.send_key(KeyCode::Char('c'), KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
harness.assert_screen_contains("Close cancelled");
harness.assert_screen_contains("Keep this");
} else {
panic!("Could not find × close button after * in tab bar");
}
} else {
panic!("Could not find * modified indicator in tab bar");
}
}
#[test]
fn test_next_buffer_skips_hidden_buffers() {
use fresh::primitives::text_property::TextPropertyEntry;
use fresh::services::plugins::api::PluginCommand;
use std::collections::HashMap;
let mut harness = EditorTestHarness::with_temp_project(80, 24).unwrap();
let project_dir = harness.project_dir().unwrap();
let file1_path = project_dir.join("visible1.txt");
let file2_path = project_dir.join("visible2.txt");
std::fs::write(&file1_path, "VISIBLE_BUFFER_1_CONTENT").unwrap();
std::fs::write(&file2_path, "VISIBLE_BUFFER_2_CONTENT").unwrap();
harness.open_file(&file1_path).unwrap();
harness.render().unwrap();
let hidden_cmd = PluginCommand::CreateVirtualBufferWithContent {
name: "*Hidden*".to_string(),
mode: "hidden-test".to_string(),
read_only: true,
entries: vec![TextPropertyEntry {
text: "HIDDEN_BUFFER_CONTENT".to_string(),
properties: HashMap::new(),
}],
show_line_numbers: true,
show_cursors: true,
editing_disabled: true,
hidden_from_tabs: true, request_id: None,
};
harness
.editor_mut()
.handle_plugin_command(hidden_cmd)
.unwrap();
harness.render().unwrap();
harness.open_file(&file2_path).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("VISIBLE_BUFFER_2_CONTENT");
for i in 0..6 {
harness
.send_key(KeyCode::PageDown, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
println!("After next_buffer #{}: screen:\n{}", i + 1, screen);
assert!(
!screen.contains("HIDDEN_BUFFER_CONTENT"),
"next_buffer should skip hidden buffer. Iteration {}. Screen:\n{}",
i + 1,
screen
);
assert!(
screen.contains("VISIBLE_BUFFER_1_CONTENT")
|| screen.contains("VISIBLE_BUFFER_2_CONTENT"),
"Should be on a visible buffer. Iteration {}. Screen:\n{}",
i + 1,
screen
);
}
for i in 0..6 {
harness
.send_key(KeyCode::PageUp, KeyModifiers::CONTROL)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
println!("After prev_buffer #{}: screen:\n{}", i + 1, screen);
assert!(
!screen.contains("HIDDEN_BUFFER_CONTENT"),
"prev_buffer should skip hidden buffer. Iteration {}. Screen:\n{}",
i + 1,
screen
);
assert!(
screen.contains("VISIBLE_BUFFER_1_CONTENT")
|| screen.contains("VISIBLE_BUFFER_2_CONTENT"),
"Should be on a visible buffer. Iteration {}. Screen:\n{}",
i + 1,
screen
);
}
}
#[test]
fn test_close_returns_to_previous_focused() {
let mut harness = EditorTestHarness::new(80, 24).unwrap();
let temp_dir = tempfile::TempDir::new().unwrap();
let file_a = temp_dir.path().join("file_a.txt");
let file_b = temp_dir.path().join("file_b.txt");
let file_c = temp_dir.path().join("file_c.txt");
std::fs::write(&file_a, "CONTENT_A").unwrap();
std::fs::write(&file_b, "CONTENT_B").unwrap();
std::fs::write(&file_c, "CONTENT_C").unwrap();
harness.open_file(&file_a).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("CONTENT_A");
harness.open_file(&file_b).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("CONTENT_B");
harness.open_file(&file_c).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("CONTENT_C");
harness.open_file(&file_a).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("CONTENT_A");
harness.open_file(&file_b).unwrap();
harness.render().unwrap();
harness.assert_screen_contains("CONTENT_B");
harness
.send_key(KeyCode::Char('w'), KeyModifiers::ALT)
.unwrap();
harness.render().unwrap();
let screen = harness.screen_to_string();
assert!(
screen.contains("CONTENT_A"),
"After closing B, should return to previously focused A. Screen:\n{}",
screen
);
}