use super::*;
#[test]
fn test_app_creation() -> Result<(), String> {
let app = App::new()?;
assert_eq!(app.editor.status(), "NORMAL");
assert!(!app.should_quit);
Ok(())
}
#[test]
fn test_mode_switching() -> Result<(), String> {
let mut app = App::new()?;
app.handle_key(KeyEvent::from(KeyCode::Char('i')));
assert_eq!(app.editor.status(), "INSERT");
app.handle_key(KeyEvent::from(KeyCode::Esc));
assert_eq!(app.editor.status(), "NORMAL");
Ok(())
}
#[test]
fn test_insert_mode_typing() -> Result<(), String> {
let mut app = App::new()?;
app.handle_key(KeyEvent::from(KeyCode::Char('i')));
app.handle_key(KeyEvent::from(KeyCode::Char('h')));
app.handle_key(KeyEvent::from(KeyCode::Char('i')));
assert_eq!(app.repl_state.input, "hi");
Ok(())
}
#[test]
fn test_normal_mode_navigation() -> Result<(), String> {
let mut app = App::new()?;
app.handle_key(KeyEvent::from(KeyCode::Char('i')));
for c in "hello".chars() {
app.handle_key(KeyEvent::from(KeyCode::Char(c)));
}
app.handle_key(KeyEvent::from(KeyCode::Esc));
app.handle_key(KeyEvent::from(KeyCode::Char('0')));
assert_eq!(app.repl_state.cursor, 0);
app.handle_key(KeyEvent::from(KeyCode::Char('l')));
assert_eq!(app.repl_state.cursor, 1);
app.handle_key(KeyEvent::from(KeyCode::Char('h')));
assert_eq!(app.repl_state.cursor, 0);
app.handle_key(KeyEvent::from(KeyCode::Char('$')));
assert_eq!(app.repl_state.cursor, 4); Ok(())
}
#[test]
fn test_history_navigation() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state
.add_entry(HistoryEntry::new("first").with_output("1"));
app.repl_state
.add_entry(HistoryEntry::new("second").with_output("2"));
app.handle_key(KeyEvent::from(KeyCode::Up));
assert_eq!(app.repl_state.input, "second");
app.handle_key(KeyEvent::from(KeyCode::Up));
assert_eq!(app.repl_state.input, "first");
app.handle_key(KeyEvent::from(KeyCode::Down));
assert_eq!(app.repl_state.input, "second");
Ok(())
}
#[test]
fn test_jk_history_navigation() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state
.add_entry(HistoryEntry::new("first").with_output("1"));
app.repl_state
.add_entry(HistoryEntry::new("second").with_output("2"));
app.handle_key(KeyEvent::from(KeyCode::Char('k')));
assert_eq!(app.repl_state.input, "second");
app.handle_key(KeyEvent::from(KeyCode::Char('k')));
assert_eq!(app.repl_state.input, "first");
app.handle_key(KeyEvent::from(KeyCode::Char('j')));
assert_eq!(app.repl_state.input, "second");
app.handle_key(KeyEvent::from(KeyCode::Char('j')));
assert_eq!(app.repl_state.input, "");
Ok(())
}
#[test]
fn test_jk_multiline_navigation() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state
.add_entry(HistoryEntry::new("oldest").with_output("1"));
app.repl_state
.add_entry(HistoryEntry::new("line1\nline2\nline3").with_output("2"));
app.handle_key(KeyEvent::from(KeyCode::Char('k'))); assert_eq!(app.repl_state.input, "line1\nline2\nline3");
app.handle_key(KeyEvent::from(KeyCode::Char('j')));
assert_eq!(app.repl_state.input, "");
app.handle_key(KeyEvent::from(KeyCode::Char('k')));
assert_eq!(app.repl_state.input, "line1\nline2\nline3");
app.handle_key(KeyEvent::from(KeyCode::Char('0')));
app.handle_key(KeyEvent::from(KeyCode::Char('k')));
assert_eq!(app.repl_state.input, "line1\nline2\nline3");
app.handle_key(KeyEvent::from(KeyCode::Char('k')));
assert_eq!(app.repl_state.input, "line1\nline2\nline3");
app.handle_key(KeyEvent::from(KeyCode::Char('k')));
assert_eq!(app.repl_state.input, "oldest");
Ok(())
}
#[test]
fn test_jk_with_unicode() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state
.add_entry(HistoryEntry::new("hello 👋").with_output("1"));
app.handle_key(KeyEvent::from(KeyCode::Char('k')));
assert_eq!(app.repl_state.input, "hello 👋");
app.handle_key(KeyEvent::from(KeyCode::Char('j')));
assert_eq!(app.repl_state.input, "");
Ok(())
}
#[test]
fn test_jk_empty_input() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state
.add_entry(HistoryEntry::new("history").with_output("1"));
assert_eq!(app.repl_state.input, "");
app.handle_key(KeyEvent::from(KeyCode::Char('k'))); assert_eq!(app.repl_state.input, "history");
app.handle_key(KeyEvent::from(KeyCode::Char('j'))); assert_eq!(app.repl_state.input, "");
Ok(())
}
#[test]
fn test_jk_insert_mode_no_history() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state
.add_entry(HistoryEntry::new("history").with_output("1"));
app.handle_key(KeyEvent::from(KeyCode::Char('i')));
assert_eq!(app.editor.status(), "INSERT");
app.handle_key(KeyEvent::from(KeyCode::Char('j')));
assert_eq!(app.repl_state.input, "j");
app.handle_key(KeyEvent::from(KeyCode::Char('k')));
assert_eq!(app.repl_state.input, "jk");
Ok(())
}
#[test]
fn test_quit_command() -> Result<(), String> {
let mut app = App::new()?;
app.handle_key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL));
assert!(app.should_quit);
Ok(())
}
#[test]
fn test_repl_command() -> Result<(), String> {
let mut app = App::new()?;
app.handle_key(KeyEvent::from(KeyCode::Char('i')));
app.handle_key(KeyEvent::from(KeyCode::Char(':')));
app.handle_key(KeyEvent::from(KeyCode::Char('q')));
app.handle_key(KeyEvent::from(KeyCode::Enter));
assert!(app.should_quit);
Ok(())
}
#[test]
fn test_word_effect_lookup() -> Result<(), String> {
let app = App::new()?;
assert!(app.get_word_effect("dup").is_some());
assert!(app.get_word_effect("swap").is_some());
assert!(app.get_word_effect("i.add").is_some());
assert!(app.get_word_effect("i.+").is_some());
assert!(app.get_word_effect("i.multiply").is_some());
assert!(app.get_word_effect("i.*").is_some());
assert!(app.get_word_effect("i.<").is_some());
assert!(app.get_word_effect("i.=").is_some());
assert!(app.get_word_effect("f.add").is_some());
assert!(app.get_word_effect("f.*").is_some());
assert!(app.get_word_effect("f.<").is_some());
assert!(app.get_word_effect("unknown").is_none());
Ok(())
}
#[test]
fn test_ctrl_c_quits() -> Result<(), String> {
let mut app = App::new()?;
let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
app.handle_key(key);
assert!(app.should_quit);
Ok(())
}
#[test]
fn test_tab_completion() -> Result<(), String> {
let mut app = App::new()?;
app.handle_key(KeyEvent::from(KeyCode::Char('i')));
app.handle_key(KeyEvent::from(KeyCode::Char('d')));
app.handle_key(KeyEvent::from(KeyCode::Char('u')));
assert_eq!(app.repl_state.input, "du");
assert_eq!(app.repl_state.cursor, 2);
app.handle_key(KeyEvent::from(KeyCode::Tab));
assert!(
app.completions.is_visible(),
"Completions should be visible after Tab"
);
assert!(
!app.completions.items().is_empty(),
"Should have completions for 'du'"
);
assert!(
app.completions.items().iter().any(|c| c.label == "dup"),
"Should include 'dup' in completions"
);
Ok(())
}
#[test]
fn test_tab_completion_empty_prefix() -> Result<(), String> {
let mut app = App::new()?;
app.handle_key(KeyEvent::from(KeyCode::Tab));
assert!(
!app.completions.is_visible(),
"Should not show completions for empty prefix"
);
assert!(
app.status_message
.as_ref()
.is_some_and(|m| m.contains("type a prefix")),
"Should show 'type a prefix' message"
);
Ok(())
}
#[test]
fn test_search_mode_enter_and_exit() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state
.add_entry(HistoryEntry::new("first entry").with_output("1"));
app.repl_state
.add_entry(HistoryEntry::new("second entry").with_output("2"));
assert!(!app.search_mode);
app.handle_key(KeyEvent::from(KeyCode::Char('/')));
assert!(app.search_mode);
assert!(
app.status_message
.as_ref()
.is_some_and(|m| m.starts_with("/")),
"Status should show search prompt"
);
app.handle_key(KeyEvent::from(KeyCode::Esc));
assert!(!app.search_mode);
Ok(())
}
#[test]
fn test_search_mode_filtering() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state.history.clear();
app.repl_state
.add_entry(HistoryEntry::new("dup swap").with_output("1"));
app.repl_state
.add_entry(HistoryEntry::new("drop").with_output("2"));
app.repl_state
.add_entry(HistoryEntry::new("dup dup").with_output("3"));
app.handle_key(KeyEvent::from(KeyCode::Char('/')));
assert!(app.search_mode);
app.handle_key(KeyEvent::from(KeyCode::Char('d')));
app.handle_key(KeyEvent::from(KeyCode::Char('u')));
app.handle_key(KeyEvent::from(KeyCode::Char('p')));
assert_eq!(app.search_matches.len(), 2);
assert_eq!(app.repl_state.input, "dup dup");
assert!(
app.status_message
.as_ref()
.is_some_and(|m| m.contains("/dup") && m.contains("(1/2)")),
"Status should show search pattern and match count"
);
Ok(())
}
#[test]
fn test_search_mode_accept() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state
.add_entry(HistoryEntry::new("first").with_output("1"));
app.repl_state
.add_entry(HistoryEntry::new("second").with_output("2"));
app.handle_key(KeyEvent::from(KeyCode::Char('/')));
for c in "first".chars() {
app.handle_key(KeyEvent::from(KeyCode::Char(c)));
}
assert_eq!(app.repl_state.input, "first");
app.handle_key(KeyEvent::from(KeyCode::Enter));
assert!(!app.search_mode);
assert_eq!(app.repl_state.input, "first");
Ok(())
}
#[test]
fn test_search_mode_cancel_restores_input() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state
.add_entry(HistoryEntry::new("history entry").with_output("1"));
app.handle_key(KeyEvent::from(KeyCode::Char('i')));
for c in "my input".chars() {
app.handle_key(KeyEvent::from(KeyCode::Char(c)));
}
app.handle_key(KeyEvent::from(KeyCode::Esc));
assert_eq!(app.repl_state.input, "my input");
app.handle_key(KeyEvent::from(KeyCode::Char('/')));
for c in "history".chars() {
app.handle_key(KeyEvent::from(KeyCode::Char(c)));
}
assert_eq!(app.repl_state.input, "history entry");
app.handle_key(KeyEvent::from(KeyCode::Esc));
assert!(!app.search_mode);
assert_eq!(app.repl_state.input, "my input");
Ok(())
}
#[test]
fn test_search_mode_navigate_matches() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state.history.clear();
app.repl_state
.add_entry(HistoryEntry::new("test1").with_output("1"));
app.repl_state
.add_entry(HistoryEntry::new("test2").with_output("2"));
app.repl_state
.add_entry(HistoryEntry::new("test3").with_output("3"));
app.handle_key(KeyEvent::from(KeyCode::Char('/')));
for c in "test".chars() {
app.handle_key(KeyEvent::from(KeyCode::Char(c)));
}
assert_eq!(app.search_matches.len(), 3);
assert_eq!(app.search_match_index, 0);
assert_eq!(app.repl_state.input, "test3");
app.handle_key(KeyEvent::from(KeyCode::Tab));
assert_eq!(app.search_match_index, 1);
assert_eq!(app.repl_state.input, "test2");
app.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT));
assert_eq!(app.search_match_index, 0);
assert_eq!(app.repl_state.input, "test3");
app.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT));
assert_eq!(app.search_match_index, 2);
assert_eq!(app.repl_state.input, "test1");
Ok(())
}
#[test]
fn test_search_mode_backspace() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state.history.clear();
app.repl_state
.add_entry(HistoryEntry::new("dup").with_output("1"));
app.repl_state
.add_entry(HistoryEntry::new("drop").with_output("2"));
app.handle_key(KeyEvent::from(KeyCode::Char('/')));
for c in "dup".chars() {
app.handle_key(KeyEvent::from(KeyCode::Char(c)));
}
assert_eq!(app.search_pattern, "dup");
assert_eq!(app.search_matches.len(), 1);
app.handle_key(KeyEvent::from(KeyCode::Backspace));
assert_eq!(app.search_pattern, "du");
assert_eq!(app.search_matches.len(), 1);
Ok(())
}
#[test]
fn test_search_mode_case_insensitive() -> Result<(), String> {
let mut app = App::new()?;
app.repl_state.history.clear();
app.repl_state
.add_entry(HistoryEntry::new("DUP").with_output("1"));
app.repl_state
.add_entry(HistoryEntry::new("Dup").with_output("2"));
app.repl_state
.add_entry(HistoryEntry::new("dup").with_output("3"));
app.handle_key(KeyEvent::from(KeyCode::Char('/')));
for c in "dup".chars() {
app.handle_key(KeyEvent::from(KeyCode::Char(c)));
}
assert_eq!(app.search_matches.len(), 3);
Ok(())
}
#[test]
fn test_search_not_in_insert_mode() -> Result<(), String> {
let mut app = App::new()?;
app.handle_key(KeyEvent::from(KeyCode::Char('i')));
assert_eq!(app.editor.status(), "INSERT");
app.handle_key(KeyEvent::from(KeyCode::Char('/')));
assert!(!app.search_mode);
assert_eq!(app.repl_state.input, "/");
Ok(())
}