Files
telegram-tui/tests/input_navigation.rs
Mikhail Kilin 264f183510
Some checks failed
ci/woodpecker/pr/check Pipeline failed
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled
style: auto-format entire codebase with cargo fmt (stable rustfmt.toml)
2026-02-22 17:09:51 +03:00

452 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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));
}