#[cfg(test)]
mod textarea_tests {
use crate::textarea::Model;
use crate::Component;
#[derive(Debug)]
struct Want {
view: Option<String>,
cursor_row: usize,
cursor_col: usize,
}
#[derive(Debug)]
struct TestCase {
name: &'static str,
model_func: Option<fn(Model) -> Model>,
want: Want,
}
fn new_text_area() -> Model {
let mut textarea = Model::new();
textarea.prompt = "> ".to_string();
textarea.placeholder = "Hello, World!".to_string();
textarea.focus = true;
textarea.current_style = textarea.focused_style.clone();
textarea
}
fn send_string(mut model: Model, input: &str) -> Model {
for ch in input.chars() {
if ch == '\n' {
model.insert_newline();
} else {
model.insert_rune(ch);
}
}
model
}
fn normalize_string(s: &str) -> String {
lipgloss_extras::lipgloss::strip_ansi(s)
.lines()
.map(|line| line.trim_end())
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn test_comprehensive_view_rendering() {
let test_cases = vec![
TestCase {
name: "placeholder",
model_func: None,
want: Want {
view: Some("> 1 Hello, World!\n>\n>\n>\n>\n>".to_string()),
cursor_row: 0,
cursor_col: 0,
},
},
TestCase {
name: "single line",
model_func: Some(|mut m| {
m.set_value("the first line");
m
}),
want: Want {
view: Some("> 1 the first line\n>\n>\n>\n>\n>".to_string()),
cursor_row: 0,
cursor_col: 14,
},
},
TestCase {
name: "multiple lines",
model_func: Some(|mut m| {
m.set_value("the first line\nthe second line\nthe third line");
m
}),
want: Want {
view: Some("> 1 the first line\n> 2 the second line\n> 3 the third line\n>\n>\n>".to_string()),
cursor_row: 2,
cursor_col: 14,
},
},
TestCase {
name: "single line without line numbers",
model_func: Some(|mut m| {
m.set_value("the first line");
m.show_line_numbers = false;
m
}),
want: Want {
view: Some("> the first line\n>\n>\n>\n>\n>".to_string()),
cursor_row: 0,
cursor_col: 14,
},
},
TestCase {
name: "multiple lines without line numbers",
model_func: Some(|mut m| {
m.set_value("the first line\nthe second line\nthe third line");
m.show_line_numbers = false;
m
}),
want: Want {
view: Some("> the first line\n> the second line\n> the third line\n>\n>\n>".to_string()),
cursor_row: 2,
cursor_col: 14,
},
},
TestCase {
name: "custom end of buffer character",
model_func: Some(|mut m| {
m.set_value("the first line");
m.end_of_buffer_character = '*';
m
}),
want: Want {
view: Some("> 1 the first line\n> *\n> *\n> *\n> *\n> *".to_string()),
cursor_row: 0,
cursor_col: 14,
},
},
TestCase {
name: "custom prompt",
model_func: Some(|mut m| {
m.set_value("the first line");
m.prompt = "* ".to_string();
m
}),
want: Want {
view: Some("* 1 the first line\n*\n*\n*\n*\n*".to_string()),
cursor_row: 0,
cursor_col: 14,
},
},
TestCase {
name: "character limit",
model_func: Some(|mut m| {
m.char_limit = 7;
m = send_string(m, "foo bar baz");
m
}),
want: Want {
view: Some("> 1 foo bar\n>\n>\n>\n>\n>".to_string()),
cursor_row: 0,
cursor_col: 7,
},
},
];
for test_case in test_cases {
let mut textarea = new_text_area();
if let Some(model_func) = test_case.model_func {
textarea = model_func(textarea);
}
if let Some(expected_view) = test_case.want.view {
let actual_view = normalize_string(&textarea.view());
let expected_view = normalize_string(&expected_view);
assert_eq!(
actual_view, expected_view,
"Test case '{}' failed view comparison",
test_case.name
);
}
let cursor_row = textarea.cursor_line_number();
let cursor_col = textarea.line_info().column_offset;
assert_eq!(
cursor_row, test_case.want.cursor_row,
"Test case '{}' failed cursor row: expected {}, got {}",
test_case.name, test_case.want.cursor_row, cursor_row
);
assert_eq!(
cursor_col, test_case.want.cursor_col,
"Test case '{}' failed cursor col: expected {}, got {}",
test_case.name, test_case.want.cursor_col, cursor_col
);
}
}
#[test]
fn test_vertical_scrolling() {
let mut textarea = new_text_area();
textarea.prompt = String::new();
textarea.show_line_numbers = false;
textarea.set_height(1);
textarea.set_width(20);
textarea.char_limit = 100;
let input = "This is a really long line that should wrap around the text area.";
textarea = send_string(textarea, input);
assert!(textarea.value().contains("This is a really"));
}
#[test]
fn test_word_wrap_overflowing() {
let mut textarea = new_text_area();
textarea.set_height(3);
textarea.set_width(20);
textarea.char_limit = 500;
let input = "Testing Testing Testing Testing Testing Testing Testing Testing";
textarea = send_string(textarea, input);
textarea.row = 0;
textarea.col = 0;
let input2 = "Testing";
for ch in input2.chars() {
textarea.insert_rune(ch);
}
let line_info = textarea.line_info();
assert!(
line_info.width <= 20,
"Line width should not exceed max width"
);
}
#[test]
fn test_value_soft_wrap() {
let mut textarea = new_text_area();
textarea.set_width(16);
textarea.set_height(10);
textarea.char_limit = 500;
let input = "Testing Testing Testing Testing Testing Testing Testing Testing";
textarea = send_string(textarea, input);
let value = textarea.value();
assert_eq!(
value, input,
"Value should be preserved despite soft wrapping"
);
}
#[test]
fn test_set_value() {
let mut textarea = new_text_area();
textarea.set_value("Foo\nBar\nBaz");
assert_eq!(textarea.row, 2, "Cursor should be on row 2");
assert_eq!(textarea.col, 3, "Cursor should be on column 3");
let value = textarea.value();
assert_eq!(value, "Foo\nBar\nBaz", "Value should match input");
textarea.set_value("Test");
let value = textarea.value();
assert_eq!(
value, "Test",
"Textarea should be reset when SetValue is called"
);
}
#[test]
fn test_insert_string() {
let mut textarea = new_text_area();
textarea = send_string(textarea, "foo baz");
textarea.col = 4;
textarea.insert_string("bar ");
let value = textarea.value();
assert_eq!(
value, "foo bar baz",
"Expected insert string to insert bar between foo and baz"
);
}
#[test]
fn test_can_handle_emoji() {
let mut textarea = new_text_area();
let input = "🧋";
textarea = send_string(textarea, input);
let value = textarea.value();
assert_eq!(value, input, "Expected emoji to be inserted");
let input2 = "🧋🧋🧋";
textarea.set_value(input2);
let value = textarea.value();
assert_eq!(value, input2, "Expected multiple emojis to be inserted");
assert_eq!(
textarea.col, 3,
"Expected cursor to be on the third character"
);
let char_offset = textarea.line_info().char_offset;
assert_eq!(
char_offset, 6,
"Expected cursor to be on the sixth character due to emoji width"
);
}
#[test]
fn test_vertical_navigation_keeps_cursor_horizontal_position() {
let mut textarea = new_text_area();
textarea.set_width(20);
textarea.set_value("你好你好\nHello");
textarea.row = 0;
textarea.col = 2;
let line_info = textarea.line_info();
assert_eq!(
line_info.char_offset, 4,
"Expected cursor to be on fourth character"
);
assert_eq!(
line_info.column_offset, 2,
"Expected cursor to be on second column"
);
textarea.cursor_down();
let line_info = textarea.line_info();
assert_eq!(
line_info.char_offset, 4,
"Expected cursor to maintain character offset"
);
assert_eq!(
line_info.column_offset, 4,
"Expected cursor to be on fourth column after moving down"
);
}
#[test]
fn test_vertical_navigation_should_remember_position_while_traversing() {
let mut textarea = new_text_area();
textarea.set_width(40);
textarea.set_value("Hello\nWorld\nThis is a long line.");
assert_eq!(textarea.col, 20, "Expected cursor to be on 20th character");
assert_eq!(textarea.row, 2, "Expected cursor to be on last line");
textarea.cursor_up();
assert_eq!(
textarea.col, 5,
"Expected cursor to be on 5th character of second line"
);
assert_eq!(textarea.row, 1, "Expected cursor to be on second line");
textarea.cursor_up();
assert_eq!(
textarea.col, 5,
"Expected cursor to be on 5th character of first line"
);
assert_eq!(textarea.row, 0, "Expected cursor to be on first line");
textarea.cursor_down();
textarea.cursor_down();
assert_eq!(
textarea.col, 20,
"Expected cursor to be back on 20th character of last line"
);
assert_eq!(textarea.row, 2, "Expected cursor to be back on last line");
textarea.cursor_up();
textarea.character_left(false);
assert_eq!(
textarea.col, 4,
"Expected cursor to be on 4th character after moving left"
);
assert_eq!(textarea.row, 1, "Expected cursor to be on second line");
textarea.cursor_down();
assert_eq!(
textarea.col, 4,
"Expected cursor to stay on 4th character after moving down"
);
assert_eq!(textarea.row, 2, "Expected cursor to be on last line");
}
#[test]
fn test_basic_editing_operations() {
let mut textarea = new_text_area();
textarea.insert_rune('H');
textarea.insert_rune('e');
textarea.insert_rune('l');
textarea.insert_rune('l');
textarea.insert_rune('o');
assert_eq!(textarea.value(), "Hello");
assert_eq!(textarea.col, 5);
textarea.delete_character_backward();
assert_eq!(textarea.value(), "Hell");
assert_eq!(textarea.col, 4);
textarea.insert_rune('o');
textarea.character_left(false);
textarea.delete_character_forward();
assert_eq!(textarea.value(), "Hell");
assert_eq!(textarea.col, 4);
}
#[test]
fn test_word_operations() {
let mut textarea = new_text_area();
textarea.insert_string("hello world test");
textarea.col = 8;
textarea.delete_word_backward();
assert_eq!(textarea.value(), "hello test");
textarea.cursor_end();
textarea.delete_word_backward();
assert_eq!(textarea.value(), "hello ");
}
#[test]
fn test_line_operations() {
let mut textarea = new_text_area();
textarea.insert_string("first line\nsecond line");
textarea.cursor_start();
assert_eq!(textarea.col, 0);
textarea.cursor_end();
assert_eq!(textarea.col, 11);
textarea.col = 6; textarea.insert_newline();
assert_eq!(textarea.value(), "first line\nsecond\n line");
assert_eq!(textarea.row, 2);
assert_eq!(textarea.col, 0);
}
#[test]
fn test_character_limit() {
let mut textarea = new_text_area();
textarea.char_limit = 5;
textarea.insert_string("hello world");
assert_eq!(textarea.value(), "hello");
assert_eq!(textarea.length(), 5);
}
#[test]
fn test_case_transformations() {
let mut textarea = new_text_area();
textarea.insert_string("hello world");
textarea.col = 0;
textarea.uppercase_right();
assert!(textarea.value().starts_with("HELLO"));
textarea.set_value("HELLO WORLD");
textarea.col = 0;
textarea.lowercase_right();
assert!(textarea.value().starts_with("hello"));
textarea.set_value("hello world");
textarea.col = 0;
textarea.capitalize_right();
assert!(textarea.value().starts_with("Hello"));
}
#[test]
fn test_transpose_characters() {
let mut textarea = new_text_area();
textarea.insert_string("hello");
textarea.col = 2;
textarea.transpose_left();
assert_eq!(textarea.value(), "hlelo");
}
#[test]
fn test_multi_line_editing() {
let mut textarea = new_text_area();
textarea.insert_string("line1\nline2\nline3");
assert_eq!(textarea.line_count(), 3);
assert_eq!(textarea.line(), 2);
textarea.row = 1;
textarea.col = 0;
textarea.delete_character_backward();
assert_eq!(textarea.value(), "line1line2\nline3");
assert_eq!(textarea.line_count(), 2);
}
#[test]
fn test_cursor_movement() {
let mut textarea = new_text_area();
textarea.insert_string("hello world\nfoo bar");
textarea.move_to_begin();
assert_eq!(textarea.col, 0);
assert_eq!(textarea.row, 0);
textarea.word_right();
assert_eq!(textarea.col, 5);
textarea.word_right();
assert_eq!(textarea.col, 11);
textarea.word_left();
assert_eq!(textarea.col, 6);
textarea.move_to_begin();
assert_eq!(textarea.row, 0);
assert_eq!(textarea.col, 0);
textarea.move_to_end();
assert_eq!(textarea.row, 1);
assert_eq!(textarea.col, 7); }
#[test]
fn test_width_height_constraints() {
let mut textarea = new_text_area();
textarea.set_width(10);
assert_eq!(textarea.width(), 4);
textarea.set_height(5);
assert_eq!(textarea.height(), 5);
textarea.max_width = 15;
textarea.set_width(20); assert!(textarea.width() <= 15);
textarea.max_height = 3;
textarea.set_height(5); assert_eq!(textarea.height(), 3);
}
#[test]
fn test_line_info_calculation() {
let mut textarea = new_text_area();
textarea.set_width(10);
textarea.insert_string("hello world this is a test");
textarea.col = 5;
let line_info = textarea.line_info();
assert!(line_info.width > 0);
assert!(line_info.height > 0);
assert_eq!(line_info.column_offset, 1);
}
#[test]
fn test_focus_blur() {
let mut textarea = new_text_area();
assert!(textarea.focused());
textarea.blur();
assert!(!textarea.focused());
textarea.focus();
assert!(textarea.focused());
}
#[test]
fn test_reset() {
let mut textarea = new_text_area();
textarea.insert_string("hello world");
textarea.col = 5;
textarea.row = 0;
textarea.reset();
assert_eq!(textarea.value(), "");
assert_eq!(textarea.col, 0);
assert_eq!(textarea.row, 0);
}
#[test]
fn test_unicode_handling() {
let mut textarea = new_text_area();
textarea.insert_string("Hello 世界 🌍 🚀");
assert_eq!(textarea.value(), "Hello 世界 🌍 🚀");
textarea.cursor_start();
for _ in 0..6 {
textarea.character_right();
}
textarea.delete_character_forward();
assert_eq!(textarea.value(), "Hello 界 🌍 🚀");
}
#[test]
fn test_line_wrapping() {
let mut textarea = new_text_area();
textarea.set_width(10);
textarea.show_line_numbers = false;
textarea.prompt = String::new();
textarea.insert_string("this is a long line that should wrap");
assert!(!textarea.value().contains('\n'));
let line_info = textarea.line_info();
assert!(line_info.height > 1);
}
#[test]
fn test_delete_operations() {
let mut textarea = new_text_area();
textarea.insert_string("hello world");
textarea.col = 5; textarea.delete_after_cursor();
assert_eq!(textarea.value(), "hello");
textarea.set_value("hello world");
textarea.col = 5;
textarea.delete_before_cursor();
assert_eq!(textarea.value(), " world");
assert_eq!(textarea.col, 0);
}
#[test]
fn test_prompt_and_placeholder() {
let mut textarea = new_text_area();
textarea.prompt = ">> ".to_string();
textarea.placeholder = "Enter text here...".to_string();
assert_eq!(textarea.prompt, ">> ");
assert_eq!(textarea.placeholder, "Enter text here...");
}
#[test]
fn test_end_of_buffer_character() {
let mut textarea = new_text_area();
textarea.end_of_buffer_character = '*';
assert_eq!(textarea.end_of_buffer_character, '*');
}
#[test]
fn test_width_constraints() {
let mut textarea = new_text_area();
textarea.set_width(6); let val_after = send_string(textarea, "123");
let _ = val_after;
textarea = new_text_area();
textarea.max_width = 10;
textarea.set_width(20); assert!(textarea.width() <= 10);
textarea = new_text_area();
textarea.show_line_numbers = false;
textarea.set_width(6);
textarea = send_string(textarea, "123");
assert_eq!(textarea.value(), "123");
}
#[test]
fn test_placeholder_functionality() {
let mut textarea = new_text_area();
textarea.placeholder = "Enter text here...".to_string();
let _view = textarea.view();
textarea.placeholder = "Line 1\nLine 2\nLine 3".to_string();
let _view = textarea.view();
textarea.show_line_numbers = false;
textarea.end_of_buffer_character = '*';
let _view = textarea.view();
textarea.placeholder = "输入消息...".to_string();
let _view = textarea.view();
assert_eq!(textarea.placeholder, "输入消息...");
}
#[test]
fn test_soft_wrapping() {
let mut textarea = new_text_area();
textarea.show_line_numbers = false;
textarea.prompt = String::new();
textarea.set_width(5);
textarea = send_string(textarea, "foo bar baz");
assert_eq!(textarea.value(), "foo bar baz");
let _view = textarea.view();
}
#[test]
fn test_viewport_scrolling() {
let mut textarea = new_text_area();
textarea.prompt = String::new();
textarea.show_line_numbers = false;
textarea.set_height(1);
textarea.set_width(20);
textarea.char_limit = 100;
let input = "This is a really long line that should wrap around the text area.";
textarea = send_string(textarea, input);
let _view = textarea.view();
assert!(textarea.value().contains("This is a really"));
textarea.scroll_down(1);
let _view_after_scroll = textarea.view();
textarea.scroll_up(1);
let _view_back_up = textarea.view();
}
#[test]
fn test_dynamic_prompt_function() {
let mut textarea = new_text_area();
textarea.set_prompt_func(2, |line_num| format!("{}: ", line_num));
textarea.set_value("first\nsecond\nthird");
assert_eq!(textarea.prompt_width, 2);
}
#[test]
fn test_comprehensive_editing_operations() {
let mut textarea = new_text_area();
textarea.set_value("hello world test");
textarea.col = 5; textarea.delete_after_cursor();
assert_eq!(textarea.value(), "hello");
textarea.set_value("hello world");
textarea.col = 6; textarea.delete_before_cursor();
assert_eq!(textarea.value(), "world");
textarea.set_value("line1\nline2");
textarea.row = 1;
textarea.col = 0;
textarea.delete_character_backward(); assert_eq!(textarea.value(), "line1line2");
}
#[test]
fn test_advanced_cursor_movement() {
let mut textarea = new_text_area();
textarea.set_value("Hello World\nThis is a test\nFinal line");
textarea.move_to_begin();
assert_eq!(textarea.row, 0);
assert_eq!(textarea.col, 0);
textarea.move_to_end();
assert_eq!(textarea.row, 2);
assert_eq!(textarea.col, 10);
textarea.row = 1;
textarea.cursor_start();
assert_eq!(textarea.col, 0);
textarea.cursor_end();
assert_eq!(textarea.col, 14); }
#[test]
fn test_line_info_calculation_wrapped() {
let mut textarea = new_text_area();
textarea.set_width(10);
textarea.insert_string("hello world this is a very long line for testing");
textarea.col = 15;
let line_info = textarea.line_info();
assert!(line_info.width > 0);
assert!(line_info.height > 1); assert!(line_info.column_offset <= line_info.width);
}
#[test]
fn test_character_width_handling() {
let mut textarea = new_text_area();
textarea.set_value("你好世界");
assert_eq!(textarea.col, 4);
textarea.set_value("Hello 🌍 World");
assert!(textarea.col > 12);
textarea.set_value("你好你好\nHello");
textarea.row = 0;
textarea.col = 2;
let line_info = textarea.line_info();
assert!(line_info.char_offset > line_info.column_offset);
}
#[test]
fn test_memoization() {
let mut textarea = new_text_area();
textarea.set_width(10);
textarea.insert_string("this is a long line that should be wrapped");
let info1 = textarea.line_info();
let info2 = textarea.line_info();
assert_eq!(info1.width, info2.width);
assert_eq!(info1.height, info2.height);
}
#[test]
fn test_edge_cases() {
let mut textarea = new_text_area();
textarea.set_width(6); textarea.insert_string("test");
let view = textarea.view();
assert!(!view.is_empty(), "Zero-width textarea should still render");
textarea = new_text_area();
textarea.set_height(1);
textarea.insert_string("test");
let view = textarea.view();
assert!(!view.is_empty(), "Zero-height textarea should still render");
textarea = new_text_area();
textarea.insert_string("hello");
textarea.col = 1000;
textarea.cursor_start(); assert_eq!(textarea.col, 0);
textarea = new_text_area();
textarea.word_right(); textarea.word_left(); textarea.delete_word_backward(); textarea.delete_word_forward(); assert_eq!(textarea.col, 0);
assert_eq!(textarea.row, 0);
textarea = new_text_area();
textarea.cursor_up(); textarea.cursor_down(); textarea.cursor_start(); textarea.cursor_end(); assert_eq!(textarea.col, 0);
assert_eq!(textarea.row, 0);
textarea = new_text_area();
textarea.char_limit = 0; textarea.insert_string("test with no limit");
assert_eq!(textarea.value(), "test with no limit");
textarea = new_text_area();
textarea.char_limit = 1;
textarea.insert_string("test with limit");
assert_eq!(textarea.value(), "t");
textarea = new_text_area();
textarea.insert_string("a\u{200B}b"); let line_info = textarea.line_info();
assert!(line_info.width > 0);
textarea = new_text_area();
textarea.set_width(10);
let long_text = "a".repeat(100);
textarea.insert_string(&long_text);
let line_info = textarea.line_info();
assert!(line_info.height > 1);
textarea = new_text_area();
textarea.insert_string("line1\nline2\nline3");
textarea.row = 1000; textarea.col = 1000; textarea.move_to_begin(); assert_eq!(textarea.row, 0);
assert_eq!(textarea.col, 0);
textarea = new_text_area();
textarea.set_height(2);
textarea.insert_string("line1\nline2\nline3\nline4\nline5");
textarea.scroll_down(1000); textarea.scroll_up(1000); }
}