use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::interactive_tui::state::arrow_belongs_to_history;
fn arrow(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn typed_multi_line_draft_keeps_arrows_until_the_caret_hits_an_edge() {
let state = test_tui_state();
let editor = state.editor.clone();
push_history(&state, "an older prompt");
editor.set_text("first line\nsecond line");
editor.set_cursor(1, 3);
assert!(arrow_belongs_to_history(&state, &editor, 1));
assert!(!arrow_belongs_to_history(&state, &editor, -1));
editor.handle_key(arrow(KeyCode::Up));
assert_eq!(editor.cursor_position(), (0, 3), "↑ must reach line 1");
assert!(arrow_belongs_to_history(&state, &editor, -1));
assert!(!arrow_belongs_to_history(&state, &editor, 1));
editor.handle_key(arrow(KeyCode::Down));
assert_eq!(editor.cursor_position(), (1, 3), "↓ must reach line 2");
assert_eq!(
editor.get_text(),
"first line\nsecond line",
"arrow navigation must never replace the draft"
);
assert_eq!(*state.history_index.lock().unwrap(), -1);
}
#[test]
fn recalled_multi_line_prompt_keeps_its_lines_reachable_with_arrows() {
let state = test_tui_state();
let editor = state.editor.clone();
push_history(&state, "line one\nline two");
assert!(arrow_belongs_to_history(&state, &editor, -1));
navigate_history(&state, &editor, -1);
assert_eq!(*state.history_index.lock().unwrap(), 0);
assert_eq!(
editor.cursor_position(),
(1, "line two".len()),
"a recalled prompt must be edited from its tail, not from line 1"
);
assert!(!arrow_belongs_to_history(&state, &editor, -1));
editor.handle_key(arrow(KeyCode::Up));
assert_eq!(editor.cursor_position(), (0, "line one".len()));
assert_eq!(
editor.get_text(),
"line one\nline two",
"↑ inside the recalled entry must not swap it for another history item"
);
assert!(arrow_belongs_to_history(&state, &editor, -1));
navigate_history(&state, &editor, -1);
assert_eq!(*state.history_index.lock().unwrap(), 0);
assert_eq!(editor.get_text(), "line one\nline two");
}
#[test]
fn a_single_line_draft_never_hands_the_arrows_to_history() {
let state = test_tui_state();
let editor = state.editor.clone();
push_history(&state, "recalled");
editor.set_text("my draft");
editor.set_cursor(0, 0);
assert!(!arrow_belongs_to_history(&state, &editor, -1));
assert!(!arrow_belongs_to_history(&state, &editor, 1));
assert_eq!(*state.history_index.lock().unwrap(), -1);
assert_eq!(editor.get_text(), "my draft");
editor.set_text("");
assert!(arrow_belongs_to_history(&state, &editor, -1));
navigate_history(&state, &editor, -1);
assert_eq!(editor.get_text(), "recalled");
navigate_history(&state, &editor, 1);
assert_eq!(editor.get_text(), "");
assert_eq!(*state.history_index.lock().unwrap(), -1);
}
#[test]
fn arrows_stay_with_the_editor_when_there_is_no_history() {
let state = test_tui_state();
let editor = state.editor.clone();
editor.set_text("only line");
editor.set_cursor(0, 0);
assert!(!arrow_belongs_to_history(&state, &editor, -1));
assert!(!arrow_belongs_to_history(&state, &editor, 1));
assert_eq!(*state.history_index.lock().unwrap(), -1);
}
#[test]
fn submitting_a_recalled_entry_always_ends_the_browse_session() {
let state = test_tui_state();
let editor = state.editor.clone();
push_history(&state, "the only prompt");
navigate_history(&state, &editor, -1);
assert_eq!(*state.history_index.lock().unwrap(), 0);
push_history(&state, "the only prompt");
assert_eq!(state.history.lock().unwrap().len(), 1);
assert_eq!(
*state.history_index.lock().unwrap(),
-1,
"a submit must end the browse session even when history did not grow"
);
assert!(state.history_draft.lock().unwrap().is_none());
}
#[test]
fn arrows_over_a_recalled_prompt_keep_its_line_breaks_on_submit() {
let state = test_tui_state();
let editor = state.editor.clone();
let submitted = Arc::new(std::sync::Mutex::new(Vec::new()));
{
let sink = submitted.clone();
editor.on_submit(Arc::new(move |text: &str| {
sink.lock().unwrap().push(text.to_string());
}));
}
push_history(&state, "alpha\nbeta");
navigate_history(&state, &editor, -1);
editor.handle_key(arrow(KeyCode::Up));
editor.handle_key(arrow(KeyCode::Down));
editor.handle_key(arrow(KeyCode::Enter));
assert_eq!(
submitted.lock().unwrap().as_slice(),
["alpha\nbeta"],
"the submitted prompt must still carry both lines"
);
}
#[test]
fn single_line_recall_continues_browsing_with_both_arrows() {
let state = test_tui_state();
let editor = state.editor.clone();
push_history(&state, "older prompt");
push_history(&state, "newest prompt");
assert!(arrow_belongs_to_history(&state, &editor, -1));
navigate_history(&state, &editor, -1);
assert_eq!(editor.get_text(), "newest prompt");
assert_eq!(*state.history_index.lock().unwrap(), 0);
assert!(
arrow_belongs_to_history(&state, &editor, -1),
"↑ during an active browse session must step history, not the caret"
);
navigate_history(&state, &editor, -1);
assert_eq!(editor.get_text(), "older prompt");
assert!(
arrow_belongs_to_history(&state, &editor, 1),
"↓ during an active browse session must stay in history"
);
navigate_history(&state, &editor, 1);
assert_eq!(editor.get_text(), "newest prompt");
assert!(arrow_belongs_to_history(&state, &editor, 1));
navigate_history(&state, &editor, 1);
assert_eq!(editor.get_text(), "");
assert_eq!(*state.history_index.lock().unwrap(), -1);
}
#[test]
fn typed_single_line_draft_is_still_never_replaced() {
let state = test_tui_state();
let editor = state.editor.clone();
push_history(&state, "some prompt");
editor.set_text("my draft");
editor.set_cursor(0, 4);
assert!(!arrow_belongs_to_history(&state, &editor, -1));
assert!(!arrow_belongs_to_history(&state, &editor, 1));
assert_eq!(*state.history_index.lock().unwrap(), -1);
assert_eq!(editor.get_text(), "my draft");
}