This commit is contained in:
Mikhail Kilin
2026-02-13 19:52:53 +03:00
parent 6d08300daa
commit 6639dc876c
38 changed files with 961 additions and 123 deletions

View File

@@ -3,7 +3,7 @@
use ratatui::widgets::ListState;
use std::collections::HashMap;
use super::FakeTdClient;
use tele_tui::app::{App, AppScreen, ChatState};
use tele_tui::app::{App, AppScreen, ChatState, InputMode};
use tele_tui::config::Config;
use tele_tui::tdlib::AuthState;
use tele_tui::tdlib::{ChatInfo, MessageInfo};
@@ -19,6 +19,7 @@ pub struct TestAppBuilder {
is_searching: bool,
search_query: String,
chat_state: Option<ChatState>,
input_mode: Option<InputMode>,
messages: HashMap<i64, Vec<MessageInfo>>,
status_message: Option<String>,
auth_state: Option<AuthState>,
@@ -44,6 +45,7 @@ impl TestAppBuilder {
is_searching: false,
search_query: String::new(),
chat_state: None,
input_mode: None,
messages: HashMap::new(),
status_message: None,
auth_state: None,
@@ -171,6 +173,12 @@ impl TestAppBuilder {
self
}
/// Установить Insert mode
pub fn insert_mode(mut self) -> Self {
self.input_mode = Some(InputMode::Insert);
self
}
/// Режим пересылки сообщения
pub fn forward_mode(mut self, message_id: i64) -> Self {
self.chat_state = Some(ChatState::Forward {
@@ -252,6 +260,11 @@ impl TestAppBuilder {
app.chat_state = chat_state;
}
// Применяем input_mode если он установлен
if let Some(input_mode) = self.input_mode {
app.input_mode = input_mode;
}
// Применяем status_message
if let Some(status) = self.status_message {
app.status_message = Some(status);

View File

@@ -31,6 +31,7 @@ fn snapshot_input_with_text() {
let mut app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(123)
.insert_mode()
.message_input("Hello, how are you?")
.build();
@@ -52,6 +53,7 @@ fn snapshot_input_long_text_2_lines() {
let mut app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(123)
.insert_mode()
.message_input(long_text)
.build();
@@ -73,6 +75,7 @@ fn snapshot_input_long_text_max_lines() {
let mut app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(123)
.insert_mode()
.message_input(very_long_text)
.build();
@@ -95,6 +98,7 @@ fn snapshot_input_editing_mode() {
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.insert_mode()
.editing_message(1, 0)
.message_input("Edited text here")
.build();
@@ -118,6 +122,7 @@ fn snapshot_input_reply_mode() {
.with_chat(chat)
.with_message(123, original_msg)
.selected_chat(123)
.insert_mode()
.replying_to(1)
.message_input("I think it's great!")
.build();

View File

@@ -145,6 +145,7 @@ 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"
@@ -182,6 +183,7 @@ 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();
// Вводим текст
@@ -206,6 +208,7 @@ 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"
@@ -238,6 +241,7 @@ 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"
@@ -259,25 +263,25 @@ async fn test_insert_char_at_cursor_position() {
assert_eq!(app.cursor_position, 2);
}
/// Test: Навигация вверх по сообщениям из пустого инпута
/// Test: Normal mode автоматически входит в MessageSelection
#[tokio::test]
async fn test_up_arrow_selects_last_message_when_input_empty() {
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, "");
// Up - должен начать выбор сообщения (последнего)
// Любая клавиша в Normal mode — auto-enters MessageSelection
handle_main_input(&mut app, key(KeyCode::Up)).await;
// Проверяем что вошли в режим выбора сообщения

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -24,5 +24,5 @@ expression: output
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение...
│> Press i to type...
└──────────────────────────────────────────────────────────────────────────────┘

629
tests/vim_mode.rs Normal file
View File

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