diff --git a/tests/input_navigation.rs b/tests/input_navigation.rs new file mode 100644 index 0000000..6eb5a42 --- /dev/null +++ b/tests/input_navigation.rs @@ -0,0 +1,310 @@ +//! Integration tests for input navigation +//! +//! Tests that keyboard navigation actually works through main_input handler + +mod helpers; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use helpers::app_builder::TestAppBuilder; +use helpers::test_data::{create_test_chat, TestMessageBuilder}; +use tele_tui::input::handle_main_input; + +fn key(code: KeyCode) -> KeyEvent { + KeyEvent::new(code, KeyModifiers::empty()) +} + +/// Test: Стрелки вверх/вниз навигация по списку чатов +#[tokio::test] +async fn test_arrow_navigation_in_chat_list() { + let mut app = TestAppBuilder::new() + .with_chats(vec![ + create_test_chat("Chat 1", 101), + create_test_chat("Chat 2", 102), + create_test_chat("Chat 3", 103), + ]) + .build(); + + // Начинаем с первого чата (индекс 0) + assert_eq!(app.chat_list_state.selected(), Some(0)); + + // Down - переходим на второй чат + handle_main_input(&mut app, key(KeyCode::Down)).await; + assert_eq!(app.chat_list_state.selected(), Some(1)); + + // Down - переходим на третий чат + handle_main_input(&mut app, key(KeyCode::Down)).await; + assert_eq!(app.chat_list_state.selected(), Some(2)); + + // Down - циклим обратно в начало (циклическая навигация) + handle_main_input(&mut app, key(KeyCode::Down)).await; + assert_eq!(app.chat_list_state.selected(), Some(0)); + + // Up - возвращаемся на второй + handle_main_input(&mut app, key(KeyCode::Up)).await; + assert_eq!(app.chat_list_state.selected(), Some(1)); + + // Up - возвращаемся на первый + handle_main_input(&mut app, key(KeyCode::Up)).await; + assert_eq!(app.chat_list_state.selected(), Some(0)); + + // Up - циклим в конец (циклическая навигация) + handle_main_input(&mut app, key(KeyCode::Up)).await; + assert_eq!(app.chat_list_state.selected(), Some(2)); +} + +/// Test: Vim-style j/k навигация по списку чатов +#[tokio::test] +async fn test_vim_navigation_in_chat_list() { + let mut app = TestAppBuilder::new() + .with_chats(vec![ + create_test_chat("Chat 1", 101), + create_test_chat("Chat 2", 102), + create_test_chat("Chat 3", 103), + ]) + .build(); + + assert_eq!(app.chat_list_state.selected(), Some(0)); + + // j - вниз + handle_main_input(&mut app, key(KeyCode::Char('j'))).await; + assert_eq!(app.chat_list_state.selected(), Some(1)); + + // j - ещё вниз + handle_main_input(&mut app, key(KeyCode::Char('j'))).await; + assert_eq!(app.chat_list_state.selected(), Some(2)); + + // k - вверх + handle_main_input(&mut app, key(KeyCode::Char('k'))).await; + assert_eq!(app.chat_list_state.selected(), Some(1)); + + // k - ещё вверх + handle_main_input(&mut app, key(KeyCode::Char('k'))).await; + assert_eq!(app.chat_list_state.selected(), Some(0)); +} + +/// Test: Русские клавиши о/р для навигации +#[tokio::test] +async fn test_russian_keyboard_navigation() { + let mut app = TestAppBuilder::new() + .with_chats(vec![ + create_test_chat("Chat 1", 101), + create_test_chat("Chat 2", 102), + ]) + .build(); + + assert_eq!(app.chat_list_state.selected(), Some(0)); + + // о (русская j) - вниз + handle_main_input(&mut app, key(KeyCode::Char('о'))).await; + assert_eq!(app.chat_list_state.selected(), Some(1)); + + // р (русская k) - вверх + handle_main_input(&mut app, key(KeyCode::Char('р'))).await; + assert_eq!(app.chat_list_state.selected(), Some(0)); +} + +/// Test: Enter открывает чат +#[tokio::test] +async fn test_enter_opens_chat() { + let mut app = TestAppBuilder::new() + .with_chats(vec![ + create_test_chat("Chat 1", 101), + create_test_chat("Chat 2", 102), + ]) + .build(); + + // Чат не открыт + assert_eq!(app.selected_chat_id, None); + assert_eq!(app.chat_list_state.selected(), Some(0)); + + // Enter - открываем первый чат + handle_main_input(&mut app, key(KeyCode::Enter)).await; + assert_eq!(app.selected_chat_id, Some(101.into())); +} + +/// Test: Esc закрывает чат +#[tokio::test] +async fn test_esc_closes_chat() { + let mut app = TestAppBuilder::new() + .with_chats(vec![create_test_chat("Chat 1", 101)]) + .selected_chat(101) + .build(); + + // Чат открыт + assert_eq!(app.selected_chat_id, Some(101.into())); + + // Esc - закрываем чат + handle_main_input(&mut app, key(KeyCode::Esc)).await; + assert_eq!(app.selected_chat_id, None); +} + +/// Test: Навигация курсором в поле ввода (Left/Right) +#[tokio::test] +async fn test_cursor_navigation_in_input() { + let mut app = TestAppBuilder::new() + .with_chats(vec![create_test_chat("Chat 1", 101)]) + .selected_chat(101) + .build(); + + // Вводим текст "Hello" + for c in "Hello".chars() { + handle_main_input(&mut app, key(KeyCode::Char(c))).await; + } + + assert_eq!(app.message_input, "Hello"); + assert_eq!(app.cursor_position, 5); // Курсор в конце + + // Left - курсор влево + handle_main_input(&mut app, key(KeyCode::Left)).await; + assert_eq!(app.cursor_position, 4); + + // Left - ещё влево + handle_main_input(&mut app, key(KeyCode::Left)).await; + assert_eq!(app.cursor_position, 3); + + // Right - курсор вправо + handle_main_input(&mut app, key(KeyCode::Right)).await; + assert_eq!(app.cursor_position, 4); + + // Right - ещё вправо + handle_main_input(&mut app, key(KeyCode::Right)).await; + assert_eq!(app.cursor_position, 5); + + // Right - на границе (не выходим за пределы) + handle_main_input(&mut app, key(KeyCode::Right)).await; + assert_eq!(app.cursor_position, 5); +} + +/// Test: Home/End навигация в поле ввода +#[tokio::test] +async fn test_home_end_in_input() { + let mut app = TestAppBuilder::new() + .with_chats(vec![create_test_chat("Chat 1", 101)]) + .selected_chat(101) + .build(); + + // Вводим текст + for c in "Hello World".chars() { + handle_main_input(&mut app, key(KeyCode::Char(c))).await; + } + + assert_eq!(app.cursor_position, 11); + + // Home - в начало + handle_main_input(&mut app, key(KeyCode::Home)).await; + assert_eq!(app.cursor_position, 0); + + // End - в конец + handle_main_input(&mut app, key(KeyCode::End)).await; + assert_eq!(app.cursor_position, 11); +} + +/// Test: Backspace удаляет символ перед курсором +#[tokio::test] +async fn test_backspace_with_cursor() { + let mut app = TestAppBuilder::new() + .with_chats(vec![create_test_chat("Chat 1", 101)]) + .selected_chat(101) + .build(); + + // Вводим "Hello" + for c in "Hello".chars() { + handle_main_input(&mut app, key(KeyCode::Char(c))).await; + } + + assert_eq!(app.message_input, "Hello"); + assert_eq!(app.cursor_position, 5); + + // Backspace - удаляем "o" + handle_main_input(&mut app, key(KeyCode::Backspace)).await; + assert_eq!(app.message_input, "Hell"); + assert_eq!(app.cursor_position, 4); + + // Перемещаем курсор в середину (после "e") + handle_main_input(&mut app, key(KeyCode::Left)).await; + handle_main_input(&mut app, key(KeyCode::Left)).await; + assert_eq!(app.cursor_position, 2); + + // Backspace - удаляем "e" + handle_main_input(&mut app, key(KeyCode::Backspace)).await; + assert_eq!(app.message_input, "Hll"); + assert_eq!(app.cursor_position, 1); +} + +/// Test: Ввод символа в середину текста +#[tokio::test] +async fn test_insert_char_at_cursor_position() { + let mut app = TestAppBuilder::new() + .with_chats(vec![create_test_chat("Chat 1", 101)]) + .selected_chat(101) + .build(); + + // Вводим "Hllo" + for c in "Hllo".chars() { + handle_main_input(&mut app, key(KeyCode::Char(c))).await; + } + + assert_eq!(app.message_input, "Hllo"); + + // Курсор на позицию 1 (после "H") + for _ in 0..3 { + handle_main_input(&mut app, key(KeyCode::Left)).await; + } + assert_eq!(app.cursor_position, 1); + + // Вставляем "e" + handle_main_input(&mut app, key(KeyCode::Char('e'))).await; + assert_eq!(app.message_input, "Hello"); + assert_eq!(app.cursor_position, 2); +} + +/// Test: Навигация вверх по сообщениям из пустого инпута +#[tokio::test] +async fn test_up_arrow_selects_last_message_when_input_empty() { + let mut app = TestAppBuilder::new() + .with_chats(vec![create_test_chat("Chat 1", 101)]) + .selected_chat(101) + .build(); + + // Добавляем сообщения + let messages = vec![ + TestMessageBuilder::new("Msg 1", 1).outgoing().build(), + TestMessageBuilder::new("Msg 2", 2).outgoing().build(), + TestMessageBuilder::new("Msg 3", 3).outgoing().build(), + ]; + app.td_client.message_manager.current_chat_messages = messages; + + // Инпут пустой + assert_eq!(app.message_input, ""); + + // Up - должен начать выбор сообщения (последнего) + handle_main_input(&mut app, key(KeyCode::Up)).await; + + // Проверяем что вошли в режим выбора сообщения + assert!(app.is_selecting_message()); +} + +/// Test: Циклическая навигация по списку чатов (переход с конца в начало) +#[tokio::test] +async fn test_circular_navigation_optional() { + let mut app = TestAppBuilder::new() + .with_chats(vec![ + create_test_chat("Chat 1", 101), + create_test_chat("Chat 2", 102), + ]) + .build(); + + // На первом чате + assert_eq!(app.chat_list_state.selected(), Some(0)); + + // j - на второй чат + handle_main_input(&mut app, key(KeyCode::Char('j'))).await; + assert_eq!(app.chat_list_state.selected(), Some(1)); + + // j - остаёмся на втором (или циклим в начало, зависит от реализации) + // В текущей реализации должны остаться на месте + handle_main_input(&mut app, key(KeyCode::Char('j'))).await; + // Может быть либо 1 (остались), либо 0 (циклились) + let selected = app.chat_list_state.selected(); + assert!(selected == Some(1) || selected == Some(0)); +}