//! 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::app::methods::messages::MessageMethods; 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(2)); // 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)); } /// 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) .insert_mode() .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) .insert_mode() .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) .insert_mode() .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) .insert_mode() .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: Normal mode автоматически входит в MessageSelection #[tokio::test] async fn test_normal_mode_auto_enters_message_selection() { let messages = vec![ TestMessageBuilder::new("Msg 1", 1).outgoing().build(), TestMessageBuilder::new("Msg 2", 2).outgoing().build(), TestMessageBuilder::new("Msg 3", 3).outgoing().build(), ]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat 1", 101)]) .selected_chat(101) .with_messages(101, messages) .build(); // Инпут пустой, Normal mode assert_eq!(app.message_input, ""); // Любая клавиша в Normal mode — auto-enters MessageSelection handle_main_input(&mut app, key(KeyCode::Up)).await; // Проверяем что вошли в режим выбора сообщения assert!(app.is_selecting_message()); } /// Test: j/k перескакивают через альбом как одно сообщение #[tokio::test] async fn test_album_navigation_skips_grouped_messages() { let messages = vec![ TestMessageBuilder::new("Before album", 1).sender("Alice").build(), TestMessageBuilder::new("Photo 1", 2) .sender("Alice") .media_album_id(100) .build(), TestMessageBuilder::new("Photo 2", 3) .sender("Alice") .media_album_id(100) .build(), TestMessageBuilder::new("Photo 3", 4) .sender("Alice") .media_album_id(100) .build(), TestMessageBuilder::new("After album", 5).sender("Alice").build(), ]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat 1", 101)]) .selected_chat(101) .with_messages(101, messages) .build(); // Входим в режим выбора — начинаем с последнего (index=4, "After album") app.start_message_selection(); assert!(app.is_selecting_message()); let msg = app.get_selected_message().unwrap(); assert_eq!(msg.text(), "After album"); // k (up) — перескакиваем альбом, попадаем на первый элемент альбома (index=1) app.select_previous_message(); let msg = app.get_selected_message().unwrap(); assert_eq!(msg.text(), "Photo 1"); assert_eq!(msg.media_album_id(), 100); // k (up) — перескакиваем на сообщение до альбома (index=0) app.select_previous_message(); let msg = app.get_selected_message().unwrap(); assert_eq!(msg.text(), "Before album"); // j (down) — перескакиваем на первый элемент альбома (index=1) app.select_next_message(); let msg = app.get_selected_message().unwrap(); assert_eq!(msg.text(), "Photo 1"); // j (down) — перескакиваем альбом, попадаем на "After album" (index=4) app.select_next_message(); let msg = app.get_selected_message().unwrap(); assert_eq!(msg.text(), "After album"); } /// Test: Начало выбора, когда последнее сообщение — часть альбома #[tokio::test] async fn test_album_navigation_start_at_album_end() { let messages = vec![ TestMessageBuilder::new("Regular", 1).sender("Alice").build(), TestMessageBuilder::new("Album Photo 1", 2) .sender("Alice") .media_album_id(200) .build(), TestMessageBuilder::new("Album Photo 2", 3) .sender("Alice") .media_album_id(200) .build(), ]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat 1", 101)]) .selected_chat(101) .with_messages(101, messages) .build(); // Входим в режим выбора — должны оказаться на первом элементе альбома (index=1) app.start_message_selection(); let msg = app.get_selected_message().unwrap(); assert_eq!(msg.text(), "Album Photo 1"); // k (up) — на обычное сообщение app.select_previous_message(); let msg = app.get_selected_message().unwrap(); assert_eq!(msg.text(), "Regular"); } /// Test: Два альбома подряд — навигация между ними #[tokio::test] async fn test_album_navigation_two_albums() { let messages = vec![ TestMessageBuilder::new("A1-P1", 1) .sender("Alice") .media_album_id(100) .build(), TestMessageBuilder::new("A1-P2", 2) .sender("Alice") .media_album_id(100) .build(), TestMessageBuilder::new("A2-P1", 3) .sender("Alice") .media_album_id(200) .build(), TestMessageBuilder::new("A2-P2", 4) .sender("Alice") .media_album_id(200) .build(), ]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat 1", 101)]) .selected_chat(101) .with_messages(101, messages) .build(); // Начинаем — последний альбом (index=2, первый элемент album 200) app.start_message_selection(); let msg = app.get_selected_message().unwrap(); assert_eq!(msg.text(), "A2-P1"); // k — перескакиваем на первый альбом (index=0) app.select_previous_message(); let msg = app.get_selected_message().unwrap(); assert_eq!(msg.text(), "A1-P1"); // j — перескакиваем на второй альбом (index=2) app.select_next_message(); let msg = app.get_selected_message().unwrap(); assert_eq!(msg.text(), "A2-P1"); } /// 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)); }