//! Tests for Vim Normal/Insert mode feature //! //! Covers: //! - Mode transitions (i→Insert, Esc→Normal, auto-Insert on Reply/Edit) //! - Command blocking in Insert mode (vim keys type text) //! - Insert mode input handling (NewLine, DeleteWord, MoveToStart, MoveToEnd) //! - Close chat resets mode //! - Edge cases (Esc cancels Reply/Editing from Insert) mod helpers; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use helpers::app_builder::TestAppBuilder; use helpers::test_data::{create_test_chat, TestMessageBuilder}; use tele_tui::app::methods::compose::ComposeMethods; use tele_tui::app::methods::messages::MessageMethods; use tele_tui::app::InputMode; use tele_tui::input::handle_main_input; fn key(code: KeyCode) -> KeyEvent { KeyEvent::new(code, KeyModifiers::empty()) } fn ctrl_key(c: char) -> KeyEvent { KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL) } // ============================================================ // Mode Transitions // ============================================================ /// `i` в Normal mode → переход в Insert mode #[tokio::test] async fn test_i_enters_insert_mode() { let messages = vec![TestMessageBuilder::new("Hello", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .selecting_message(0) .build(); assert_eq!(app.input_mode, InputMode::Normal); handle_main_input(&mut app, key(KeyCode::Char('i'))).await; assert_eq!(app.input_mode, InputMode::Insert); // Выходим из MessageSelection assert!(!app.is_selecting_message()); } /// `ш` (русская i) в Normal mode → переход в Insert mode #[tokio::test] async fn test_russian_i_enters_insert_mode() { let messages = vec![TestMessageBuilder::new("Hello", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .selecting_message(0) .build(); handle_main_input(&mut app, key(KeyCode::Char('ш'))).await; assert_eq!(app.input_mode, InputMode::Insert); } /// Esc в Insert mode → Normal mode + MessageSelection #[tokio::test] async fn test_esc_exits_insert_mode() { let messages = vec![TestMessageBuilder::new("Hello", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .insert_mode() .build(); assert_eq!(app.input_mode, InputMode::Insert); handle_main_input(&mut app, key(KeyCode::Esc)).await; assert_eq!(app.input_mode, InputMode::Normal); assert!(app.is_selecting_message()); } /// Esc в Normal mode → закрывает чат #[tokio::test] async fn test_esc_in_normal_closes_chat() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .build(); assert!(app.selected_chat_id.is_some()); handle_main_input(&mut app, key(KeyCode::Esc)).await; assert!(app.selected_chat_id.is_none()); assert_eq!(app.input_mode, InputMode::Normal); } /// close_chat() сбрасывает input_mode #[tokio::test] async fn test_close_chat_resets_input_mode() { use tele_tui::app::methods::navigation::NavigationMethods; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .build(); assert_eq!(app.input_mode, InputMode::Insert); app.close_chat(); assert_eq!(app.input_mode, InputMode::Normal); } /// Auto-Insert при Reply (`r` в MessageSelection) #[tokio::test] async fn test_reply_auto_enters_insert_mode() { let messages = vec![TestMessageBuilder::new("Hello from friend", 1) .sender("Friend") .build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .selecting_message(0) .build(); assert_eq!(app.input_mode, InputMode::Normal); // `r` → reply handle_main_input(&mut app, key(KeyCode::Char('r'))).await; assert_eq!(app.input_mode, InputMode::Insert); assert!(app.is_replying()); } /// Auto-Insert при Edit (Enter в MessageSelection) #[tokio::test] async fn test_edit_auto_enters_insert_mode() { let messages = vec![TestMessageBuilder::new("My message", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .selecting_message(0) .build(); assert_eq!(app.input_mode, InputMode::Normal); // Enter → edit selected message handle_main_input(&mut app, key(KeyCode::Enter)).await; assert_eq!(app.input_mode, InputMode::Insert); assert!(app.is_editing()); } /// При открытии чата → Normal mode (selected_chat задан builder'ом, как после open) #[test] fn test_open_chat_defaults_to_normal_mode() { // Проверяем что при настройке чата (аналог состояния после open_chat_and_load_data) // режим = Normal, и start_message_selection() корректно входит в MessageSelection let messages = vec![ TestMessageBuilder::new("Msg 1", 1).build(), TestMessageBuilder::new("Msg 2", 2).build(), ]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .build(); assert_eq!(app.input_mode, InputMode::Normal); assert!(app.selected_chat_id.is_some()); // open_chat_and_load_data вызывает start_message_selection() app.start_message_selection(); assert!(app.is_selecting_message()); } /// После отправки сообщения — остаёмся в Insert #[tokio::test] async fn test_send_message_stays_in_insert() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .message_input("Hello!") .build(); app.cursor_position = 6; assert_eq!(app.input_mode, InputMode::Insert); // Enter → отправить handle_main_input(&mut app, key(KeyCode::Enter)).await; // Остаёмся в Insert assert_eq!(app.input_mode, InputMode::Insert); // Инпут очищен assert_eq!(app.message_input, ""); } // ============================================================ // Command Blocking in Insert Mode // ============================================================ /// `j` в Insert mode → набирает символ, НЕ навигация #[tokio::test] async fn test_j_types_in_insert_mode() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .build(); handle_main_input(&mut app, key(KeyCode::Char('j'))).await; assert_eq!(app.message_input, "j"); } /// `k` в Insert mode → набирает символ #[tokio::test] async fn test_k_types_in_insert_mode() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .build(); handle_main_input(&mut app, key(KeyCode::Char('k'))).await; assert_eq!(app.message_input, "k"); } /// `d` в Insert mode → набирает "d", НЕ удаляет сообщение #[tokio::test] async fn test_d_types_in_insert_mode() { let messages = vec![TestMessageBuilder::new("Hello", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .insert_mode() .build(); handle_main_input(&mut app, key(KeyCode::Char('d'))).await; assert_eq!(app.message_input, "d"); // НЕ вошли в delete confirmation assert!(!app.chat_state.is_delete_confirmation()); } /// `r` в Insert mode → набирает "r", НЕ reply #[tokio::test] async fn test_r_types_in_insert_mode() { let messages = vec![TestMessageBuilder::new("Hello", 1).build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .insert_mode() .build(); handle_main_input(&mut app, key(KeyCode::Char('r'))).await; assert_eq!(app.message_input, "r"); assert!(!app.is_replying()); } /// `f` в Insert mode → набирает "f", НЕ forward #[tokio::test] async fn test_f_types_in_insert_mode() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .build(); handle_main_input(&mut app, key(KeyCode::Char('f'))).await; assert_eq!(app.message_input, "f"); assert!(!app.is_forwarding()); } /// `q` в Insert mode → набирает "q", НЕ quit #[tokio::test] async fn test_q_types_in_insert_mode() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .build(); handle_main_input(&mut app, key(KeyCode::Char('q'))).await; assert_eq!(app.message_input, "q"); } /// Ctrl+S в Insert mode → НЕ открывает поиск #[tokio::test] async fn test_ctrl_s_blocked_in_insert_mode() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .build(); handle_main_input(&mut app, ctrl_key('s')).await; assert!(!app.is_searching); } /// Ctrl+F в Insert mode → НЕ открывает поиск по сообщениям #[tokio::test] async fn test_ctrl_f_blocked_in_insert_mode() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .build(); handle_main_input(&mut app, ctrl_key('f')).await; assert!(!app.chat_state.is_search_in_chat()); } // ============================================================ // Normal Mode — commands work // ============================================================ /// `j` в Normal mode → навигация вниз (MoveDown) в MessageSelection #[tokio::test] async fn test_j_navigates_in_normal_mode() { let messages = vec![ TestMessageBuilder::new("Msg 1", 1).build(), TestMessageBuilder::new("Msg 2", 2).build(), TestMessageBuilder::new("Msg 3", 3).build(), ]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .selecting_message(1) .build(); assert_eq!(app.input_mode, InputMode::Normal); assert_eq!(app.chat_state.selected_message_index(), Some(1)); handle_main_input(&mut app, key(KeyCode::Char('j'))).await; // j = MoveDown = select_next_message assert_eq!(app.chat_state.selected_message_index(), Some(2)); // Текст НЕ добавился assert_eq!(app.message_input, ""); } /// `k` в Normal mode → навигация вверх в MessageSelection #[tokio::test] async fn test_k_navigates_in_normal_mode() { let messages = vec![ TestMessageBuilder::new("Msg 1", 1).build(), TestMessageBuilder::new("Msg 2", 2).build(), TestMessageBuilder::new("Msg 3", 3).build(), ]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .selecting_message(2) .build(); handle_main_input(&mut app, key(KeyCode::Char('k'))).await; assert_eq!(app.chat_state.selected_message_index(), Some(1)); assert_eq!(app.message_input, ""); } /// `d` в Normal mode → показывает подтверждение удаления #[tokio::test] async fn test_d_deletes_in_normal_mode() { let messages = vec![TestMessageBuilder::new("My message", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .selecting_message(0) .build(); handle_main_input(&mut app, key(KeyCode::Char('d'))).await; assert!(app.chat_state.is_delete_confirmation()); } // ============================================================ // Insert Mode Input Handling // ============================================================ /// Ctrl+W → удаляет слово в Insert mode #[tokio::test] async fn test_ctrl_w_deletes_word_in_insert() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .message_input("Hello World") .build(); app.cursor_position = 11; // конец "Hello World" handle_main_input(&mut app, ctrl_key('w')).await; assert_eq!(app.message_input, "Hello "); assert_eq!(app.cursor_position, 6); } /// Ctrl+W → удаляет слово + пробелы перед ним #[tokio::test] async fn test_ctrl_w_deletes_word_with_spaces() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .message_input("one two three") .build(); app.cursor_position = 14; // конец handle_main_input(&mut app, ctrl_key('w')).await; // "one two " → удалили "three", осталось "one two " assert_eq!(app.message_input, "one two "); assert_eq!(app.cursor_position, 9); } /// Ctrl+A → курсор в начало в Insert mode #[tokio::test] async fn test_ctrl_a_moves_to_start_in_insert() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .message_input("Hello World") .build(); app.cursor_position = 11; handle_main_input(&mut app, ctrl_key('a')).await; assert_eq!(app.cursor_position, 0); } /// Ctrl+E → курсор в конец в Insert mode #[tokio::test] async fn test_ctrl_e_moves_to_end_in_insert() { let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .insert_mode() .message_input("Hello World") .build(); app.cursor_position = 0; handle_main_input(&mut app, ctrl_key('e')).await; assert_eq!(app.cursor_position, 11); } // ============================================================ // Edge Cases — Esc from Insert cancels Reply/Editing // ============================================================ /// Esc из Insert при активном Reply → отменяет reply + Normal + MessageSelection #[tokio::test] async fn test_esc_from_insert_cancels_reply() { let messages = vec![TestMessageBuilder::new("Hello", 1).sender("Friend").build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .insert_mode() .replying_to(1) .build(); assert!(app.is_replying()); assert_eq!(app.input_mode, InputMode::Insert); handle_main_input(&mut app, key(KeyCode::Esc)).await; assert!(!app.is_replying()); assert_eq!(app.input_mode, InputMode::Normal); assert!(app.is_selecting_message()); } /// Esc из Insert при активном Editing → отменяет editing + Normal + MessageSelection #[tokio::test] async fn test_esc_from_insert_cancels_editing() { let messages = vec![TestMessageBuilder::new("My message", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .insert_mode() .editing_message(1, 0) .message_input("Edited text") .build(); assert!(app.is_editing()); assert_eq!(app.input_mode, InputMode::Insert); handle_main_input(&mut app, key(KeyCode::Esc)).await; assert!(!app.is_editing()); assert_eq!(app.input_mode, InputMode::Normal); assert!(app.is_selecting_message()); // Инпут очищен (cancel_editing) assert_eq!(app.message_input, ""); } /// Normal mode auto-enters MessageSelection при первом нажатии /// Используем `k` (MoveUp), т.к. `j` (MoveDown) на последнем сообщении выходит из selection #[tokio::test] async fn test_normal_mode_auto_enters_selection_on_any_key() { let messages = vec![ TestMessageBuilder::new("Msg 1", 1).build(), TestMessageBuilder::new("Msg 2", 2).build(), ]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .build(); // ChatState::Normal, InputMode::Normal — не в MessageSelection assert!(!app.is_selecting_message()); // `k` (MoveUp) в Normal mode → auto-enter MessageSelection + move up handle_main_input(&mut app, key(KeyCode::Char('k'))).await; assert!(app.is_selecting_message()); // Начали с последнего (index 1), MoveUp → index 0 assert_eq!(app.chat_state.selected_message_index(), Some(0)); } /// Полный цикл: Normal → i → набор текста → Esc → Normal #[tokio::test] async fn test_full_mode_cycle() { let messages = vec![TestMessageBuilder::new("Msg", 1).build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .selecting_message(0) .build(); // 1. Normal mode assert_eq!(app.input_mode, InputMode::Normal); assert!(app.is_selecting_message()); // 2. i → Insert handle_main_input(&mut app, key(KeyCode::Char('i'))).await; assert_eq!(app.input_mode, InputMode::Insert); assert!(!app.is_selecting_message()); // 3. Набираем текст handle_main_input(&mut app, key(KeyCode::Char('H'))).await; handle_main_input(&mut app, key(KeyCode::Char('i'))).await; assert_eq!(app.message_input, "Hi"); // 4. Esc → Normal + MessageSelection handle_main_input(&mut app, key(KeyCode::Esc)).await; assert_eq!(app.input_mode, InputMode::Normal); assert!(app.is_selecting_message()); // Текст сохранён (черновик) assert_eq!(app.message_input, "Hi"); } /// Полный цикл: Normal → r (reply) → набор → Enter (отправка) → остаёмся в Insert #[tokio::test] async fn test_reply_send_stays_insert() { let messages = vec![TestMessageBuilder::new("Question?", 1) .sender("Friend") .build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) .with_messages(101, messages) .selecting_message(0) .build(); // 1. r → auto-Insert + Reply handle_main_input(&mut app, key(KeyCode::Char('r'))).await; assert_eq!(app.input_mode, InputMode::Insert); assert!(app.is_replying()); // 2. Набираем ответ for c in "Yes!".chars() { handle_main_input(&mut app, key(KeyCode::Char(c))).await; } assert_eq!(app.message_input, "Yes!"); // 3. Enter → отправить handle_main_input(&mut app, key(KeyCode::Enter)).await; // Остаёмся в Insert после отправки assert_eq!(app.input_mode, InputMode::Insert); assert_eq!(app.message_input, ""); }