Files
telegram-tui/tests/vim_mode.rs
Mikhail Kilin 6639dc876c fixes
2026-02-13 19:52:53 +03:00

630 lines
20 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.
//! 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::InputMode;
use tele_tui::app::methods::compose::ComposeMethods;
use tele_tui::app::methods::messages::MessageMethods;
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, "");
}