Split core and TUI crates

This commit is contained in:
Mikhail Kilin
2026-05-20 00:31:18 +03:00
parent 91a8700b8e
commit eefac431e5
238 changed files with 624 additions and 191 deletions

View File

@@ -0,0 +1,451 @@
//! 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));
}