Split core and TUI crates
This commit is contained in:
192
crates/tele-tui/tests/account_switcher.rs
Normal file
192
crates/tele-tui/tests/account_switcher.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
// Integration tests for account switcher modal
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::app_builder::TestAppBuilder;
|
||||
use tele_tui::app::AccountSwitcherState;
|
||||
|
||||
// ============ Open/Close Tests ============
|
||||
|
||||
#[test]
|
||||
fn test_open_account_switcher() {
|
||||
let mut app = TestAppBuilder::new().build();
|
||||
assert!(app.account_switcher.is_none());
|
||||
|
||||
app.open_account_switcher();
|
||||
|
||||
assert!(app.account_switcher.is_some());
|
||||
match &app.account_switcher {
|
||||
Some(AccountSwitcherState::SelectAccount { accounts, selected_index, current_account }) => {
|
||||
assert!(!accounts.is_empty());
|
||||
assert_eq!(*selected_index, 0);
|
||||
assert_eq!(current_account, "default");
|
||||
}
|
||||
_ => panic!("Expected SelectAccount state"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_close_account_switcher() {
|
||||
let mut app = TestAppBuilder::new().build();
|
||||
app.open_account_switcher();
|
||||
assert!(app.account_switcher.is_some());
|
||||
|
||||
app.close_account_switcher();
|
||||
assert!(app.account_switcher.is_none());
|
||||
}
|
||||
|
||||
// ============ Navigation Tests ============
|
||||
|
||||
#[test]
|
||||
fn test_account_switcher_navigate_down() {
|
||||
let mut app = TestAppBuilder::new().build();
|
||||
app.open_account_switcher();
|
||||
|
||||
let num_accounts = match &app.account_switcher {
|
||||
Some(AccountSwitcherState::SelectAccount { accounts, .. }) => accounts.len(),
|
||||
_ => panic!("Expected SelectAccount state"),
|
||||
};
|
||||
|
||||
// Navigate down past all accounts to "Add account" item
|
||||
for _ in 0..num_accounts {
|
||||
app.account_switcher_select_next();
|
||||
}
|
||||
|
||||
match &app.account_switcher {
|
||||
Some(AccountSwitcherState::SelectAccount { selected_index, accounts, .. }) => {
|
||||
// Should be at the "Add account" item (index == accounts.len())
|
||||
assert_eq!(*selected_index, accounts.len());
|
||||
}
|
||||
_ => panic!("Expected SelectAccount state"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_switcher_navigate_up() {
|
||||
let mut app = TestAppBuilder::new().build();
|
||||
app.open_account_switcher();
|
||||
|
||||
// Navigate down first
|
||||
app.account_switcher_select_next();
|
||||
// Navigate back up
|
||||
app.account_switcher_select_prev();
|
||||
|
||||
match &app.account_switcher {
|
||||
Some(AccountSwitcherState::SelectAccount { selected_index, .. }) => {
|
||||
assert_eq!(*selected_index, 0);
|
||||
}
|
||||
_ => panic!("Expected SelectAccount state"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_switcher_navigate_up_at_top() {
|
||||
let mut app = TestAppBuilder::new().build();
|
||||
app.open_account_switcher();
|
||||
|
||||
// Already at 0, navigate up should stay at 0
|
||||
app.account_switcher_select_prev();
|
||||
|
||||
match &app.account_switcher {
|
||||
Some(AccountSwitcherState::SelectAccount { selected_index, .. }) => {
|
||||
assert_eq!(*selected_index, 0);
|
||||
}
|
||||
_ => panic!("Expected SelectAccount state"),
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Confirm Tests ============
|
||||
|
||||
#[test]
|
||||
fn test_confirm_current_account_closes_modal() {
|
||||
let mut app = TestAppBuilder::new().build();
|
||||
app.open_account_switcher();
|
||||
|
||||
// Confirm on the current account (default) should just close
|
||||
app.account_switcher_confirm();
|
||||
|
||||
assert!(app.account_switcher.is_none());
|
||||
assert!(app.pending_account_switch.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_confirm_add_account_transitions_to_add_state() {
|
||||
let mut app = TestAppBuilder::new().build();
|
||||
app.open_account_switcher();
|
||||
|
||||
let num_accounts = match &app.account_switcher {
|
||||
Some(AccountSwitcherState::SelectAccount { accounts, .. }) => accounts.len(),
|
||||
_ => panic!("Expected SelectAccount state"),
|
||||
};
|
||||
|
||||
// Navigate past all accounts to "+ Add account"
|
||||
for _ in 0..num_accounts {
|
||||
app.account_switcher_select_next();
|
||||
}
|
||||
|
||||
// Confirm should transition to AddAccount
|
||||
app.account_switcher_confirm();
|
||||
|
||||
match &app.account_switcher {
|
||||
Some(AccountSwitcherState::AddAccount { name_input, cursor_position, error }) => {
|
||||
assert!(name_input.is_empty());
|
||||
assert_eq!(*cursor_position, 0);
|
||||
assert!(error.is_none());
|
||||
}
|
||||
_ => panic!("Expected AddAccount state"),
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Add Account State Tests ============
|
||||
|
||||
#[test]
|
||||
fn test_start_add_from_select() {
|
||||
let mut app = TestAppBuilder::new().build();
|
||||
app.open_account_switcher();
|
||||
|
||||
// Use quick shortcut
|
||||
app.account_switcher_start_add();
|
||||
|
||||
match &app.account_switcher {
|
||||
Some(AccountSwitcherState::AddAccount { .. }) => {}
|
||||
_ => panic!("Expected AddAccount state"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_back_from_add_to_select() {
|
||||
let mut app = TestAppBuilder::new().build();
|
||||
app.open_account_switcher();
|
||||
app.account_switcher_start_add();
|
||||
|
||||
// Go back
|
||||
app.account_switcher_back();
|
||||
|
||||
match &app.account_switcher {
|
||||
Some(AccountSwitcherState::SelectAccount { .. }) => {}
|
||||
_ => panic!("Expected SelectAccount state after back"),
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Footer Tests ============
|
||||
|
||||
#[test]
|
||||
fn test_default_account_name() {
|
||||
let app = TestAppBuilder::new().build();
|
||||
assert_eq!(app.current_account_name, "default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_account_name() {
|
||||
let mut app = TestAppBuilder::new().build();
|
||||
app.current_account_name = "work".to_string();
|
||||
assert_eq!(app.current_account_name, "work");
|
||||
}
|
||||
|
||||
// ============ Pending Switch Tests ============
|
||||
|
||||
#[test]
|
||||
fn test_pending_switch_initially_none() {
|
||||
let app = TestAppBuilder::new().build();
|
||||
assert!(app.pending_account_switch.is_none());
|
||||
}
|
||||
180
crates/tele-tui/tests/accounts.rs
Normal file
180
crates/tele-tui/tests/accounts.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
// Integration tests for accounts module
|
||||
|
||||
use tele_tui::accounts::{account_db_path, validate_account_name, AccountProfile, AccountsConfig};
|
||||
|
||||
#[test]
|
||||
fn test_default_single_config() {
|
||||
let config = AccountsConfig::default_single();
|
||||
assert_eq!(config.default_account, "default");
|
||||
assert_eq!(config.accounts.len(), 1);
|
||||
assert_eq!(config.accounts[0].name, "default");
|
||||
assert_eq!(config.accounts[0].display_name, "Default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_account_exists() {
|
||||
let config = AccountsConfig::default_single();
|
||||
let account = config.find_account("default");
|
||||
assert!(account.is_some());
|
||||
assert_eq!(account.unwrap().name, "default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_account_not_found() {
|
||||
let config = AccountsConfig::default_single();
|
||||
assert!(config.find_account("work").is_none());
|
||||
assert!(config.find_account("").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_db_path_structure() {
|
||||
let path = account_db_path("default");
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
assert!(path_str.contains("tele-tui"));
|
||||
assert!(path_str.contains("accounts"));
|
||||
assert!(path_str.contains("default"));
|
||||
assert!(path_str.ends_with("tdlib_data"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_db_path_per_account() {
|
||||
let path_default = account_db_path("default");
|
||||
let path_work = account_db_path("work");
|
||||
|
||||
assert_ne!(path_default, path_work);
|
||||
assert!(path_default.to_string_lossy().contains("default"));
|
||||
assert!(path_work.to_string_lossy().contains("work"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_profile_db_path() {
|
||||
let profile = AccountProfile {
|
||||
name: "test-account".to_string(),
|
||||
display_name: "Test".to_string(),
|
||||
};
|
||||
let path = profile.db_path();
|
||||
assert!(path.to_string_lossy().contains("test-account"));
|
||||
assert!(path.to_string_lossy().ends_with("tdlib_data"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_account_name_valid() {
|
||||
assert!(validate_account_name("default").is_ok());
|
||||
assert!(validate_account_name("work").is_ok());
|
||||
assert!(validate_account_name("my-account").is_ok());
|
||||
assert!(validate_account_name("account123").is_ok());
|
||||
assert!(validate_account_name("test_account").is_ok());
|
||||
assert!(validate_account_name("a").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_account_name_empty() {
|
||||
let err = validate_account_name("").unwrap_err();
|
||||
assert!(err.contains("empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_account_name_too_long() {
|
||||
let long_name = "a".repeat(33);
|
||||
let err = validate_account_name(&long_name).unwrap_err();
|
||||
assert!(err.contains("32"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_account_name_uppercase() {
|
||||
assert!(validate_account_name("MyAccount").is_err());
|
||||
assert!(validate_account_name("WORK").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_account_name_spaces() {
|
||||
assert!(validate_account_name("my account").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_account_name_starts_with_dash() {
|
||||
assert!(validate_account_name("-bad").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_account_name_starts_with_underscore() {
|
||||
assert!(validate_account_name("_bad").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_account_name_special_chars() {
|
||||
assert!(validate_account_name("foo@bar").is_err());
|
||||
assert!(validate_account_name("foo.bar").is_err());
|
||||
assert!(validate_account_name("foo/bar").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_account_default() {
|
||||
let config = AccountsConfig::default_single();
|
||||
let result = tele_tui::accounts::resolve_account(&config, None);
|
||||
assert!(result.is_ok());
|
||||
let (name, path) = result.unwrap();
|
||||
assert_eq!(name, "default");
|
||||
assert!(path.to_string_lossy().contains("default"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_account_explicit() {
|
||||
let config = AccountsConfig::default_single();
|
||||
let result = tele_tui::accounts::resolve_account(&config, Some("default"));
|
||||
assert!(result.is_ok());
|
||||
let (name, _) = result.unwrap();
|
||||
assert_eq!(name, "default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_account_not_found() {
|
||||
let config = AccountsConfig::default_single();
|
||||
let result = tele_tui::accounts::resolve_account(&config, Some("work"));
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.contains("work"));
|
||||
assert!(err.contains("not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_account_invalid_name() {
|
||||
let config = AccountsConfig::default_single();
|
||||
let result = tele_tui::accounts::resolve_account(&config, Some("BAD NAME"));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_accounts_config_serde_roundtrip() {
|
||||
let config = AccountsConfig::default_single();
|
||||
let toml_str = toml::to_string_pretty(&config).unwrap();
|
||||
let parsed: AccountsConfig = toml::from_str(&toml_str).unwrap();
|
||||
|
||||
assert_eq!(parsed.default_account, config.default_account);
|
||||
assert_eq!(parsed.accounts.len(), config.accounts.len());
|
||||
assert_eq!(parsed.accounts[0].name, config.accounts[0].name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_accounts_config_multi_account_serde() {
|
||||
let config = AccountsConfig {
|
||||
default_account: "default".to_string(),
|
||||
accounts: vec![
|
||||
AccountProfile {
|
||||
name: "default".to_string(),
|
||||
display_name: "Default".to_string(),
|
||||
},
|
||||
AccountProfile {
|
||||
name: "work".to_string(),
|
||||
display_name: "Work".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let toml_str = toml::to_string_pretty(&config).unwrap();
|
||||
let parsed: AccountsConfig = toml::from_str(&toml_str).unwrap();
|
||||
|
||||
assert_eq!(parsed.accounts.len(), 2);
|
||||
assert!(parsed.find_account("work").is_some());
|
||||
}
|
||||
505
crates/tele-tui/tests/chat_list.rs
Normal file
505
crates/tele-tui/tests/chat_list.rs
Normal file
@@ -0,0 +1,505 @@
|
||||
// Chat list UI snapshot tests
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::app_builder::TestAppBuilder;
|
||||
use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
|
||||
use helpers::test_data::{create_test_chat, TestChatBuilder, TestMessageBuilder};
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn snapshot_empty_chat_list() {
|
||||
let mut app = TestAppBuilder::new().build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("empty_chat_list", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_chat_list_with_three_chats() {
|
||||
let chat1 = create_test_chat("Mom", 123);
|
||||
let chat2 = create_test_chat("Boss", 456);
|
||||
let chat3 = create_test_chat("Rust Community", 789);
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chats(vec![chat1, chat2, chat3])
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("chat_list_three_chats", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_chat_with_unread_count() {
|
||||
let chat = TestChatBuilder::new("Mom", 123)
|
||||
.unread_count(5)
|
||||
.last_message("Привет, как дела?")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new().with_chat(chat).build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("chat_with_unread", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incoming_message_shows_unread_badge() {
|
||||
// Создаём чат БЕЗ непрочитанных сообщений
|
||||
let chat = TestChatBuilder::new("Friend", 999)
|
||||
.unread_count(0)
|
||||
.last_message("Как дела?")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new().with_chat(chat).build();
|
||||
|
||||
// Рендерим UI - должно быть без "(1)"
|
||||
let buffer_before = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
let output_before = buffer_to_string(&buffer_before);
|
||||
|
||||
// Проверяем что нет "(1)" в первой строке чата
|
||||
assert!(!output_before.contains("(1)"), "Before: should not contain (1)");
|
||||
|
||||
// Симулируем входящее сообщение - обновляем unread_count
|
||||
app.chats[0].unread_count = 1;
|
||||
app.chats[0].last_message = "Привет!".to_string();
|
||||
|
||||
// Рендерим UI снова - теперь должно быть "(1)"
|
||||
let buffer_after = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
let output_after = buffer_to_string(&buffer_after);
|
||||
|
||||
// Проверяем что появилось "(1)" в первой строке чата
|
||||
assert!(
|
||||
output_after.contains("(1)"),
|
||||
"After: should contain (1)\nActual output:\n{}",
|
||||
output_after
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_opening_chat_clears_unread_badge() {
|
||||
use helpers::test_data::TestMessageBuilder;
|
||||
|
||||
use tele_tui::types::{ChatId, MessageId};
|
||||
|
||||
// Создаём чат с 3 непрочитанными сообщениями
|
||||
let chat = TestChatBuilder::new("Friend", 999)
|
||||
.unread_count(3)
|
||||
.last_message("У тебя 3 новых сообщения")
|
||||
.build();
|
||||
|
||||
// Создаём 3 входящих сообщения (по умолчанию is_outgoing = false)
|
||||
let messages = vec![
|
||||
TestMessageBuilder::new("Привет!", 1)
|
||||
.sender("Friend")
|
||||
.build(),
|
||||
TestMessageBuilder::new("Как дела?", 2)
|
||||
.sender("Friend")
|
||||
.build(),
|
||||
TestMessageBuilder::new("Ответь мне!", 3)
|
||||
.sender("Friend")
|
||||
.build(),
|
||||
];
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(999, messages)
|
||||
.build();
|
||||
|
||||
// Рендерим UI - должно быть "(3)"
|
||||
let buffer_before = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
let output_before = buffer_to_string(&buffer_before);
|
||||
|
||||
// Проверяем что есть "(3)" в списке чатов
|
||||
assert!(
|
||||
output_before.contains("(3)"),
|
||||
"Before opening: should contain (3)\nActual output:\n{}",
|
||||
output_before
|
||||
);
|
||||
|
||||
// Симулируем открытие чата - загружаем историю
|
||||
let chat_id = ChatId::new(999);
|
||||
let loaded_messages = app.td_client.get_chat_history(chat_id, 100).await.unwrap();
|
||||
|
||||
// Собираем ID входящих сообщений (как в реальном коде)
|
||||
let incoming_message_ids: Vec<MessageId> = loaded_messages
|
||||
.iter()
|
||||
.filter(|msg| !msg.is_outgoing())
|
||||
.map(|msg| msg.id())
|
||||
.collect();
|
||||
|
||||
// Проверяем что нашли 3 входящих сообщения
|
||||
assert_eq!(incoming_message_ids.len(), 3, "Should have 3 incoming messages");
|
||||
|
||||
// Добавляем в очередь для отметки как прочитанные (напрямую через Mutex)
|
||||
app.td_client
|
||||
.pending_view_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((chat_id, incoming_message_ids));
|
||||
|
||||
// Обрабатываем очередь (как в main loop)
|
||||
app.td_client.process_pending_view_messages().await;
|
||||
|
||||
// В FakeTdClient это должно записаться в viewed_messages
|
||||
let viewed = app.td_client.get_viewed_messages();
|
||||
assert_eq!(viewed.len(), 1, "Should have one batch of viewed messages");
|
||||
assert_eq!(viewed[0].0, 999, "Should be for chat 999");
|
||||
assert_eq!(viewed[0].1.len(), 3, "Should have viewed 3 messages");
|
||||
|
||||
// В реальном приложении TDLib отправит Update::ChatReadInbox
|
||||
// который обновит unread_count в чате. Симулируем это:
|
||||
app.chats[0].unread_count = 0;
|
||||
|
||||
// Рендерим UI снова - "(3)" должно пропасть
|
||||
let buffer_after = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
let output_after = buffer_to_string(&buffer_after);
|
||||
|
||||
// Проверяем что "(3)" больше нет
|
||||
assert!(
|
||||
!output_after.contains("(3)"),
|
||||
"After opening: should not contain (3)\nActual output:\n{}",
|
||||
output_after
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_opening_chat_loads_many_messages() {
|
||||
use helpers::test_data::TestMessageBuilder;
|
||||
|
||||
use tele_tui::types::ChatId;
|
||||
|
||||
// Создаём чат с 50 сообщениями
|
||||
let chat = TestChatBuilder::new("History Chat", 888)
|
||||
.last_message("Message 50")
|
||||
.build();
|
||||
|
||||
// Создаём 50 сообщений
|
||||
let messages: Vec<_> = (1..=50)
|
||||
.map(|i| {
|
||||
TestMessageBuilder::new(&format!("Message {}", i), i)
|
||||
.sender("Friend")
|
||||
.build()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(888, messages)
|
||||
.build();
|
||||
|
||||
// Открываем чат - загружаем историю (запрашиваем 100 сообщений)
|
||||
let chat_id = ChatId::new(888);
|
||||
let loaded_messages = app.td_client.get_chat_history(chat_id, 100).await.unwrap();
|
||||
|
||||
// Проверяем что загрузились ВСЕ 50 сообщений, а не только последние 2-3
|
||||
assert_eq!(
|
||||
loaded_messages.len(),
|
||||
50,
|
||||
"Should load all 50 messages, not just last few. Got: {}",
|
||||
loaded_messages.len()
|
||||
);
|
||||
|
||||
// Проверяем что сообщения в правильном порядке (от старых к новым)
|
||||
assert_eq!(loaded_messages[0].text(), "Message 1");
|
||||
assert_eq!(loaded_messages[24].text(), "Message 25");
|
||||
assert_eq!(loaded_messages[49].text(), "Message 50");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_history_chunked_loading() {
|
||||
use tele_tui::types::ChatId;
|
||||
|
||||
// Создаём чат с 120 сообщениями (больше чем TDLIB_MESSAGE_LIMIT = 50)
|
||||
let chat = TestChatBuilder::new("Long History Chat", 999)
|
||||
.last_message("Message 120")
|
||||
.build();
|
||||
|
||||
// Создаём 120 сообщений
|
||||
let messages: Vec<_> = (1..=120)
|
||||
.map(|i| {
|
||||
TestMessageBuilder::new(&format!("Message {}", i), i)
|
||||
.sender("Friend")
|
||||
.build()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(999, messages)
|
||||
.build();
|
||||
|
||||
// Тест 1: Загружаем 100 сообщений (больше чем 50, меньше чем 120)
|
||||
let chat_id = ChatId::new(999);
|
||||
let loaded_messages = app.td_client.get_chat_history(chat_id, 100).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
loaded_messages.len(),
|
||||
100,
|
||||
"Should load 100 messages with chunked loading. Got: {}",
|
||||
loaded_messages.len()
|
||||
);
|
||||
|
||||
// Проверяем что сообщения в правильном порядке (от старых к новым)
|
||||
assert_eq!(loaded_messages[0].text(), "Message 1");
|
||||
assert_eq!(loaded_messages[49].text(), "Message 50"); // Граница первого чанка
|
||||
assert_eq!(loaded_messages[50].text(), "Message 51"); // Начало второго чанка
|
||||
assert_eq!(loaded_messages[99].text(), "Message 100");
|
||||
|
||||
// Тест 2: Загружаем все 120 сообщений
|
||||
let all_messages = app.td_client.get_chat_history(chat_id, 120).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
all_messages.len(),
|
||||
120,
|
||||
"Should load all 120 messages. Got: {}",
|
||||
all_messages.len()
|
||||
);
|
||||
|
||||
assert_eq!(all_messages[0].text(), "Message 1");
|
||||
assert_eq!(all_messages[119].text(), "Message 120");
|
||||
|
||||
// Тест 3: Запрашиваем 200 сообщений, но есть только 120
|
||||
let limited_messages = app.td_client.get_chat_history(chat_id, 200).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
limited_messages.len(),
|
||||
120,
|
||||
"Should load only available 120 messages when requesting 200. Got: {}",
|
||||
limited_messages.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_history_loads_all_without_limit() {
|
||||
use tele_tui::types::ChatId;
|
||||
|
||||
// Создаём чат с 200 сообщениями (4 чанка по 50)
|
||||
let chat = TestChatBuilder::new("Very Long Chat", 1001)
|
||||
.last_message("Message 200")
|
||||
.build();
|
||||
|
||||
let messages: Vec<_> = (1..=200)
|
||||
.map(|i| {
|
||||
TestMessageBuilder::new(&format!("Msg {}", i), i)
|
||||
.sender("User")
|
||||
.build()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(1001, messages)
|
||||
.build();
|
||||
|
||||
// Загружаем без лимита (i32::MAX)
|
||||
let chat_id = ChatId::new(1001);
|
||||
let all = app
|
||||
.td_client
|
||||
.get_chat_history(chat_id, i32::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(all.len(), 200, "Should load all 200 messages without limit");
|
||||
assert_eq!(all[0].text(), "Msg 1", "First message should be oldest");
|
||||
assert_eq!(all[199].text(), "Msg 200", "Last message should be newest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_load_older_messages_pagination() {
|
||||
use tele_tui::types::ChatId;
|
||||
|
||||
// Создаём чат со 150 сообщениями
|
||||
let chat = TestChatBuilder::new("Paginated Chat", 1002)
|
||||
.last_message("Message 150")
|
||||
.build();
|
||||
|
||||
let messages: Vec<_> = (1..=150)
|
||||
.map(|i| {
|
||||
TestMessageBuilder::new(&format!("Msg {}", i), i)
|
||||
.sender("User")
|
||||
.build()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(1002, messages)
|
||||
.build();
|
||||
|
||||
let chat_id = ChatId::new(1002);
|
||||
|
||||
// Шаг 1: Загружаем только последние 30 сообщений
|
||||
// get_chat_history загружает от конца, поэтому получим сообщения 1-30
|
||||
let initial_batch = app.td_client.get_chat_history(chat_id, 30).await.unwrap();
|
||||
assert_eq!(initial_batch.len(), 30, "Should load 30 messages initially");
|
||||
assert_eq!(initial_batch[0].text(), "Msg 1", "First message should be Msg 1");
|
||||
assert_eq!(initial_batch[29].text(), "Msg 30", "Last should be Msg 30");
|
||||
|
||||
// Шаг 2: Загружаем все 150 сообщений для проверки load_older
|
||||
let all_messages = app.td_client.get_chat_history(chat_id, 150).await.unwrap();
|
||||
assert_eq!(all_messages.len(), 150);
|
||||
|
||||
// Имитируем ситуацию: у нас есть сообщения 101-150, хотим загрузить 51-100
|
||||
// Берем ID сообщения 101 (первое в нашем "окне")
|
||||
let msg_101_id = all_messages[100].id(); // index 100 = Msg 101
|
||||
|
||||
// Загружаем сообщения старше 101
|
||||
let older_batch = app
|
||||
.td_client
|
||||
.load_older_messages(chat_id, msg_101_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Должны получить сообщения 1-100 (все что старше 101)
|
||||
assert_eq!(older_batch.len(), 100, "Should load 100 older messages");
|
||||
assert_eq!(older_batch[0].text(), "Msg 1", "Oldest should be Msg 1");
|
||||
assert_eq!(older_batch[99].text(), "Msg 100", "Newest in batch should be Msg 100");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_chat_with_pinned() {
|
||||
let chat = TestChatBuilder::new("Important Chat", 123)
|
||||
.pinned()
|
||||
.last_message("Pinned message")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new().with_chat(chat).build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("chat_pinned", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_chat_with_muted() {
|
||||
let chat = TestChatBuilder::new("Spam Group", 123)
|
||||
.muted()
|
||||
.unread_count(99)
|
||||
.last_message("Too many messages")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new().with_chat(chat).build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("chat_muted", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_chat_with_mentions() {
|
||||
let chat = TestChatBuilder::new("Work Group", 123)
|
||||
.unread_count(10)
|
||||
.unread_mentions(2)
|
||||
.last_message("@me check this out")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new().with_chat(chat).build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("chat_with_mentions", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_selected_chat() {
|
||||
let chat1 = create_test_chat("Mom", 123);
|
||||
let chat2 = create_test_chat("Boss", 456);
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chats(vec![chat1, chat2])
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("chat_selected", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_chat_long_title() {
|
||||
let chat = TestChatBuilder::new("Very Long Chat Title That Should Be Truncated", 123)
|
||||
.last_message("Test message")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new().with_chat(chat).build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("chat_long_title", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_chat_search_mode() {
|
||||
let chat1 = create_test_chat("Mom", 123);
|
||||
let chat2 = create_test_chat("Boss", 456);
|
||||
let chat3 = create_test_chat("Rust Community", 789);
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chats(vec![chat1, chat2, chat3])
|
||||
.searching("Mom")
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("chat_list_search_mode", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_chat_with_online_status() {
|
||||
let chat = TestChatBuilder::new("Alice", 123)
|
||||
.last_message("Hey there!")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
// Note: Online status setup removed due to trait-based DI
|
||||
// User status is not critical for this UI snapshot test
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("chat_with_online_status", output);
|
||||
}
|
||||
231
crates/tele-tui/tests/config.rs
Normal file
231
crates/tele-tui/tests/config.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
// Integration tests for config flow
|
||||
|
||||
use tele_tui::config::{
|
||||
AudioConfig, ColorsConfig, Config, ImagesConfig, Keybindings, NotificationsConfig,
|
||||
};
|
||||
|
||||
/// Test: Дефолтные значения конфигурации
|
||||
#[test]
|
||||
fn test_config_default_values() {
|
||||
let config = Config::default();
|
||||
|
||||
// Проверяем дефолтные цвета
|
||||
assert_eq!(config.colors.incoming_message, "white");
|
||||
assert_eq!(config.colors.outgoing_message, "green");
|
||||
assert_eq!(config.colors.selected_message, "yellow");
|
||||
assert_eq!(config.colors.reaction_chosen, "yellow");
|
||||
assert_eq!(config.colors.reaction_other, "gray");
|
||||
}
|
||||
|
||||
/// Test: Создание конфига с кастомными значениями
|
||||
#[test]
|
||||
fn test_config_custom_values() {
|
||||
let config = Config {
|
||||
colors: ColorsConfig {
|
||||
incoming_message: "cyan".to_string(),
|
||||
outgoing_message: "blue".to_string(),
|
||||
selected_message: "red".to_string(),
|
||||
reaction_chosen: "green".to_string(),
|
||||
reaction_other: "white".to_string(),
|
||||
},
|
||||
keybindings: Keybindings::default(),
|
||||
notifications: NotificationsConfig::default(),
|
||||
images: ImagesConfig::default(),
|
||||
audio: AudioConfig::default(),
|
||||
};
|
||||
|
||||
assert_eq!(config.colors.incoming_message, "cyan");
|
||||
assert_eq!(config.colors.outgoing_message, "blue");
|
||||
}
|
||||
|
||||
/// Test: Парсинг валидных цветов
|
||||
#[test]
|
||||
fn test_parse_valid_colors() {
|
||||
use ratatui::style::Color;
|
||||
|
||||
let config = Config::default();
|
||||
|
||||
assert_eq!(config.parse_color("red"), Color::Red);
|
||||
assert_eq!(config.parse_color("green"), Color::Green);
|
||||
assert_eq!(config.parse_color("blue"), Color::Blue);
|
||||
assert_eq!(config.parse_color("yellow"), Color::Yellow);
|
||||
assert_eq!(config.parse_color("cyan"), Color::Cyan);
|
||||
assert_eq!(config.parse_color("magenta"), Color::Magenta);
|
||||
assert_eq!(config.parse_color("white"), Color::White);
|
||||
assert_eq!(config.parse_color("black"), Color::Black);
|
||||
assert_eq!(config.parse_color("gray"), Color::Gray);
|
||||
assert_eq!(config.parse_color("grey"), Color::Gray);
|
||||
}
|
||||
|
||||
/// Test: Парсинг light цветов
|
||||
#[test]
|
||||
fn test_parse_light_colors() {
|
||||
use ratatui::style::Color;
|
||||
|
||||
let config = Config::default();
|
||||
|
||||
assert_eq!(config.parse_color("lightred"), Color::LightRed);
|
||||
assert_eq!(config.parse_color("lightgreen"), Color::LightGreen);
|
||||
assert_eq!(config.parse_color("lightblue"), Color::LightBlue);
|
||||
assert_eq!(config.parse_color("lightyellow"), Color::LightYellow);
|
||||
assert_eq!(config.parse_color("lightcyan"), Color::LightCyan);
|
||||
assert_eq!(config.parse_color("lightmagenta"), Color::LightMagenta);
|
||||
}
|
||||
|
||||
/// Test: Парсинг невалидного цвета использует fallback (White)
|
||||
#[test]
|
||||
fn test_parse_invalid_color_fallback() {
|
||||
use ratatui::style::Color;
|
||||
|
||||
let config = Config::default();
|
||||
|
||||
// Невалидные цвета должны возвращать White
|
||||
assert_eq!(config.parse_color("invalid_color"), Color::White);
|
||||
assert_eq!(config.parse_color(""), Color::White);
|
||||
assert_eq!(config.parse_color("purple"), Color::White); // purple не поддерживается
|
||||
assert_eq!(config.parse_color("Orange"), Color::White); // orange не поддерживается
|
||||
}
|
||||
|
||||
/// Test: Case-insensitive парсинг цветов
|
||||
#[test]
|
||||
fn test_parse_color_case_insensitive() {
|
||||
use ratatui::style::Color;
|
||||
|
||||
let config = Config::default();
|
||||
|
||||
assert_eq!(config.parse_color("RED"), Color::Red);
|
||||
assert_eq!(config.parse_color("Green"), Color::Green);
|
||||
assert_eq!(config.parse_color("BLUE"), Color::Blue);
|
||||
assert_eq!(config.parse_color("YeLLoW"), Color::Yellow);
|
||||
}
|
||||
|
||||
/// Test: Сериализация и десериализация TOML
|
||||
#[test]
|
||||
fn test_config_toml_serialization() {
|
||||
let original_config = Config {
|
||||
colors: ColorsConfig {
|
||||
incoming_message: "cyan".to_string(),
|
||||
outgoing_message: "blue".to_string(),
|
||||
selected_message: "red".to_string(),
|
||||
reaction_chosen: "green".to_string(),
|
||||
reaction_other: "white".to_string(),
|
||||
},
|
||||
keybindings: Keybindings::default(),
|
||||
notifications: NotificationsConfig::default(),
|
||||
images: ImagesConfig::default(),
|
||||
audio: AudioConfig::default(),
|
||||
};
|
||||
|
||||
// Сериализуем в TOML
|
||||
let toml_string = toml::to_string(&original_config).expect("Failed to serialize config");
|
||||
|
||||
// Десериализуем обратно
|
||||
let deserialized: Config = toml::from_str(&toml_string).expect("Failed to deserialize config");
|
||||
|
||||
// Проверяем что всё совпадает
|
||||
assert_eq!(deserialized.colors.incoming_message, "cyan");
|
||||
assert_eq!(deserialized.colors.outgoing_message, "blue");
|
||||
assert_eq!(deserialized.colors.selected_message, "red");
|
||||
}
|
||||
|
||||
/// Test: Парсинг TOML с частичными данными использует дефолты
|
||||
#[test]
|
||||
fn test_config_partial_toml_uses_defaults() {
|
||||
// TOML только с colors.incoming_message
|
||||
let toml_str = r#"
|
||||
[colors]
|
||||
incoming_message = "cyan"
|
||||
"#;
|
||||
|
||||
let config: Config = toml::from_str(toml_str).expect("Failed to parse partial TOML");
|
||||
|
||||
// Кастомный цвет должен примениться
|
||||
assert_eq!(config.colors.incoming_message, "cyan");
|
||||
// Остальные colors должны быть дефолтными
|
||||
assert_eq!(config.colors.outgoing_message, "green");
|
||||
assert_eq!(config.colors.selected_message, "yellow");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod credentials_tests {
|
||||
use super::*;
|
||||
use std::env;
|
||||
|
||||
/// Test: Загрузка credentials из переменных окружения
|
||||
#[test]
|
||||
fn test_load_credentials_from_env() {
|
||||
// Устанавливаем env переменные для теста
|
||||
unsafe {
|
||||
env::set_var("API_ID", "12345");
|
||||
env::set_var("API_HASH", "test_hash_from_env");
|
||||
}
|
||||
|
||||
// Загружаем credentials
|
||||
let result = Config::load_credentials();
|
||||
|
||||
// Проверяем что загрузилось из env
|
||||
// Примечание: этот тест может зафейлиться если есть credentials файл,
|
||||
// так как он имеет приоритет. Для полноценного тестирования нужно
|
||||
// моковать файловую систему или использовать временные директории.
|
||||
if let Ok((api_id, api_hash)) = result {
|
||||
// Может быть либо из файла, либо из env
|
||||
assert!(api_id > 0);
|
||||
assert!(!api_hash.is_empty());
|
||||
}
|
||||
|
||||
// Очищаем env переменные после теста
|
||||
unsafe {
|
||||
env::remove_var("API_ID");
|
||||
env::remove_var("API_HASH");
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Проверка формата ошибки когда credentials не найдены
|
||||
#[test]
|
||||
fn test_load_credentials_error_message() {
|
||||
// Проверяем есть ли credentials файл в системе
|
||||
let has_credentials_file = Config::credentials_path()
|
||||
.map(|p| p.exists())
|
||||
.unwrap_or(false);
|
||||
|
||||
// Если есть credentials файл, тест не может проверить ошибку
|
||||
if has_credentials_file {
|
||||
// Просто проверяем что credentials загружаются
|
||||
let result = Config::load_credentials();
|
||||
assert!(result.is_ok(), "Credentials file exists but loading failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Временно сохраняем и удаляем env переменные
|
||||
let original_api_id = env::var("API_ID").ok();
|
||||
let original_api_hash = env::var("API_HASH").ok();
|
||||
|
||||
unsafe {
|
||||
env::remove_var("API_ID");
|
||||
env::remove_var("API_HASH");
|
||||
}
|
||||
|
||||
// Пытаемся загрузить credentials без файла и без env
|
||||
let result = Config::load_credentials();
|
||||
|
||||
// Должна быть ошибка
|
||||
if let Err(err_msg) = result {
|
||||
// Проверяем формат ошибки
|
||||
assert!(!err_msg.is_empty(), "Error message should not be empty");
|
||||
} else {
|
||||
// Возможно env переменные установлены глобально и не удаляются
|
||||
// Тест пропускается
|
||||
eprintln!("Warning: credentials loaded despite removing env vars");
|
||||
}
|
||||
|
||||
// Восстанавливаем env переменные
|
||||
unsafe {
|
||||
if let Some(api_id) = original_api_id {
|
||||
env::set_var("API_ID", api_id);
|
||||
}
|
||||
if let Some(api_hash) = original_api_hash {
|
||||
env::set_var("API_HASH", api_hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
174
crates/tele-tui/tests/copy.rs
Normal file
174
crates/tele-tui/tests/copy.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
// Integration tests for copy message flow
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::test_data::TestMessageBuilder;
|
||||
|
||||
/// Test: Форматирование простого сообщения для копирования
|
||||
#[test]
|
||||
fn test_format_plain_message() {
|
||||
let msg = TestMessageBuilder::new("Hello, world!", 1)
|
||||
.sender("Alice")
|
||||
.outgoing()
|
||||
.build();
|
||||
|
||||
// Простое сообщение должно содержать только текст
|
||||
let formatted = format_message_for_test(&msg);
|
||||
assert_eq!(formatted, "Hello, world!");
|
||||
}
|
||||
|
||||
/// Test: Форматирование сообщения с forward контекстом
|
||||
#[test]
|
||||
fn test_format_message_with_forward() {
|
||||
let msg = TestMessageBuilder::new("Forwarded message", 1)
|
||||
.sender("Bob")
|
||||
.forwarded_from("Alice")
|
||||
.build();
|
||||
|
||||
// Сообщение с forward должно содержать контекст
|
||||
let formatted = format_message_for_test(&msg);
|
||||
assert!(formatted.contains("↪ Переслано от Alice"));
|
||||
assert!(formatted.contains("Forwarded message"));
|
||||
}
|
||||
|
||||
/// Test: Форматирование сообщения с reply контекстом
|
||||
#[test]
|
||||
fn test_format_message_with_reply() {
|
||||
let reply_msg = TestMessageBuilder::new("Reply text", 2)
|
||||
.sender("Bob")
|
||||
.reply_to(1, "Alice", "Original message")
|
||||
.build();
|
||||
|
||||
// Сообщение с reply должно содержать контекст оригинала
|
||||
let formatted = format_message_for_test(&reply_msg);
|
||||
assert!(formatted.contains("┌ Alice: Original message"));
|
||||
assert!(formatted.contains("Reply text"));
|
||||
}
|
||||
|
||||
/// Test: Форматирование сообщения с forward и reply одновременно
|
||||
#[test]
|
||||
fn test_format_message_with_both_contexts() {
|
||||
// Создаём сообщение с reply и forward
|
||||
let msg = TestMessageBuilder::new("Complex message", 2)
|
||||
.sender("Bob")
|
||||
.reply_to(1, "Alice", "Original")
|
||||
.forwarded_from("Charlie")
|
||||
.build();
|
||||
|
||||
let formatted = format_message_for_test(&msg);
|
||||
|
||||
// Должны быть оба контекста
|
||||
assert!(formatted.contains("↪ Переслано от Charlie"));
|
||||
assert!(formatted.contains("┌ Alice: Original"));
|
||||
assert!(formatted.contains("Complex message"));
|
||||
}
|
||||
|
||||
/// Test: Форматирование длинного сообщения
|
||||
#[test]
|
||||
fn test_format_long_message() {
|
||||
let long_text = "This is a very long message that spans multiple lines. ".repeat(10);
|
||||
let msg = TestMessageBuilder::new(&long_text, 1)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
|
||||
let formatted = format_message_for_test(&msg);
|
||||
assert_eq!(formatted, long_text);
|
||||
}
|
||||
|
||||
/// Test: Форматирование сообщения с markdown entities
|
||||
#[test]
|
||||
fn test_format_message_with_markdown() {
|
||||
// Этот тест проверяет что entities сохраняются при копировании
|
||||
// В реальном коде entities конвертируются в markdown
|
||||
let msg = TestMessageBuilder::new("Bold text", 1)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
|
||||
let formatted = format_message_for_test(&msg);
|
||||
// Для простоты проверяем что текст присутствует
|
||||
// В реальности здесь должна быть конвертация entities в markdown
|
||||
assert!(formatted.contains("Bold text"));
|
||||
}
|
||||
|
||||
// Helper функция для форматирования (упрощённая версия)
|
||||
// В реальном коде это делается в src/input/main_input.rs::format_message_for_clipboard
|
||||
fn format_message_for_test(msg: &tele_tui::tdlib::MessageInfo) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
// Добавляем forward контекст если есть
|
||||
if let Some(forward) = &msg.forward_from() {
|
||||
result.push_str(&format!("↪ Переслано от {}\n", forward.sender_name));
|
||||
}
|
||||
|
||||
// Добавляем reply контекст если есть
|
||||
if let Some(reply) = &msg.reply_to() {
|
||||
result.push_str(&format!("┌ {}: {}\n", reply.sender_name, reply.text));
|
||||
}
|
||||
|
||||
// Добавляем основной текст
|
||||
result.push_str(msg.text());
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "clipboard"))]
|
||||
mod clipboard_tests {
|
||||
|
||||
/// Test: Проверка что clipboard функции не падают
|
||||
/// Примечание: Реальное тестирование clipboard требует GUI окружения
|
||||
/// и может быть ненадёжным в CI. Этот тест просто проверяет что
|
||||
/// arboard::Clipboard инициализируется без ошибок.
|
||||
#[test]
|
||||
#[ignore] // Игнорируем в CI, так как может не быть GUI окружения
|
||||
fn test_clipboard_initialization() {
|
||||
use arboard::Clipboard;
|
||||
|
||||
// Проверяем что можем создать clipboard
|
||||
let result = Clipboard::new();
|
||||
|
||||
// В headless окружении может вернуть ошибку - это нормально
|
||||
// Главное что не паникует
|
||||
match result {
|
||||
Ok(_) => {
|
||||
// Clipboard доступен - отлично!
|
||||
}
|
||||
Err(_) => {
|
||||
// Clipboard недоступен - ожидаемо в headless окружении
|
||||
// Тест всё равно проходит
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Копирование в реальный clipboard (только для локального тестирования)
|
||||
#[test]
|
||||
#[ignore] // Игнорируем по умолчанию, запускать вручную: cargo test --ignored
|
||||
fn test_copy_to_real_clipboard() {
|
||||
use arboard::Clipboard;
|
||||
|
||||
let test_text = "Test message for clipboard";
|
||||
|
||||
// Пытаемся скопировать
|
||||
if let Ok(mut clipboard) = Clipboard::new() {
|
||||
let copy_result = clipboard.set_text(test_text);
|
||||
assert!(copy_result.is_ok(), "Failed to copy to clipboard");
|
||||
|
||||
// Пытаемся прочитать обратно
|
||||
if let Ok(content) = clipboard.get_text() {
|
||||
assert_eq!(content, test_text, "Clipboard content mismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Кроссплатформенность clipboard
|
||||
#[test]
|
||||
fn test_clipboard_availability() {
|
||||
use arboard::Clipboard;
|
||||
|
||||
// Этот тест просто проверяет что arboard доступен на всех платформах
|
||||
// arboard поддерживает: Linux (X11/Wayland), Windows, macOS
|
||||
let _clipboard_available = Clipboard::new().is_ok();
|
||||
|
||||
// Тест всегда проходит - мы просто проверяем что код компилируется
|
||||
// и не паникует на разных платформах
|
||||
}
|
||||
}
|
||||
186
crates/tele-tui/tests/delete_message.rs
Normal file
186
crates/tele-tui/tests/delete_message.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
// Integration tests for delete message flow
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::fake_tdclient::FakeTdClient;
|
||||
use helpers::test_data::TestMessageBuilder;
|
||||
use tele_tui::types::{ChatId, MessageId};
|
||||
|
||||
/// Test: Удаление сообщения убирает его из списка
|
||||
#[tokio::test]
|
||||
async fn test_delete_message_removes_from_list() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Отправляем сообщение
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "Delete me".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что сообщение есть
|
||||
assert_eq!(client.get_messages(123).len(), 1);
|
||||
|
||||
// Удаляем сообщение
|
||||
client
|
||||
.delete_messages(ChatId::new(123), vec![msg.id()], false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что удаление записалось
|
||||
assert_eq!(client.get_deleted_messages().len(), 1);
|
||||
assert_eq!(client.get_deleted_messages()[0].message_ids[0], msg.id());
|
||||
|
||||
// Проверяем что сообщение удалено из списка
|
||||
assert_eq!(client.get_messages(123).len(), 0);
|
||||
}
|
||||
|
||||
/// Test: Удаление нескольких сообщений
|
||||
#[tokio::test]
|
||||
async fn test_delete_multiple_messages() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Отправляем 3 сообщения
|
||||
let msg1 = client
|
||||
.send_message(ChatId::new(123), "Message 1".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg2 = client
|
||||
.send_message(ChatId::new(123), "Message 2".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg3 = client
|
||||
.send_message(ChatId::new(123), "Message 3".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(client.get_messages(123).len(), 3);
|
||||
|
||||
// Удаляем первое и третье
|
||||
client
|
||||
.delete_messages(ChatId::new(123), vec![msg1.id()], false)
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.delete_messages(ChatId::new(123), vec![msg3.id()], false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем историю удалений
|
||||
assert_eq!(client.get_deleted_messages().len(), 2);
|
||||
assert_eq!(client.get_deleted_messages()[0].message_ids[0], msg1.id());
|
||||
assert_eq!(client.get_deleted_messages()[1].message_ids[0], msg3.id());
|
||||
|
||||
// Проверяем что осталось только второе сообщение
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0].id(), msg2.id());
|
||||
assert_eq!(messages[0].content.text, "Message 2");
|
||||
}
|
||||
|
||||
/// Test: Удаление только своих сообщений (проверка через can_be_deleted_for_all_users)
|
||||
#[tokio::test]
|
||||
async fn test_can_only_delete_own_messages_for_all() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Наше исходящее сообщение (можно удалить для всех)
|
||||
let outgoing_msg = TestMessageBuilder::new("My message", 1).outgoing().build();
|
||||
|
||||
let client = client.with_message(123, outgoing_msg);
|
||||
|
||||
// Входящее сообщение от собеседника (можно удалить только для себя)
|
||||
let incoming_msg = TestMessageBuilder::new("Their message", 2)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, incoming_msg);
|
||||
|
||||
// Проверяем флаги удаления
|
||||
let messages = client.get_messages(123);
|
||||
assert!(messages[0].can_be_deleted_for_all_users()); // Наше
|
||||
assert!(!messages[1].can_be_deleted_for_all_users()); // Чужое
|
||||
|
||||
// Оба можно удалить для себя
|
||||
assert!(messages[0].can_be_deleted_only_for_self());
|
||||
assert!(messages[1].can_be_deleted_only_for_self());
|
||||
}
|
||||
|
||||
/// Test: Удаление несуществующего сообщения (ничего не происходит)
|
||||
#[tokio::test]
|
||||
async fn test_delete_nonexistent_message() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Отправляем одно сообщение
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "Exists".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(client.get_messages(123).len(), 1);
|
||||
|
||||
// Пытаемся удалить несуществующее
|
||||
client
|
||||
.delete_messages(ChatId::new(123), vec![MessageId::new(999)], false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Удаление записалось в историю
|
||||
assert_eq!(client.get_deleted_messages().len(), 1);
|
||||
assert_eq!(client.get_deleted_messages()[0].message_ids[0], MessageId::new(999));
|
||||
|
||||
// Но существующее сообщение осталось
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0].id(), msg.id());
|
||||
}
|
||||
|
||||
/// Test: Подтверждение удаления (симуляция модалки)
|
||||
/// FakeTdClient сразу удаляет, но в реальном App должна быть модалка подтверждения
|
||||
#[tokio::test]
|
||||
async fn test_delete_with_confirmation_flow() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "To delete".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Шаг 1: Пользователь нажал 'd' -> показывается модалка (в App)
|
||||
// В FakeTdClient просто проверяем что сообщение ещё есть
|
||||
assert_eq!(client.get_messages(123).len(), 1);
|
||||
assert_eq!(client.get_deleted_messages().len(), 0);
|
||||
|
||||
// Шаг 2: Пользователь подтвердил 'y' -> удаляем
|
||||
client
|
||||
.delete_messages(ChatId::new(123), vec![msg.id()], false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что удалено
|
||||
assert_eq!(client.get_messages(123).len(), 0);
|
||||
assert_eq!(client.get_deleted_messages().len(), 1);
|
||||
}
|
||||
|
||||
/// Test: Отмена удаления (Esc) - сообщение остаётся
|
||||
#[tokio::test]
|
||||
async fn test_cancel_delete_keeps_message() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "Keep me".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Шаг 1: Пользователь нажал 'd' -> показалась модалка
|
||||
assert_eq!(client.get_messages(123).len(), 1);
|
||||
|
||||
// Шаг 2: Пользователь нажал 'Esc' -> НЕ вызываем delete_message
|
||||
|
||||
// Проверяем что сообщение осталось
|
||||
assert_eq!(client.get_messages(123).len(), 1);
|
||||
assert_eq!(client.get_deleted_messages().len(), 0);
|
||||
|
||||
// Сообщение на месте
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages[0].id(), msg.id());
|
||||
assert_eq!(messages[0].content.text, "Keep me");
|
||||
}
|
||||
190
crates/tele-tui/tests/drafts.rs
Normal file
190
crates/tele-tui/tests/drafts.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
// Integration tests for drafts flow
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::test_data::{create_test_chat, TestChatBuilder};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Простая структура для хранения черновиков (как в реальном App)
|
||||
struct DraftManager {
|
||||
drafts: HashMap<i64, String>, // chat_id -> draft text
|
||||
}
|
||||
|
||||
impl DraftManager {
|
||||
fn new() -> Self {
|
||||
Self { drafts: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Сохранить черновик для чата
|
||||
fn save_draft(&mut self, chat_id: i64, text: String) {
|
||||
if text.is_empty() {
|
||||
self.drafts.remove(&chat_id);
|
||||
} else {
|
||||
self.drafts.insert(chat_id, text);
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить черновик для чата
|
||||
fn get_draft(&self, chat_id: i64) -> Option<&String> {
|
||||
self.drafts.get(&chat_id)
|
||||
}
|
||||
|
||||
/// Очистить черновик для чата
|
||||
fn clear_draft(&mut self, chat_id: i64) {
|
||||
self.drafts.remove(&chat_id);
|
||||
}
|
||||
|
||||
/// Проверить есть ли черновик
|
||||
fn has_draft(&self, chat_id: i64) -> bool {
|
||||
self.drafts.contains_key(&chat_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Переключение между чатами сохраняет текст
|
||||
#[tokio::test]
|
||||
async fn test_switching_chats_saves_draft() {
|
||||
let mut drafts = DraftManager::new();
|
||||
|
||||
// Пользователь в чате 123, начал печатать
|
||||
let current_chat = 123;
|
||||
let input_text = "Hello, this is a draft message";
|
||||
|
||||
// Перед переключением на другой чат - сохраняем
|
||||
drafts.save_draft(current_chat, input_text.to_string());
|
||||
|
||||
// Переключаемся на чат 456
|
||||
let _new_chat = 456;
|
||||
|
||||
// Проверяем что черновик для 123 сохранился
|
||||
assert!(drafts.has_draft(123));
|
||||
assert_eq!(drafts.get_draft(123).unwrap(), input_text);
|
||||
|
||||
// В новом чате 456 черновика нет
|
||||
assert!(!drafts.has_draft(456));
|
||||
}
|
||||
|
||||
/// Test: Возврат в чат восстанавливает текст
|
||||
#[tokio::test]
|
||||
async fn test_returning_to_chat_restores_draft() {
|
||||
let mut drafts = DraftManager::new();
|
||||
|
||||
// Сохраняем черновик в чате 123
|
||||
drafts.save_draft(123, "Unfinished message".to_string());
|
||||
|
||||
// Переключились на другие чаты
|
||||
// ...
|
||||
|
||||
// Возвращаемся в чат 123
|
||||
let restored_text = drafts.get_draft(123);
|
||||
|
||||
assert!(restored_text.is_some());
|
||||
assert_eq!(restored_text.unwrap(), "Unfinished message");
|
||||
}
|
||||
|
||||
/// Test: Отправка сообщения удаляет черновик
|
||||
#[tokio::test]
|
||||
async fn test_sending_message_clears_draft() {
|
||||
let mut drafts = DraftManager::new();
|
||||
|
||||
// Сохранили черновик
|
||||
drafts.save_draft(123, "Draft text".to_string());
|
||||
|
||||
assert!(drafts.has_draft(123));
|
||||
|
||||
// Пользователь отправил сообщение - очищаем черновик
|
||||
drafts.clear_draft(123);
|
||||
|
||||
assert!(!drafts.has_draft(123));
|
||||
assert_eq!(drafts.get_draft(123), None);
|
||||
}
|
||||
|
||||
/// Test: Индикатор черновика в списке чатов
|
||||
#[tokio::test]
|
||||
async fn test_draft_indicator_in_chat_list() {
|
||||
let mut drafts = DraftManager::new();
|
||||
|
||||
// Создаём несколько чатов
|
||||
let _chat1 = create_test_chat("Mom", 123);
|
||||
let chat2 = TestChatBuilder::new("Boss", 456)
|
||||
.draft("Draft: Meeting notes")
|
||||
.build();
|
||||
let _chat3 = create_test_chat("Friend", 789);
|
||||
|
||||
// В реальном App: chat.draft_text устанавливается из DraftManager
|
||||
// Здесь просто проверяем что у chat2 есть draft_text поле
|
||||
assert_eq!(chat2.draft_text.as_ref().unwrap(), "Draft: Meeting notes");
|
||||
|
||||
// Симулируем: пользователь набрал текст в чате 123
|
||||
drafts.save_draft(123, "My draft".to_string());
|
||||
|
||||
// Проверяем что драфт есть
|
||||
assert!(drafts.has_draft(123));
|
||||
assert_eq!(drafts.get_draft(123).unwrap(), "My draft");
|
||||
|
||||
// В UI рядом с чатом 123 будет показываться индикатор/превью
|
||||
// Например: "Mom" | "Draft: My draft"
|
||||
}
|
||||
|
||||
/// Test: Множественные черновики в разных чатах
|
||||
#[tokio::test]
|
||||
async fn test_multiple_drafts_in_different_chats() {
|
||||
let mut drafts = DraftManager::new();
|
||||
|
||||
// Создаём черновики в 3 чатах
|
||||
drafts.save_draft(123, "Draft for Mom".to_string());
|
||||
drafts.save_draft(456, "Draft for Boss".to_string());
|
||||
drafts.save_draft(789, "Draft for Friend".to_string());
|
||||
|
||||
// Проверяем что все сохранились
|
||||
assert_eq!(drafts.get_draft(123).unwrap(), "Draft for Mom");
|
||||
assert_eq!(drafts.get_draft(456).unwrap(), "Draft for Boss");
|
||||
assert_eq!(drafts.get_draft(789).unwrap(), "Draft for Friend");
|
||||
|
||||
// Очищаем один
|
||||
drafts.clear_draft(456);
|
||||
|
||||
// Проверяем что остальные на месте
|
||||
assert!(drafts.has_draft(123));
|
||||
assert!(!drafts.has_draft(456));
|
||||
assert!(drafts.has_draft(789));
|
||||
}
|
||||
|
||||
/// Test: Пустой текст не сохраняется как черновик
|
||||
#[tokio::test]
|
||||
async fn test_empty_text_does_not_save_draft() {
|
||||
let mut drafts = DraftManager::new();
|
||||
|
||||
// Пытаемся сохранить пустой черновик
|
||||
drafts.save_draft(123, "".to_string());
|
||||
|
||||
// Не должен сохраниться
|
||||
assert!(!drafts.has_draft(123));
|
||||
|
||||
// Сохраняем нормальный черновик
|
||||
drafts.save_draft(123, "Text".to_string());
|
||||
assert!(drafts.has_draft(123));
|
||||
|
||||
// Затем очищаем (сохраняем пустой)
|
||||
drafts.save_draft(123, "".to_string());
|
||||
|
||||
// Черновик должен удалиться
|
||||
assert!(!drafts.has_draft(123));
|
||||
}
|
||||
|
||||
/// Test: Редактирование черновика
|
||||
#[tokio::test]
|
||||
async fn test_editing_draft() {
|
||||
let mut drafts = DraftManager::new();
|
||||
|
||||
// Сохраняем начальный черновик
|
||||
drafts.save_draft(123, "First version".to_string());
|
||||
assert_eq!(drafts.get_draft(123).unwrap(), "First version");
|
||||
|
||||
// Пользователь редактирует - сохраняем обновлённую версию
|
||||
drafts.save_draft(123, "Second version".to_string());
|
||||
assert_eq!(drafts.get_draft(123).unwrap(), "Second version");
|
||||
|
||||
// Ещё раз редактирует
|
||||
drafts.save_draft(123, "Final version".to_string());
|
||||
assert_eq!(drafts.get_draft(123).unwrap(), "Final version");
|
||||
}
|
||||
83
crates/tele-tui/tests/e2e_smoke.rs
Normal file
83
crates/tele-tui/tests/e2e_smoke.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
// E2E Smoke tests для базовых сценариев запуска приложения
|
||||
|
||||
use tele_tui::tdlib::NetworkState;
|
||||
|
||||
/// Тест: Приложение запускается без краша
|
||||
/// Проверяем что базовые структуры создаются корректно
|
||||
#[tokio::test]
|
||||
async fn test_app_starts_without_crash() {
|
||||
// Проверяем что NetworkState enum работает корректно
|
||||
let states = vec![
|
||||
NetworkState::Ready,
|
||||
NetworkState::WaitingForNetwork,
|
||||
NetworkState::Connecting,
|
||||
NetworkState::ConnectingToProxy,
|
||||
NetworkState::Updating,
|
||||
];
|
||||
|
||||
for state in states {
|
||||
// Просто проверяем что состояния создаются без паники
|
||||
let _text = match state {
|
||||
NetworkState::Ready => "Ready",
|
||||
NetworkState::WaitingForNetwork => "Waiting for network",
|
||||
NetworkState::Connecting => "Connecting",
|
||||
NetworkState::ConnectingToProxy => "Connecting to proxy",
|
||||
NetworkState::Updating => "Updating",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Тест: Проверка минимального размера терминала
|
||||
#[test]
|
||||
fn test_minimum_terminal_size() {
|
||||
const MIN_WIDTH: u16 = 80;
|
||||
const MIN_HEIGHT: u16 = 20;
|
||||
|
||||
// Проверяем что константы установлены разумно
|
||||
const {
|
||||
assert!(MIN_WIDTH >= 80, "Минимальная ширина должна быть >= 80");
|
||||
assert!(MIN_HEIGHT >= 20, "Минимальная высота должна быть >= 20");
|
||||
};
|
||||
|
||||
// Проверяем граничные случаи
|
||||
let too_small_width = MIN_WIDTH - 1;
|
||||
let too_small_height = MIN_HEIGHT - 1;
|
||||
|
||||
assert!(too_small_width < MIN_WIDTH);
|
||||
assert!(too_small_height < MIN_HEIGHT);
|
||||
}
|
||||
|
||||
/// Тест: Базовые константы приложения
|
||||
#[test]
|
||||
fn test_app_constants() {
|
||||
use tele_tui::constants::*;
|
||||
|
||||
// Проверяем что лимиты установлены
|
||||
const {
|
||||
assert!(MAX_MESSAGES_IN_CHAT > 0, "Лимит сообщений должен быть > 0");
|
||||
assert!(MAX_CHATS > 0, "Лимит чатов должен быть > 0");
|
||||
assert!(MAX_USER_CACHE_SIZE > 0, "Размер кэша пользователей должен быть > 0");
|
||||
assert!(MAX_MESSAGES_IN_CHAT <= 1000, "Слишком большой лимит сообщений");
|
||||
assert!(MAX_CHATS <= 500, "Слишком большой лимит чатов");
|
||||
};
|
||||
}
|
||||
|
||||
/// Тест: Graceful shutdown флаг
|
||||
#[test]
|
||||
fn test_shutdown_flag() {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Проверяем начальное состояние
|
||||
assert!(!shutdown.load(Ordering::Relaxed), "Флаг должен быть false при создании");
|
||||
|
||||
// Проверяем установку флага
|
||||
shutdown.store(true, Ordering::Relaxed);
|
||||
assert!(shutdown.load(Ordering::Relaxed), "Флаг должен быть true после установки");
|
||||
|
||||
// Проверяем клонирование Arc
|
||||
let shutdown_clone = Arc::clone(&shutdown);
|
||||
assert!(shutdown_clone.load(Ordering::Relaxed), "Клон должен видеть то же значение");
|
||||
}
|
||||
167
crates/tele-tui/tests/e2e_termwright.rs
Normal file
167
crates/tele-tui/tests/e2e_termwright.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
#![cfg(feature = "test-support")]
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use termwright::prelude::*;
|
||||
|
||||
fn fixture_path() -> &'static str {
|
||||
env!("CARGO_BIN_EXE_tele-tui-test-fixture")
|
||||
}
|
||||
|
||||
async fn spawn_fixture(scenario: &str) -> Result<Terminal> {
|
||||
let mut builder = Terminal::builder()
|
||||
.size(100, 30)
|
||||
.working_dir(env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
if let Some(lib_path) = tdlib_library_path() {
|
||||
builder = builder
|
||||
.env("DYLD_LIBRARY_PATH", &lib_path)
|
||||
.env("LD_LIBRARY_PATH", &lib_path);
|
||||
}
|
||||
let command = format!(
|
||||
"stty -echo -ixon; exec {} --scenario {}",
|
||||
shell_quote(fixture_path()),
|
||||
shell_quote(scenario)
|
||||
);
|
||||
builder.spawn("/bin/sh", &["-lc", &command]).await
|
||||
}
|
||||
|
||||
fn shell_quote(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "'\\''"))
|
||||
}
|
||||
|
||||
fn tdlib_library_path() -> Option<String> {
|
||||
let build_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("target")
|
||||
.join("debug")
|
||||
.join("build");
|
||||
let entries = std::fs::read_dir(build_dir).ok()?;
|
||||
|
||||
let mut paths = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let lib_dir = entry.path().join("out").join("tdlib").join("lib");
|
||||
if lib_dir.join("libtdjson.1.8.29.dylib").exists() || lib_dir.join("libtdjson.so").exists()
|
||||
{
|
||||
paths.push(lib_dir);
|
||||
}
|
||||
}
|
||||
|
||||
(!paths.is_empty()).then(|| {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|path| path.to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join(":")
|
||||
})
|
||||
}
|
||||
|
||||
async fn stop_fixture(term: &mut Terminal) {
|
||||
let _ = tokio::time::timeout(Duration::from_millis(500), term.send_key(Key::F(10))).await;
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
let _ = std::process::Command::new("pkill")
|
||||
.arg("-f")
|
||||
.arg("tele-tui-test-fixture")
|
||||
.status();
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
let _ = tokio::time::timeout(Duration::from_secs(1), term.kill()).await;
|
||||
}
|
||||
|
||||
async fn wait_for_text(term: &Terminal, needle: &str) -> Result<()> {
|
||||
let started = Instant::now();
|
||||
let mut last_screen = String::new();
|
||||
for _ in 0..100 {
|
||||
let Ok(screen) = screen_text(term).await else {
|
||||
continue;
|
||||
};
|
||||
if screen.contains(needle) {
|
||||
return Ok(());
|
||||
}
|
||||
last_screen = screen;
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
|
||||
let elapsed = started.elapsed();
|
||||
Err(TermwrightError::Timeout {
|
||||
condition: format!("text '{needle}' to appear\n\n{last_screen}"),
|
||||
timeout: elapsed,
|
||||
})
|
||||
}
|
||||
|
||||
async fn screen_text(term: &Terminal) -> Result<String> {
|
||||
tokio::time::timeout(Duration::from_millis(500), term.screen())
|
||||
.await
|
||||
.map(|screen| screen.text())
|
||||
.map_err(|_| TermwrightError::Timeout {
|
||||
condition: "terminal screen snapshot".to_string(),
|
||||
timeout: Duration::from_millis(500),
|
||||
})
|
||||
}
|
||||
|
||||
async fn enter_insert_mode(term: &Terminal) -> Result<()> {
|
||||
for _ in 0..5 {
|
||||
term.send_key(Key::Char('i')).await?;
|
||||
std::thread::sleep(Duration::from_millis(150));
|
||||
if !screen_text(term).await?.contains("Press i to type") {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let screen = screen_text(term).await?;
|
||||
Err(TermwrightError::Timeout {
|
||||
condition: format!("insert mode to start\n\n{screen}"),
|
||||
timeout: Duration::from_millis(750),
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e2e_termwright_user_flows() -> Result<()> {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("failed to build e2e runtime");
|
||||
|
||||
let result = runtime.block_on(async {
|
||||
tokio::time::timeout(Duration::from_secs(15), compose_and_send_message()).await
|
||||
});
|
||||
kill_fixture_processes();
|
||||
|
||||
match result {
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(TermwrightError::Timeout {
|
||||
condition: "termwright e2e user flow".to_string(),
|
||||
timeout: Duration::from_secs(15),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
async fn compose_and_send_message() -> Result<()> {
|
||||
let mut term = spawn_fixture("compose-draft").await?;
|
||||
let result = async {
|
||||
wait_for_text(&term, "Work Group").await?;
|
||||
wait_for_text(&term, "Standup notes are ready").await?;
|
||||
wait_for_text(&term, "hello from e2e").await?;
|
||||
enter_insert_mode(&term).await?;
|
||||
wait_for_text(&term, "hello from e2e").await?;
|
||||
term.send_key(Key::Enter).await?;
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
let screen = screen_text(&term).await?;
|
||||
assert!(screen.contains("hello from e2e"), "sent message should appear\n\n{}", screen);
|
||||
assert!(
|
||||
!screen.contains("Сообщение: hello from e2e"),
|
||||
"compose input should clear after send"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
stop_fixture(&mut term).await;
|
||||
result
|
||||
}
|
||||
|
||||
fn kill_fixture_processes() {
|
||||
let _ = std::process::Command::new("pkill")
|
||||
.arg("-f")
|
||||
.arg("tele-tui-test-fixture")
|
||||
.status();
|
||||
}
|
||||
423
crates/tele-tui/tests/e2e_user_journey.rs
Normal file
423
crates/tele-tui/tests/e2e_user_journey.rs
Normal file
@@ -0,0 +1,423 @@
|
||||
// E2E User Journey tests — многошаговые интеграционные тесты
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::fake_tdclient::{FakeTdClient, TdUpdate};
|
||||
use helpers::test_data::{TestChatBuilder, TestMessageBuilder};
|
||||
use tele_tui::tdlib::NetworkState;
|
||||
use tele_tui::types::ChatId;
|
||||
|
||||
/// Тест 1: App Launch → Auth → Chat List
|
||||
/// Симулирует полный путь пользователя от запуска до загрузки чатов
|
||||
#[tokio::test]
|
||||
async fn test_user_journey_app_launch_to_chat_list() {
|
||||
// 1. Создаем fake client (симуляция авторизации пропущена, клиент уже авторизован)
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// 2. Проверяем начальное состояние - нет чатов
|
||||
assert_eq!(client.get_chats().len(), 0);
|
||||
assert_eq!(client.get_network_state(), NetworkState::Ready);
|
||||
|
||||
// 3. Создаем чаты
|
||||
let chat1 = TestChatBuilder::new("Mom", 101).build();
|
||||
let chat2 = TestChatBuilder::new("Work Group", 102).build();
|
||||
let chat3 = TestChatBuilder::new("Boss", 103).build();
|
||||
|
||||
let client = client.with_chat(chat1).with_chat(chat2).with_chat(chat3);
|
||||
|
||||
// 4. Симулируем загрузку чатов через load_chats
|
||||
let loaded_chats = client.load_chats(50).await.unwrap();
|
||||
|
||||
// 5. Проверяем что чаты загружены
|
||||
assert_eq!(loaded_chats.len(), 3);
|
||||
assert_eq!(loaded_chats[0].title, "Mom");
|
||||
assert_eq!(loaded_chats[1].title, "Work Group");
|
||||
assert_eq!(loaded_chats[2].title, "Boss");
|
||||
|
||||
// 6. Проверяем что нет выбранного чата
|
||||
assert_eq!(client.get_current_chat_id(), None);
|
||||
}
|
||||
|
||||
/// Тест 2: Open Chat → Load History → Send Message
|
||||
/// Симулирует открытие чата, загрузку истории и отправку сообщения
|
||||
#[tokio::test]
|
||||
async fn test_user_journey_open_chat_send_message() {
|
||||
// 1. Подготовка: создаем клиент с чатом
|
||||
let chat = TestChatBuilder::new("Mom", 123).build();
|
||||
let client = FakeTdClient::new().with_chat(chat);
|
||||
|
||||
// 2. Создаем несколько сообщений в истории
|
||||
let msg1 = TestMessageBuilder::new("Hi, how are you?", 1)
|
||||
.sender("Mom")
|
||||
.build();
|
||||
|
||||
let msg2 = TestMessageBuilder::new("I'm good, thanks!", 2)
|
||||
.outgoing()
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, msg1).with_message(123, msg2);
|
||||
|
||||
// 3. Открываем чат
|
||||
client.open_chat(ChatId::new(123)).await.unwrap();
|
||||
|
||||
// 4. Проверяем что чат открыт
|
||||
assert_eq!(client.get_current_chat_id(), Some(123));
|
||||
|
||||
// 5. Загружаем историю сообщений
|
||||
let history = client.get_chat_history(ChatId::new(123), 50).await.unwrap();
|
||||
|
||||
// 6. Проверяем что история загружена
|
||||
assert_eq!(history.len(), 2);
|
||||
assert_eq!(history[0].text(), "Hi, how are you?");
|
||||
assert_eq!(history[1].text(), "I'm good, thanks!");
|
||||
|
||||
// 7. Отправляем новое сообщение
|
||||
let _new_msg = client
|
||||
.send_message(ChatId::new(123), "What's for dinner?".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 8. Проверяем что сообщение отправлено
|
||||
assert_eq!(client.get_sent_messages().len(), 1);
|
||||
assert_eq!(client.get_sent_messages()[0].text, "What's for dinner?");
|
||||
assert_eq!(client.get_sent_messages()[0].chat_id, 123);
|
||||
|
||||
// 9. Проверяем что сообщение добавилось в историю
|
||||
let updated_history = client.get_chat_history(ChatId::new(123), 50).await.unwrap();
|
||||
assert_eq!(updated_history.len(), 3);
|
||||
assert_eq!(updated_history[2].text(), "What's for dinner?");
|
||||
}
|
||||
|
||||
/// Тест 3: Receive Incoming Message While Chat Open
|
||||
/// Симулирует получение входящего сообщения в открытом чате
|
||||
#[tokio::test]
|
||||
async fn test_user_journey_receive_incoming_message() {
|
||||
// 1. Подготовка: создаем клиент с открытым чатом
|
||||
let chat = TestChatBuilder::new("Friend", 456).build();
|
||||
let client = FakeTdClient::new().with_chat(chat);
|
||||
|
||||
// 2. Открываем чат
|
||||
client.open_chat(ChatId::new(456)).await.unwrap();
|
||||
assert_eq!(client.get_current_chat_id(), Some(456));
|
||||
|
||||
// 3. Создаем update channel для получения событий
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
client.set_update_channel(tx);
|
||||
|
||||
// 4. Проверяем начальное состояние - нет сообщений
|
||||
let initial_history = client.get_chat_history(ChatId::new(456), 50).await.unwrap();
|
||||
assert_eq!(initial_history.len(), 0);
|
||||
|
||||
// 5. Симулируем входящее сообщение от собеседника
|
||||
client.simulate_incoming_message(ChatId::new(456), "Hey! Are you there?".to_string(), "Friend");
|
||||
|
||||
// 6. Получаем update из канала
|
||||
let update = rx.try_recv();
|
||||
assert!(update.is_ok(), "Должен быть получен update о новом сообщении");
|
||||
|
||||
if let Ok(TdUpdate::NewMessage { chat_id, message }) = update {
|
||||
assert_eq!(chat_id.as_i64(), 456);
|
||||
assert_eq!(message.text(), "Hey! Are you there?");
|
||||
assert_eq!(message.sender_name(), "Friend");
|
||||
assert!(!message.is_outgoing());
|
||||
} else {
|
||||
panic!("Неверный тип update");
|
||||
}
|
||||
|
||||
// 7. Проверяем что сообщение появилось в истории
|
||||
let updated_history = client.get_chat_history(ChatId::new(456), 50).await.unwrap();
|
||||
assert_eq!(updated_history.len(), 1);
|
||||
assert_eq!(updated_history[0].text(), "Hey! Are you there?");
|
||||
}
|
||||
|
||||
/// Тест 4: Multi-step conversation flow
|
||||
/// Симулирует полноценную беседу с несколькими сообщениями туда-обратно
|
||||
#[tokio::test]
|
||||
async fn test_user_journey_multi_step_conversation() {
|
||||
// 1. Подготовка
|
||||
let chat = TestChatBuilder::new("Alice", 789).build();
|
||||
let client = FakeTdClient::new().with_chat(chat);
|
||||
|
||||
// 2. Открываем чат
|
||||
client.open_chat(ChatId::new(789)).await.unwrap();
|
||||
|
||||
// 3. Setup update channel
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
client.set_update_channel(tx);
|
||||
|
||||
// 4. Входящее сообщение от Alice
|
||||
client.simulate_incoming_message(
|
||||
ChatId::new(789),
|
||||
"How's the project going?".to_string(),
|
||||
"Alice",
|
||||
);
|
||||
|
||||
// Проверяем update
|
||||
let update = rx.try_recv().ok();
|
||||
assert!(matches!(update, Some(TdUpdate::NewMessage { .. })));
|
||||
|
||||
// 5. Отвечаем
|
||||
client
|
||||
.send_message(
|
||||
ChatId::new(789),
|
||||
"Almost done! Just need to finish tests.".to_string(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 6. Проверяем историю после первого обмена
|
||||
let history1 = client.get_chat_history(ChatId::new(789), 50).await.unwrap();
|
||||
assert_eq!(history1.len(), 2);
|
||||
|
||||
// 7. Еще одно входящее сообщение
|
||||
client.simulate_incoming_message(
|
||||
ChatId::new(789),
|
||||
"Great! Let me know if you need help.".to_string(),
|
||||
"Alice",
|
||||
);
|
||||
|
||||
// 8. Снова отвечаем
|
||||
client
|
||||
.send_message(ChatId::new(789), "Will do, thanks!".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 9. Финальная проверка истории
|
||||
let final_history = client.get_chat_history(ChatId::new(789), 50).await.unwrap();
|
||||
assert_eq!(final_history.len(), 4);
|
||||
|
||||
// Проверяем порядок сообщений
|
||||
assert_eq!(final_history[0].text(), "How's the project going?");
|
||||
assert!(!final_history[0].is_outgoing());
|
||||
|
||||
assert_eq!(final_history[1].text(), "Almost done! Just need to finish tests.");
|
||||
assert!(final_history[1].is_outgoing());
|
||||
|
||||
assert_eq!(final_history[2].text(), "Great! Let me know if you need help.");
|
||||
assert!(!final_history[2].is_outgoing());
|
||||
|
||||
assert_eq!(final_history[3].text(), "Will do, thanks!");
|
||||
assert!(final_history[3].is_outgoing());
|
||||
}
|
||||
|
||||
/// Тест 5: Switch between chats
|
||||
/// Симулирует переключение между разными чатами
|
||||
#[tokio::test]
|
||||
async fn test_user_journey_switch_chats() {
|
||||
// 1. Создаем несколько чатов
|
||||
let chat1 = TestChatBuilder::new("Chat 1", 111).build();
|
||||
let chat2 = TestChatBuilder::new("Chat 2", 222).build();
|
||||
let chat3 = TestChatBuilder::new("Chat 3", 333).build();
|
||||
|
||||
let client = FakeTdClient::new()
|
||||
.with_chat(chat1)
|
||||
.with_chat(chat2)
|
||||
.with_chat(chat3);
|
||||
|
||||
// 2. Открываем первый чат
|
||||
client.open_chat(ChatId::new(111)).await.unwrap();
|
||||
assert_eq!(client.get_current_chat_id(), Some(111));
|
||||
|
||||
// 3. Отправляем сообщение в первом чате
|
||||
client
|
||||
.send_message(ChatId::new(111), "Message in chat 1".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 4. Переключаемся на второй чат
|
||||
client.open_chat(ChatId::new(222)).await.unwrap();
|
||||
assert_eq!(client.get_current_chat_id(), Some(222));
|
||||
|
||||
// 5. Отправляем сообщение во втором чате
|
||||
client
|
||||
.send_message(ChatId::new(222), "Message in chat 2".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 6. Переключаемся на третий чат
|
||||
client.open_chat(ChatId::new(333)).await.unwrap();
|
||||
assert_eq!(client.get_current_chat_id(), Some(333));
|
||||
|
||||
// 7. Проверяем что сообщения были отправлены в правильные чаты
|
||||
assert_eq!(client.get_sent_messages().len(), 2);
|
||||
assert_eq!(client.get_sent_messages()[0].chat_id, 111);
|
||||
assert_eq!(client.get_sent_messages()[0].text, "Message in chat 1");
|
||||
assert_eq!(client.get_sent_messages()[1].chat_id, 222);
|
||||
assert_eq!(client.get_sent_messages()[1].text, "Message in chat 2");
|
||||
|
||||
// 8. Проверяем истории отдельных чатов
|
||||
let hist1 = client.get_chat_history(ChatId::new(111), 50).await.unwrap();
|
||||
let hist2 = client.get_chat_history(ChatId::new(222), 50).await.unwrap();
|
||||
let hist3 = client.get_chat_history(ChatId::new(333), 50).await.unwrap();
|
||||
|
||||
assert_eq!(hist1.len(), 1);
|
||||
assert_eq!(hist2.len(), 1);
|
||||
assert_eq!(hist3.len(), 0);
|
||||
}
|
||||
|
||||
/// Тест 6: Edit message in conversation flow
|
||||
/// Симулирует редактирование сообщения в процессе беседы
|
||||
#[tokio::test]
|
||||
async fn test_user_journey_edit_during_conversation() {
|
||||
// 1. Подготовка
|
||||
let chat = TestChatBuilder::new("Bob", 555).build();
|
||||
let client = FakeTdClient::new().with_chat(chat);
|
||||
|
||||
client.open_chat(ChatId::new(555)).await.unwrap();
|
||||
|
||||
// 2. Отправляем сообщение с опечаткой
|
||||
let msg = client
|
||||
.send_message(ChatId::new(555), "I'll be there at 5pm tomorow".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 3. Проверяем что сообщение отправлено
|
||||
let history = client.get_chat_history(ChatId::new(555), 50).await.unwrap();
|
||||
assert_eq!(history.len(), 1);
|
||||
assert_eq!(history[0].text(), "I'll be there at 5pm tomorow");
|
||||
|
||||
// 4. Исправляем опечатку
|
||||
client
|
||||
.edit_message(ChatId::new(555), msg.id(), "I'll be there at 5pm tomorrow".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 5. Проверяем что сообщение отредактировано
|
||||
let edited_history = client.get_chat_history(ChatId::new(555), 50).await.unwrap();
|
||||
assert_eq!(edited_history.len(), 1);
|
||||
assert_eq!(edited_history[0].text(), "I'll be there at 5pm tomorrow");
|
||||
assert!(
|
||||
edited_history[0].metadata.edit_date > 0,
|
||||
"Должна быть установлена дата редактирования"
|
||||
);
|
||||
|
||||
// 6. Проверяем историю редактирований
|
||||
assert_eq!(client.get_edited_messages().len(), 1);
|
||||
assert_eq!(client.get_edited_messages()[0].new_text, "I'll be there at 5pm tomorrow");
|
||||
}
|
||||
|
||||
/// Тест 7: Reply to message in conversation
|
||||
/// Симулирует ответ на конкретное сообщение
|
||||
#[tokio::test]
|
||||
async fn test_user_journey_reply_in_conversation() {
|
||||
// 1. Подготовка
|
||||
let chat = TestChatBuilder::new("Charlie", 666).build();
|
||||
let client = FakeTdClient::new().with_chat(chat);
|
||||
|
||||
client.open_chat(ChatId::new(666)).await.unwrap();
|
||||
|
||||
// 2. Setup updates
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
client.set_update_channel(tx);
|
||||
|
||||
// 3. Входящее сообщение с вопросом
|
||||
client.simulate_incoming_message(
|
||||
ChatId::new(666),
|
||||
"Can you send me the report?".to_string(),
|
||||
"Charlie",
|
||||
);
|
||||
|
||||
let update = rx.try_recv().ok();
|
||||
assert!(matches!(update, Some(TdUpdate::NewMessage { .. })));
|
||||
|
||||
let history = client.get_chat_history(ChatId::new(666), 50).await.unwrap();
|
||||
let question_msg_id = history[0].id();
|
||||
|
||||
// 4. Отправляем другое сообщение (не связанное)
|
||||
client
|
||||
.send_message(ChatId::new(666), "Working on it now".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 5. Отвечаем на конкретный вопрос (reply)
|
||||
let reply_info = Some(tele_tui::tdlib::ReplyInfo {
|
||||
message_id: question_msg_id,
|
||||
sender_name: "Charlie".to_string(),
|
||||
text: "Can you send me the report?".to_string(),
|
||||
});
|
||||
|
||||
client
|
||||
.send_message(
|
||||
ChatId::new(666),
|
||||
"Sure, sending now!".to_string(),
|
||||
Some(question_msg_id),
|
||||
reply_info,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 6. Проверяем что reply сохранён
|
||||
let final_history = client.get_chat_history(ChatId::new(666), 50).await.unwrap();
|
||||
assert_eq!(final_history.len(), 3);
|
||||
|
||||
// Последнее сообщение должно быть reply
|
||||
let reply_msg = &final_history[2];
|
||||
assert_eq!(reply_msg.text(), "Sure, sending now!");
|
||||
assert!(reply_msg.interactions.reply_to.is_some());
|
||||
|
||||
let reply_to = reply_msg.interactions.reply_to.as_ref().unwrap();
|
||||
assert_eq!(reply_to.message_id, question_msg_id);
|
||||
assert_eq!(reply_to.text, "Can you send me the report?");
|
||||
}
|
||||
|
||||
/// Тест 8: Network state changes during conversation
|
||||
/// Симулирует изменения состояния сети во время работы
|
||||
#[tokio::test]
|
||||
async fn test_user_journey_network_state_changes() {
|
||||
// 1. Подготовка
|
||||
let chat = TestChatBuilder::new("Network Test", 888).build();
|
||||
let client = FakeTdClient::new().with_chat(chat);
|
||||
|
||||
// 2. Setup updates
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
client.set_update_channel(tx);
|
||||
|
||||
// 3. Начальное состояние - Ready
|
||||
assert_eq!(client.get_network_state(), NetworkState::Ready);
|
||||
|
||||
// 4. Открываем чат и отправляем сообщение
|
||||
client.open_chat(ChatId::new(888)).await.unwrap();
|
||||
client
|
||||
.send_message(ChatId::new(888), "Test message".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Очищаем канал от update NewMessage
|
||||
let _ = rx.try_recv();
|
||||
|
||||
// 5. Симулируем потерю сети
|
||||
client.simulate_network_change(NetworkState::WaitingForNetwork);
|
||||
|
||||
// Проверяем update
|
||||
let update = rx.try_recv().ok();
|
||||
assert!(
|
||||
matches!(
|
||||
update,
|
||||
Some(TdUpdate::ConnectionState { state: NetworkState::WaitingForNetwork })
|
||||
),
|
||||
"Expected ConnectionState update, got: {:?}",
|
||||
update
|
||||
);
|
||||
|
||||
// 6. Проверяем что состояние изменилось
|
||||
assert_eq!(client.get_network_state(), NetworkState::WaitingForNetwork);
|
||||
|
||||
// 7. Симулируем восстановление соединения
|
||||
client.simulate_network_change(NetworkState::Connecting);
|
||||
assert_eq!(client.get_network_state(), NetworkState::Connecting);
|
||||
|
||||
client.simulate_network_change(NetworkState::Ready);
|
||||
assert_eq!(client.get_network_state(), NetworkState::Ready);
|
||||
|
||||
// 8. Отправляем сообщение после восстановления
|
||||
client
|
||||
.send_message(ChatId::new(888), "Connection restored!".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 9. Проверяем что оба сообщения в истории
|
||||
let history = client.get_chat_history(ChatId::new(888), 50).await.unwrap();
|
||||
assert_eq!(history.len(), 2);
|
||||
}
|
||||
218
crates/tele-tui/tests/edit_message.rs
Normal file
218
crates/tele-tui/tests/edit_message.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
// Integration tests for edit message flow
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::fake_tdclient::FakeTdClient;
|
||||
use helpers::test_data::TestMessageBuilder;
|
||||
use tele_tui::types::{ChatId, MessageId};
|
||||
|
||||
/// Test: Редактирование сообщения изменяет текст
|
||||
#[tokio::test]
|
||||
async fn test_edit_message_changes_text() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Отправляем сообщение
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "Original text".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Редактируем сообщение
|
||||
client
|
||||
.edit_message(ChatId::new(123), msg.id(), "Edited text".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что редактирование записалось
|
||||
assert_eq!(client.get_edited_messages().len(), 1);
|
||||
assert_eq!(client.get_edited_messages()[0].message_id, msg.id());
|
||||
assert_eq!(client.get_edited_messages()[0].new_text, "Edited text");
|
||||
|
||||
// Проверяем что текст сообщения изменился
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0].text(), "Edited text");
|
||||
}
|
||||
|
||||
/// Test: Редактирование устанавливает edit_date
|
||||
#[tokio::test]
|
||||
async fn test_edit_message_sets_edit_date() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Отправляем сообщение
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "Original".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Получаем дату до редактирования
|
||||
let messages_before = client.get_messages(123);
|
||||
let date_before = messages_before[0].date();
|
||||
assert_eq!(messages_before[0].metadata.edit_date, 0); // Не редактировалось
|
||||
|
||||
// Редактируем сообщение
|
||||
client
|
||||
.edit_message(ChatId::new(123), msg.id(), "Edited".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что edit_date установлена
|
||||
let messages_after = client.get_messages(123);
|
||||
assert!(messages_after[0].metadata.edit_date > 0);
|
||||
assert!(messages_after[0].metadata.edit_date > date_before); // edit_date после date
|
||||
}
|
||||
|
||||
/// Test: Редактирование только своих сообщений (проверка через can_be_edited)
|
||||
#[tokio::test]
|
||||
async fn test_can_only_edit_own_messages() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Наше исходящее сообщение (можно редактировать)
|
||||
let outgoing_msg = TestMessageBuilder::new("My message", 1).outgoing().build();
|
||||
|
||||
let client = client.with_message(123, outgoing_msg);
|
||||
|
||||
// Входящее сообщение от собеседника (нельзя редактировать)
|
||||
let incoming_msg = TestMessageBuilder::new("Their message", 2)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, incoming_msg);
|
||||
|
||||
// Проверяем флаги
|
||||
let messages = client.get_messages(123);
|
||||
assert!(messages[0].can_be_edited()); // Наше сообщение
|
||||
assert!(!messages[1].can_be_edited()); // Чужое сообщение
|
||||
}
|
||||
|
||||
/// Test: Множественные редактирования одного сообщения
|
||||
#[tokio::test]
|
||||
async fn test_multiple_edits_of_same_message() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "Version 1".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Первое редактирование
|
||||
client
|
||||
.edit_message(ChatId::new(123), msg.id(), "Version 2".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Второе редактирование
|
||||
client
|
||||
.edit_message(ChatId::new(123), msg.id(), "Version 3".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Третье редактирование
|
||||
client
|
||||
.edit_message(ChatId::new(123), msg.id(), "Final version".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что все 3 редактирования записаны
|
||||
assert_eq!(client.get_edited_messages().len(), 3);
|
||||
assert_eq!(client.get_edited_messages()[0].new_text, "Version 2");
|
||||
assert_eq!(client.get_edited_messages()[1].new_text, "Version 3");
|
||||
assert_eq!(client.get_edited_messages()[2].new_text, "Final version");
|
||||
|
||||
// Проверяем что сообщение содержит последнюю версию
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0].text(), "Final version");
|
||||
}
|
||||
|
||||
/// Test: Редактирование несуществующего сообщения (возвращает ошибку)
|
||||
#[tokio::test]
|
||||
async fn test_edit_nonexistent_message() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Пытаемся отредактировать несуществующее сообщение
|
||||
let result = client
|
||||
.edit_message(ChatId::new(123), MessageId::new(999), "New text".to_string())
|
||||
.await;
|
||||
|
||||
// Должна вернуться ошибка
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "Message not found");
|
||||
|
||||
// В списке сообщений ничего нет
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 0);
|
||||
}
|
||||
|
||||
/// Test: Отмена редактирования (Esc) - тестируем что можно восстановить original
|
||||
/// В данном случае проверяем что FakeTdClient сохраняет историю edits
|
||||
#[tokio::test]
|
||||
async fn test_edit_history_tracking() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "Original".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Симулируем начало редактирования -> изменение -> отмена
|
||||
// Отменять на уровне FakeTdClient нельзя, но можно проверить что original сохранён
|
||||
|
||||
// Сохраняем original
|
||||
let messages_before = client.get_messages(123);
|
||||
let original = messages_before[0].text().to_string();
|
||||
|
||||
// Редактируем
|
||||
client
|
||||
.edit_message(ChatId::new(123), msg.id(), "Edited".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что изменилось
|
||||
let messages_edited = client.get_messages(123);
|
||||
assert_eq!(messages_edited[0].text(), "Edited");
|
||||
|
||||
// Можем "отменить" редактирование вернув original
|
||||
client
|
||||
.edit_message(ChatId::new(123), msg.id(), original)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что вернулось
|
||||
let messages_restored = client.get_messages(123);
|
||||
assert_eq!(messages_restored[0].text(), "Original");
|
||||
|
||||
// История показывает 2 редактирования
|
||||
assert_eq!(client.get_edited_messages().len(), 2);
|
||||
}
|
||||
|
||||
/// Test: Редактирование сразу после отправки (симуляция UpdateMessageSendSucceeded)
|
||||
/// Проверяет что после send_message можно сразу edit_message с тем же ID
|
||||
#[tokio::test]
|
||||
async fn test_edit_immediately_after_send() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Отправляем сообщение
|
||||
let sent_msg = client
|
||||
.send_message(ChatId::new(123), "Just sent".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Сразу редактируем (не должно быть ошибки "Message not found")
|
||||
let result = client
|
||||
.edit_message(ChatId::new(123), sent_msg.id(), "Immediately edited".to_string())
|
||||
.await;
|
||||
|
||||
// Редактирование должно пройти успешно
|
||||
assert!(result.is_ok(), "Should be able to edit message immediately after sending");
|
||||
|
||||
// Проверяем что текст изменился
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0].text(), "Immediately edited");
|
||||
|
||||
// История редактирований содержит это изменение
|
||||
assert_eq!(client.get_edited_messages().len(), 1);
|
||||
assert_eq!(client.get_edited_messages()[0].message_id, sent_msg.id());
|
||||
assert_eq!(client.get_edited_messages()[0].new_text, "Immediately edited");
|
||||
}
|
||||
108
crates/tele-tui/tests/footer.rs
Normal file
108
crates/tele-tui/tests/footer.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
// Footer UI snapshot tests
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::app_builder::TestAppBuilder;
|
||||
use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
|
||||
use helpers::test_data::create_test_chat;
|
||||
use insta::assert_snapshot;
|
||||
use tele_tui::tdlib::NetworkState;
|
||||
|
||||
#[test]
|
||||
fn snapshot_footer_chat_list() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let app = TestAppBuilder::new().with_chat(chat).build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::footer::render(f, f.area(), &app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("footer_chat_list", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_footer_open_chat() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::footer::render(f, f.area(), &app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("footer_open_chat", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_footer_network_waiting() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let app = TestAppBuilder::new().with_chat(chat).build();
|
||||
|
||||
// Set network state to WaitingForNetwork
|
||||
*app.td_client.network_state.lock().unwrap() = NetworkState::WaitingForNetwork;
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::footer::render(f, f.area(), &app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("footer_network_waiting", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_footer_network_connecting_proxy() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let app = TestAppBuilder::new().with_chat(chat).build();
|
||||
|
||||
// Set network state to ConnectingToProxy
|
||||
*app.td_client.network_state.lock().unwrap() = NetworkState::ConnectingToProxy;
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::footer::render(f, f.area(), &app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("footer_network_connecting_proxy", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_footer_network_connecting() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let app = TestAppBuilder::new().with_chat(chat).build();
|
||||
|
||||
// Set network state to Connecting
|
||||
*app.td_client.network_state.lock().unwrap() = NetworkState::Connecting;
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::footer::render(f, f.area(), &app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("footer_network_connecting", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_footer_search_mode() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.searching("query")
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::footer::render(f, f.area(), &app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("footer_search_mode", output);
|
||||
}
|
||||
352
crates/tele-tui/tests/helpers/app_builder.rs
Normal file
352
crates/tele-tui/tests/helpers/app_builder.rs
Normal file
@@ -0,0 +1,352 @@
|
||||
// Test App builder
|
||||
|
||||
use super::FakeTdClient;
|
||||
use ratatui::widgets::ListState;
|
||||
use std::collections::HashMap;
|
||||
use tele_tui::app::{App, AppScreen, ChatState, InputMode};
|
||||
use tele_tui::config::Config;
|
||||
use tele_tui::tdlib::AuthState;
|
||||
use tele_tui::tdlib::{ChatInfo, MessageInfo};
|
||||
use tele_tui::types::{ChatId, MessageId};
|
||||
|
||||
/// Builder для создания тестового App с FakeTdClient\n///\n/// Использует trait-based DI для подмены TdClient на FakeTdClient в тестах.
|
||||
#[allow(dead_code)]
|
||||
pub struct TestAppBuilder {
|
||||
config: Config,
|
||||
screen: AppScreen,
|
||||
chats: Vec<ChatInfo>,
|
||||
selected_chat_id: Option<i64>,
|
||||
message_input: String,
|
||||
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>,
|
||||
phone_input: Option<String>,
|
||||
code_input: Option<String>,
|
||||
password_input: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for TestAppBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TestAppBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
config: Config::default(),
|
||||
screen: AppScreen::Main,
|
||||
chats: vec![],
|
||||
selected_chat_id: None,
|
||||
message_input: String::new(),
|
||||
is_searching: false,
|
||||
search_query: String::new(),
|
||||
chat_state: None,
|
||||
input_mode: None,
|
||||
messages: HashMap::new(),
|
||||
status_message: None,
|
||||
auth_state: None,
|
||||
phone_input: None,
|
||||
code_input: None,
|
||||
password_input: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Установить экран
|
||||
pub fn screen(mut self, screen: AppScreen) -> Self {
|
||||
self.screen = screen;
|
||||
self
|
||||
}
|
||||
|
||||
/// Установить конфиг
|
||||
pub fn config(mut self, config: Config) -> Self {
|
||||
self.config = config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Добавить чат
|
||||
pub fn with_chat(mut self, chat: ChatInfo) -> Self {
|
||||
self.chats.push(chat);
|
||||
self
|
||||
}
|
||||
|
||||
/// Добавить несколько чатов
|
||||
pub fn with_chats(mut self, chats: Vec<ChatInfo>) -> Self {
|
||||
self.chats.extend(chats);
|
||||
self
|
||||
}
|
||||
|
||||
/// Выбрать чат
|
||||
pub fn selected_chat(mut self, chat_id: i64) -> Self {
|
||||
self.selected_chat_id = Some(chat_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Установить текст в инпуте
|
||||
pub fn message_input(mut self, text: &str) -> Self {
|
||||
self.message_input = text.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Режим поиска
|
||||
pub fn searching(mut self, query: &str) -> Self {
|
||||
self.is_searching = true;
|
||||
self.search_query = query.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Режим редактирования сообщения
|
||||
pub fn editing_message(mut self, message_id: i64, selected_index: usize) -> Self {
|
||||
self.chat_state = Some(ChatState::Editing {
|
||||
message_id: MessageId::new(message_id),
|
||||
selected_index,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Режим ответа на сообщение
|
||||
pub fn replying_to(mut self, message_id: i64) -> Self {
|
||||
self.chat_state = Some(ChatState::Reply { message_id: MessageId::new(message_id) });
|
||||
self
|
||||
}
|
||||
|
||||
/// Режим выбора реакции
|
||||
pub fn reaction_picker(mut self, message_id: i64, available_reactions: Vec<String>) -> Self {
|
||||
self.chat_state = Some(ChatState::ReactionPicker {
|
||||
message_id: MessageId::new(message_id),
|
||||
available_reactions,
|
||||
selected_index: 0,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Режим профиля
|
||||
pub fn profile_mode(mut self, info: tele_tui::tdlib::ProfileInfo) -> Self {
|
||||
self.chat_state = Some(ChatState::Profile {
|
||||
info,
|
||||
selected_action: 0,
|
||||
leave_group_confirmation_step: 0,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Подтверждение удаления
|
||||
pub fn delete_confirmation(mut self, message_id: i64) -> Self {
|
||||
self.chat_state =
|
||||
Some(ChatState::DeleteConfirmation { message_id: MessageId::new(message_id) });
|
||||
self
|
||||
}
|
||||
|
||||
/// Добавить сообщение для чата
|
||||
pub fn with_message(mut self, chat_id: i64, message: MessageInfo) -> Self {
|
||||
self.messages.entry(chat_id).or_default().push(message);
|
||||
self
|
||||
}
|
||||
|
||||
/// Добавить несколько сообщений для чата
|
||||
pub fn with_messages(mut self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
|
||||
self.messages.entry(chat_id).or_default().extend(messages);
|
||||
self
|
||||
}
|
||||
|
||||
/// Установить выбранное сообщение (режим selection)
|
||||
pub fn selecting_message(mut self, selected_index: usize) -> Self {
|
||||
self.chat_state = Some(ChatState::MessageSelection { selected_index });
|
||||
self
|
||||
}
|
||||
|
||||
/// Режим поиска по сообщениям в чате
|
||||
pub fn message_search(mut self, query: &str) -> Self {
|
||||
self.chat_state = Some(ChatState::SearchInChat {
|
||||
query: query.to_string(),
|
||||
results: Vec::new(),
|
||||
selected_index: 0,
|
||||
});
|
||||
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 { message_id: MessageId::new(message_id) });
|
||||
self
|
||||
}
|
||||
|
||||
/// Установить статус сообщение (для loading screen)
|
||||
pub fn status_message(mut self, message: &str) -> Self {
|
||||
self.status_message = Some(message.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Установить auth state
|
||||
pub fn auth_state(mut self, state: AuthState) -> Self {
|
||||
self.auth_state = Some(state);
|
||||
self
|
||||
}
|
||||
|
||||
/// Установить phone input
|
||||
pub fn phone_input(mut self, phone: &str) -> Self {
|
||||
self.phone_input = Some(phone.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Установить code input
|
||||
pub fn code_input(mut self, code: &str) -> Self {
|
||||
self.code_input = Some(code.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Установить password input
|
||||
pub fn password_input(mut self, password: &str) -> Self {
|
||||
self.password_input = Some(password.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Построить App с FakeTdClient
|
||||
///
|
||||
/// Создаёт App с FakeTdClient, подходит для любых тестов включая
|
||||
/// интеграционные тесты логики.
|
||||
pub fn build(self) -> App<FakeTdClient> {
|
||||
// Создаём FakeTdClient с чатами и сообщениями
|
||||
let mut fake_client = FakeTdClient::new();
|
||||
|
||||
// Добавляем чаты
|
||||
for chat in &self.chats {
|
||||
fake_client = fake_client.with_chat(chat.clone());
|
||||
}
|
||||
|
||||
// Добавляем сообщения
|
||||
for (chat_id, messages) in self.messages {
|
||||
fake_client = fake_client.with_messages(chat_id, messages);
|
||||
}
|
||||
|
||||
// Устанавливаем текущий чат если нужно
|
||||
if let Some(chat_id) = self.selected_chat_id {
|
||||
*fake_client.current_chat_id.lock().unwrap() = Some(chat_id);
|
||||
}
|
||||
|
||||
// Устанавливаем auth state если нужно
|
||||
if let Some(auth_state) = self.auth_state {
|
||||
fake_client = fake_client.with_auth_state(auth_state);
|
||||
}
|
||||
|
||||
// Создаём App с FakeTdClient
|
||||
let mut app = App::with_client(self.config, fake_client);
|
||||
|
||||
app.screen = self.screen;
|
||||
app.chats = self.chats;
|
||||
app.selected_chat_id = self.selected_chat_id.map(ChatId::new);
|
||||
app.message_input = self.message_input;
|
||||
app.is_searching = self.is_searching;
|
||||
app.search_query = self.search_query;
|
||||
|
||||
// Применяем chat_state если он установлен
|
||||
if let Some(chat_state) = self.chat_state {
|
||||
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);
|
||||
}
|
||||
|
||||
// Применяем auth inputs
|
||||
if let Some(phone) = self.phone_input {
|
||||
app.set_phone_input(phone);
|
||||
}
|
||||
if let Some(code) = self.code_input {
|
||||
app.set_code_input(code);
|
||||
}
|
||||
if let Some(password) = self.password_input {
|
||||
app.set_password_input(password);
|
||||
}
|
||||
|
||||
// Выбираем первый чат если есть
|
||||
if !app.chats.is_empty() {
|
||||
let mut list_state = ListState::default();
|
||||
list_state.select(Some(0));
|
||||
app.chat_list_state = list_state;
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::helpers::test_data::create_test_chat;
|
||||
use tele_tui::app::methods::messages::MessageMethods;
|
||||
|
||||
#[test]
|
||||
fn test_builder_defaults() {
|
||||
let app = TestAppBuilder::new().build();
|
||||
|
||||
assert_eq!(app.screen, AppScreen::Main);
|
||||
assert_eq!(app.chats.len(), 0);
|
||||
assert_eq!(app.selected_chat_id, None);
|
||||
assert_eq!(app.message_input, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_with_chats() {
|
||||
let chat1 = create_test_chat("Mom", 123);
|
||||
let chat2 = create_test_chat("Boss", 456);
|
||||
|
||||
let app = TestAppBuilder::new()
|
||||
.with_chat(chat1)
|
||||
.with_chat(chat2)
|
||||
.build();
|
||||
|
||||
assert_eq!(app.chats.len(), 2);
|
||||
assert_eq!(app.chats[0].title, "Mom");
|
||||
assert_eq!(app.chats[1].title, "Boss");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_with_selected_chat() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
assert_eq!(app.selected_chat_id, Some(ChatId::new(123)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_editing_mode() {
|
||||
let app = TestAppBuilder::new()
|
||||
.editing_message(999, 0)
|
||||
.message_input("Edited text")
|
||||
.build();
|
||||
|
||||
assert!(app.is_editing());
|
||||
assert_eq!(app.chat_state.selected_message_id(), Some(MessageId::new(999)));
|
||||
assert_eq!(app.message_input, "Edited text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_search_mode() {
|
||||
let app = TestAppBuilder::new().searching("test query").build();
|
||||
|
||||
assert!(app.is_searching);
|
||||
assert_eq!(app.search_query, "test query");
|
||||
}
|
||||
}
|
||||
146
crates/tele-tui/tests/helpers/fake_tdclient.rs
Normal file
146
crates/tele-tui/tests/helpers/fake_tdclient.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
// Fake TDLib client for testing.
|
||||
|
||||
mod builders;
|
||||
mod inspect;
|
||||
mod operations;
|
||||
mod state;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use state::{
|
||||
DeletedMessages, EditedMessage, FakeTdClient, ForwardedMessages, PendingViewMessages,
|
||||
SearchQuery, SentMessage, TdUpdate, ViewedMessages,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::helpers::test_data::create_test_chat;
|
||||
use tele_tui::types::ChatId;
|
||||
|
||||
#[test]
|
||||
fn test_fake_client_creation() {
|
||||
let client = FakeTdClient::new();
|
||||
assert_eq!(client.get_chats().len(), 0);
|
||||
assert_eq!(client.folders.lock().unwrap().len(), 1); // Default "All" folder
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fake_client_with_chat() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let client = FakeTdClient::new().with_chat(chat);
|
||||
|
||||
let chats = client.get_chats();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats[0].title, "Mom");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_message() {
|
||||
let client = FakeTdClient::new();
|
||||
let chat_id = ChatId::new(123);
|
||||
|
||||
let result = client
|
||||
.send_message(chat_id, "Hello".to_string(), None, None)
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let sent = client.get_sent_messages();
|
||||
assert_eq!(sent.len(), 1);
|
||||
assert_eq!(sent[0].text, "Hello");
|
||||
assert_eq!(client.get_messages(123).len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_edit_message() {
|
||||
let client = FakeTdClient::new();
|
||||
let chat_id = ChatId::new(123);
|
||||
|
||||
let msg = client
|
||||
.send_message(chat_id, "Hello".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg_id = msg.id();
|
||||
|
||||
let _ = client
|
||||
.edit_message(chat_id, msg_id, "Hello World".to_string())
|
||||
.await;
|
||||
|
||||
let edited = client.get_edited_messages();
|
||||
assert_eq!(edited.len(), 1);
|
||||
assert_eq!(client.get_messages(123)[0].text(), "Hello World");
|
||||
assert!(client.get_messages(123)[0].metadata.edit_date > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_message() {
|
||||
let client = FakeTdClient::new();
|
||||
let chat_id = ChatId::new(123);
|
||||
|
||||
let msg = client
|
||||
.send_message(chat_id, "Hello".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msg_id = msg.id();
|
||||
|
||||
let _ = client.delete_messages(chat_id, vec![msg_id], false).await;
|
||||
|
||||
let deleted = client.get_deleted_messages();
|
||||
assert_eq!(deleted.len(), 1);
|
||||
assert_eq!(client.get_messages(123).len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_channel() {
|
||||
let (client, mut rx) = FakeTdClient::new().with_update_channel();
|
||||
let chat_id = ChatId::new(123);
|
||||
|
||||
let _ = client
|
||||
.send_message(chat_id, "Test".to_string(), None, None)
|
||||
.await;
|
||||
|
||||
if let Some(update) = rx.recv().await {
|
||||
match update {
|
||||
TdUpdate::NewMessage { chat_id: updated_chat, .. } => {
|
||||
assert_eq!(updated_chat, chat_id);
|
||||
}
|
||||
_ => panic!("Expected NewMessage update"),
|
||||
}
|
||||
} else {
|
||||
panic!("No update received");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simulate_incoming_message() {
|
||||
let (client, mut rx) = FakeTdClient::new().with_update_channel();
|
||||
let chat_id = ChatId::new(123);
|
||||
|
||||
client.simulate_incoming_message(chat_id, "Hello from Bob".to_string(), "Bob");
|
||||
|
||||
if let Some(TdUpdate::NewMessage { message, .. }) = rx.recv().await {
|
||||
assert_eq!(message.text(), "Hello from Bob");
|
||||
assert_eq!(message.sender_name(), "Bob");
|
||||
assert!(!message.is_outgoing());
|
||||
}
|
||||
|
||||
assert_eq!(client.get_messages(123).len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fail_next_operation() {
|
||||
let client = FakeTdClient::new();
|
||||
let chat_id = ChatId::new(123);
|
||||
|
||||
client.fail_next();
|
||||
|
||||
let result = client
|
||||
.send_message(chat_id, "Test".to_string(), None, None)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
let result2 = client
|
||||
.send_message(chat_id, "Test2".to_string(), None, None)
|
||||
.await;
|
||||
assert!(result2.is_ok());
|
||||
}
|
||||
}
|
||||
86
crates/tele-tui/tests/helpers/fake_tdclient/builders.rs
Normal file
86
crates/tele-tui/tests/helpers/fake_tdclient/builders.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use super::{FakeTdClient, TdUpdate};
|
||||
use tele_tui::tdlib::types::FolderInfo;
|
||||
use tele_tui::tdlib::{AuthState, ChatInfo, MessageInfo, NetworkState, ProfileInfo};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl FakeTdClient {
|
||||
/// Create an update channel for receiving simulated TDLib events.
|
||||
pub fn with_update_channel(self) -> (Self, mpsc::UnboundedReceiver<TdUpdate>) {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
*self.update_tx.lock().unwrap() = Some(tx);
|
||||
(self, rx)
|
||||
}
|
||||
|
||||
/// Enable simulated delays, closer to real TDLib behavior.
|
||||
pub fn with_delays(mut self) -> Self {
|
||||
self.simulate_delays = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_chat(self, chat: ChatInfo) -> Self {
|
||||
self.chats.lock().unwrap().push(chat);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_chats(self, chats: Vec<ChatInfo>) -> Self {
|
||||
self.chats.lock().unwrap().extend(chats);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_message(self, chat_id: i64, message: MessageInfo) -> Self {
|
||||
self.messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(chat_id)
|
||||
.or_default()
|
||||
.push(message);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_messages(self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
|
||||
self.messages.lock().unwrap().insert(chat_id, messages);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_folder(self, id: i32, name: &str) -> Self {
|
||||
self.folders
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(FolderInfo { id, name: name.to_string() });
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_user(self, id: i64, name: &str) -> Self {
|
||||
self.user_names.lock().unwrap().insert(id, name.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_profile(self, chat_id: i64, profile: ProfileInfo) -> Self {
|
||||
self.profiles.lock().unwrap().insert(chat_id, profile);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_network_state(self, state: NetworkState) -> Self {
|
||||
*self.network_state.lock().unwrap() = state;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_auth_state(self, state: AuthState) -> Self {
|
||||
*self.auth_state.lock().unwrap() = state;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_downloaded_file(self, file_id: i32, path: &str) -> Self {
|
||||
self.downloaded_files
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(file_id, path.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_available_reactions(self, reactions: Vec<String>) -> Self {
|
||||
*self.available_reactions.lock().unwrap() = reactions;
|
||||
self
|
||||
}
|
||||
}
|
||||
92
crates/tele-tui/tests/helpers/fake_tdclient/inspect.rs
Normal file
92
crates/tele-tui/tests/helpers/fake_tdclient/inspect.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use super::{
|
||||
DeletedMessages, EditedMessage, FakeTdClient, ForwardedMessages, SearchQuery, SentMessage,
|
||||
TdUpdate,
|
||||
};
|
||||
use tele_tui::tdlib::types::FolderInfo;
|
||||
use tele_tui::tdlib::{ChatInfo, MessageInfo, NetworkState};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl FakeTdClient {
|
||||
pub fn get_chats(&self) -> Vec<ChatInfo> {
|
||||
self.chats.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_folders(&self) -> Vec<FolderInfo> {
|
||||
self.folders.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_messages(&self, chat_id: i64) -> Vec<MessageInfo> {
|
||||
self.messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&chat_id)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_sent_messages(&self) -> Vec<SentMessage> {
|
||||
self.sent_messages.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_edited_messages(&self) -> Vec<EditedMessage> {
|
||||
self.edited_messages.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_deleted_messages(&self) -> Vec<DeletedMessages> {
|
||||
self.deleted_messages.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_forwarded_messages(&self) -> Vec<ForwardedMessages> {
|
||||
self.forwarded_messages.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_search_queries(&self) -> Vec<SearchQuery> {
|
||||
self.searched_queries.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_viewed_messages(&self) -> Vec<(i64, Vec<i64>)> {
|
||||
self.viewed_messages.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_chat_actions(&self) -> Vec<(i64, String)> {
|
||||
self.chat_actions.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_network_state(&self) -> NetworkState {
|
||||
self.network_state.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_current_chat_id(&self) -> Option<i64> {
|
||||
*self.current_chat_id.lock().unwrap()
|
||||
}
|
||||
|
||||
pub fn set_current_pinned_message(&mut self, msg: Option<MessageInfo>) {
|
||||
*self.current_pinned_message.lock().unwrap() = msg;
|
||||
}
|
||||
|
||||
pub async fn process_pending_view_messages(&mut self) {
|
||||
let mut pending = self.pending_view_messages.lock().unwrap();
|
||||
for (chat_id, message_ids) in pending.drain(..) {
|
||||
let ids: Vec<i64> = message_ids.iter().map(|id| id.as_i64()).collect();
|
||||
self.viewed_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((chat_id.as_i64(), ids));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_update_channel(&self, tx: mpsc::UnboundedSender<TdUpdate>) {
|
||||
*self.update_tx.lock().unwrap() = Some(tx);
|
||||
}
|
||||
|
||||
pub fn clear_all_history(&self) {
|
||||
self.sent_messages.lock().unwrap().clear();
|
||||
self.edited_messages.lock().unwrap().clear();
|
||||
self.deleted_messages.lock().unwrap().clear();
|
||||
self.forwarded_messages.lock().unwrap().clear();
|
||||
self.searched_queries.lock().unwrap().clear();
|
||||
self.viewed_messages.lock().unwrap().clear();
|
||||
self.chat_actions.lock().unwrap().clear();
|
||||
}
|
||||
}
|
||||
458
crates/tele-tui/tests/helpers/fake_tdclient/operations.rs
Normal file
458
crates/tele-tui/tests/helpers/fake_tdclient/operations.rs
Normal file
@@ -0,0 +1,458 @@
|
||||
use super::{
|
||||
DeletedMessages, EditedMessage, FakeTdClient, ForwardedMessages, SearchQuery, SentMessage,
|
||||
TdUpdate,
|
||||
};
|
||||
use tele_tui::tdlib::types::ReactionInfo;
|
||||
use tele_tui::tdlib::{ChatInfo, MessageInfo, ProfileInfo, ReplyInfo};
|
||||
use tele_tui::types::{ChatId, MessageId, UserId};
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl FakeTdClient {
|
||||
pub async fn load_chats(&self, limit: usize) -> Result<Vec<ChatInfo>, String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to load chats".to_string());
|
||||
}
|
||||
|
||||
if self.simulate_delays {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
}
|
||||
|
||||
let chats = self
|
||||
.chats
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.take(limit)
|
||||
.cloned()
|
||||
.collect();
|
||||
Ok(chats)
|
||||
}
|
||||
|
||||
pub async fn open_chat(&self, chat_id: ChatId) -> Result<(), String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to open chat".to_string());
|
||||
}
|
||||
|
||||
*self.current_chat_id.lock().unwrap() = Some(chat_id.as_i64());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_chat_history(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
limit: i32,
|
||||
) -> Result<Vec<MessageInfo>, String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to load history".to_string());
|
||||
}
|
||||
|
||||
if self.simulate_delays {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
let messages = self
|
||||
.messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&chat_id.as_i64())
|
||||
.map(|msgs| msgs.iter().take(limit as usize).cloned().collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
pub async fn load_older_messages(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
from_message_id: MessageId,
|
||||
) -> Result<Vec<MessageInfo>, String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to load older messages".to_string());
|
||||
}
|
||||
|
||||
let messages = self.messages.lock().unwrap();
|
||||
let chat_messages = messages.get(&chat_id.as_i64()).ok_or("Chat not found")?;
|
||||
|
||||
if let Some(idx) = chat_messages.iter().position(|m| m.id() == from_message_id) {
|
||||
let older = chat_messages.iter().take(idx).cloned().collect();
|
||||
Ok(older)
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_message(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
text: String,
|
||||
reply_to: Option<MessageId>,
|
||||
reply_info: Option<ReplyInfo>,
|
||||
) -> Result<MessageInfo, String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to send message".to_string());
|
||||
}
|
||||
|
||||
if self.simulate_delays {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||
}
|
||||
|
||||
let message_id = MessageId::new((self.sent_messages.lock().unwrap().len() as i64) + 1000);
|
||||
|
||||
self.sent_messages.lock().unwrap().push(SentMessage {
|
||||
chat_id: chat_id.as_i64(),
|
||||
text: text.clone(),
|
||||
reply_to,
|
||||
reply_info: reply_info.clone(),
|
||||
});
|
||||
|
||||
let message = MessageInfo::new(
|
||||
message_id,
|
||||
"You".to_string(),
|
||||
true,
|
||||
text,
|
||||
vec![],
|
||||
chrono::Utc::now().timestamp() as i32,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
reply_info,
|
||||
None,
|
||||
vec![],
|
||||
);
|
||||
|
||||
self.messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(chat_id.as_i64())
|
||||
.or_default()
|
||||
.push(message.clone());
|
||||
|
||||
self.send_update(TdUpdate::NewMessage { chat_id, message: Box::new(message.clone()) });
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub async fn edit_message(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
message_id: MessageId,
|
||||
new_text: String,
|
||||
) -> Result<MessageInfo, String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to edit message".to_string());
|
||||
}
|
||||
|
||||
if self.simulate_delays {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
|
||||
}
|
||||
|
||||
self.edited_messages.lock().unwrap().push(EditedMessage {
|
||||
chat_id: chat_id.as_i64(),
|
||||
message_id,
|
||||
new_text: new_text.clone(),
|
||||
});
|
||||
|
||||
let mut messages = self.messages.lock().unwrap();
|
||||
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
||||
if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) {
|
||||
msg.content.text = new_text.clone();
|
||||
msg.metadata.edit_date = msg.metadata.date + 60;
|
||||
|
||||
let updated = msg.clone();
|
||||
drop(messages);
|
||||
|
||||
self.send_update(TdUpdate::MessageContent { chat_id, message_id, new_text });
|
||||
|
||||
return Ok(updated);
|
||||
}
|
||||
}
|
||||
|
||||
Err("Message not found".to_string())
|
||||
}
|
||||
|
||||
pub async fn delete_messages(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
message_ids: Vec<MessageId>,
|
||||
revoke: bool,
|
||||
) -> Result<(), String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to delete messages".to_string());
|
||||
}
|
||||
|
||||
if self.simulate_delays {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
self.deleted_messages.lock().unwrap().push(DeletedMessages {
|
||||
chat_id: chat_id.as_i64(),
|
||||
message_ids: message_ids.clone(),
|
||||
revoke,
|
||||
});
|
||||
|
||||
let mut messages = self.messages.lock().unwrap();
|
||||
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
||||
chat_msgs.retain(|m| !message_ids.contains(&m.id()));
|
||||
}
|
||||
drop(messages);
|
||||
|
||||
self.send_update(TdUpdate::DeleteMessages { chat_id, message_ids });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn forward_messages(
|
||||
&self,
|
||||
to_chat_id: ChatId,
|
||||
from_chat_id: ChatId,
|
||||
message_ids: Vec<MessageId>,
|
||||
) -> Result<(), String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to forward messages".to_string());
|
||||
}
|
||||
|
||||
if self.simulate_delays {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
|
||||
}
|
||||
|
||||
self.forwarded_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(ForwardedMessages {
|
||||
from_chat_id: from_chat_id.as_i64(),
|
||||
to_chat_id: to_chat_id.as_i64(),
|
||||
message_ids,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn search_messages(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
query: &str,
|
||||
) -> Result<Vec<MessageInfo>, String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to search messages".to_string());
|
||||
}
|
||||
|
||||
let messages = self.messages.lock().unwrap();
|
||||
let results: Vec<_> = messages
|
||||
.get(&chat_id.as_i64())
|
||||
.map(|msgs| {
|
||||
msgs.iter()
|
||||
.filter(|m| m.text().to_lowercase().contains(&query.to_lowercase()))
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
self.searched_queries.lock().unwrap().push(SearchQuery {
|
||||
chat_id: chat_id.as_i64(),
|
||||
query: query.to_string(),
|
||||
results_count: results.len(),
|
||||
});
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> {
|
||||
if text.is_empty() {
|
||||
self.drafts.lock().unwrap().remove(&chat_id.as_i64());
|
||||
} else {
|
||||
self.drafts
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(chat_id.as_i64(), text.clone());
|
||||
}
|
||||
|
||||
self.send_update(TdUpdate::ChatDraftMessage {
|
||||
chat_id,
|
||||
draft_text: if text.is_empty() { None } else { Some(text) },
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_chat_action(&self, chat_id: ChatId, action: String) {
|
||||
self.chat_actions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((chat_id.as_i64(), action.clone()));
|
||||
|
||||
if action == "Typing" {
|
||||
*self.typing_chat_id.lock().unwrap() = Some(chat_id.as_i64());
|
||||
} else if action == "Cancel" {
|
||||
*self.typing_chat_id.lock().unwrap() = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_message_available_reactions(
|
||||
&self,
|
||||
_chat_id: ChatId,
|
||||
_message_id: MessageId,
|
||||
) -> Result<Vec<String>, String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to get available reactions".to_string());
|
||||
}
|
||||
|
||||
Ok(self.available_reactions.lock().unwrap().clone())
|
||||
}
|
||||
|
||||
pub async fn toggle_reaction(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
message_id: MessageId,
|
||||
emoji: String,
|
||||
) -> Result<(), String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to toggle reaction".to_string());
|
||||
}
|
||||
|
||||
let mut messages = self.messages.lock().unwrap();
|
||||
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
||||
if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) {
|
||||
let reactions = &mut msg.interactions.reactions;
|
||||
|
||||
if let Some(pos) = reactions
|
||||
.iter()
|
||||
.position(|reaction| reaction.emoji == emoji && reaction.is_chosen)
|
||||
{
|
||||
reactions.remove(pos);
|
||||
} else if let Some(reaction) = reactions
|
||||
.iter_mut()
|
||||
.find(|reaction| reaction.emoji == emoji)
|
||||
{
|
||||
reaction.is_chosen = true;
|
||||
reaction.count += 1;
|
||||
} else {
|
||||
reactions.push(ReactionInfo {
|
||||
emoji: emoji.clone(),
|
||||
count: 1,
|
||||
is_chosen: true,
|
||||
});
|
||||
}
|
||||
|
||||
let updated_reactions = reactions.clone();
|
||||
drop(messages);
|
||||
|
||||
self.send_update(TdUpdate::MessageInteractionInfo {
|
||||
chat_id,
|
||||
message_id,
|
||||
reactions: updated_reactions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_file(&self, file_id: i32) -> Result<String, String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to download file".to_string());
|
||||
}
|
||||
|
||||
self.downloaded_files
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&file_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("File {} not found", file_id))
|
||||
}
|
||||
|
||||
pub async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo, String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to get profile info".to_string());
|
||||
}
|
||||
|
||||
self.profiles
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&chat_id.as_i64())
|
||||
.cloned()
|
||||
.ok_or_else(|| "Profile not found".to_string())
|
||||
}
|
||||
|
||||
pub async fn view_messages(&self, chat_id: ChatId, message_ids: Vec<MessageId>) {
|
||||
self.viewed_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((chat_id.as_i64(), message_ids.iter().map(|id| id.as_i64()).collect()));
|
||||
}
|
||||
|
||||
pub async fn load_folder_chats(&self, _folder_id: i32, _limit: usize) -> Result<(), String> {
|
||||
if self.should_fail() {
|
||||
return Err("Failed to load folder chats".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_update(&self, update: TdUpdate) {
|
||||
if let Some(tx) = self.update_tx.lock().unwrap().as_ref() {
|
||||
let _ = tx.send(update);
|
||||
}
|
||||
}
|
||||
|
||||
fn should_fail(&self) -> bool {
|
||||
let mut fail = self.fail_next_operation.lock().unwrap();
|
||||
if *fail {
|
||||
*fail = false;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fail_next(&self) {
|
||||
*self.fail_next_operation.lock().unwrap() = true;
|
||||
}
|
||||
|
||||
pub fn simulate_incoming_message(&self, chat_id: ChatId, text: String, sender_name: &str) {
|
||||
let message_id = MessageId::new(9000 + chrono::Utc::now().timestamp());
|
||||
|
||||
let message = MessageInfo::new(
|
||||
message_id,
|
||||
sender_name.to_string(),
|
||||
false,
|
||||
text,
|
||||
vec![],
|
||||
chrono::Utc::now().timestamp() as i32,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
vec![],
|
||||
);
|
||||
|
||||
self.messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(chat_id.as_i64())
|
||||
.or_default()
|
||||
.push(message.clone());
|
||||
|
||||
self.send_update(TdUpdate::NewMessage { chat_id, message: Box::new(message) });
|
||||
}
|
||||
|
||||
pub fn simulate_typing(&self, chat_id: ChatId, user_id: UserId) {
|
||||
self.send_update(TdUpdate::ChatAction { chat_id, user_id, action: "Typing".to_string() });
|
||||
}
|
||||
|
||||
pub fn simulate_network_change(&self, state: tele_tui::tdlib::NetworkState) {
|
||||
*self.network_state.lock().unwrap() = state.clone();
|
||||
self.send_update(TdUpdate::ConnectionState { state });
|
||||
}
|
||||
|
||||
pub fn simulate_read_outbox(&self, chat_id: ChatId, last_read_message_id: MessageId) {
|
||||
self.send_update(TdUpdate::ChatReadOutbox {
|
||||
chat_id,
|
||||
last_read_outbox_message_id: last_read_message_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
201
crates/tele-tui/tests/helpers/fake_tdclient/state.rs
Normal file
201
crates/tele-tui/tests/helpers/fake_tdclient/state.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tele_tui::tdlib::types::{FolderInfo, ReactionInfo};
|
||||
use tele_tui::tdlib::{AuthState, ChatInfo, MessageInfo, NetworkState, ProfileInfo, ReplyInfo};
|
||||
use tele_tui::types::{ChatId, MessageId, UserId};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
pub type ViewedMessages = Vec<(i64, Vec<i64>)>;
|
||||
pub type PendingViewMessages = Vec<(ChatId, Vec<MessageId>)>;
|
||||
|
||||
/// Update events from TDLib, simplified for tests.
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub enum TdUpdate {
|
||||
NewMessage {
|
||||
chat_id: ChatId,
|
||||
message: Box<MessageInfo>,
|
||||
},
|
||||
MessageContent {
|
||||
chat_id: ChatId,
|
||||
message_id: MessageId,
|
||||
new_text: String,
|
||||
},
|
||||
DeleteMessages {
|
||||
chat_id: ChatId,
|
||||
message_ids: Vec<MessageId>,
|
||||
},
|
||||
ChatAction {
|
||||
chat_id: ChatId,
|
||||
user_id: UserId,
|
||||
action: String,
|
||||
},
|
||||
MessageInteractionInfo {
|
||||
chat_id: ChatId,
|
||||
message_id: MessageId,
|
||||
reactions: Vec<ReactionInfo>,
|
||||
},
|
||||
ConnectionState {
|
||||
state: NetworkState,
|
||||
},
|
||||
ChatReadOutbox {
|
||||
chat_id: ChatId,
|
||||
last_read_outbox_message_id: MessageId,
|
||||
},
|
||||
ChatDraftMessage {
|
||||
chat_id: ChatId,
|
||||
draft_text: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Simplified mock TDLib client for tests.
|
||||
#[allow(dead_code)]
|
||||
pub struct FakeTdClient {
|
||||
pub chats: Arc<Mutex<Vec<ChatInfo>>>,
|
||||
pub messages: Arc<Mutex<HashMap<i64, Vec<MessageInfo>>>>,
|
||||
pub folders: Arc<Mutex<Vec<FolderInfo>>>,
|
||||
pub user_names: Arc<Mutex<HashMap<i64, String>>>,
|
||||
pub profiles: Arc<Mutex<HashMap<i64, ProfileInfo>>>,
|
||||
pub drafts: Arc<Mutex<HashMap<i64, String>>>,
|
||||
pub available_reactions: Arc<Mutex<Vec<String>>>,
|
||||
|
||||
pub network_state: Arc<Mutex<NetworkState>>,
|
||||
pub typing_chat_id: Arc<Mutex<Option<i64>>>,
|
||||
pub current_chat_id: Arc<Mutex<Option<i64>>>,
|
||||
pub current_pinned_message: Arc<Mutex<Option<MessageInfo>>>,
|
||||
pub auth_state: Arc<Mutex<AuthState>>,
|
||||
|
||||
pub sent_messages: Arc<Mutex<Vec<SentMessage>>>,
|
||||
pub edited_messages: Arc<Mutex<Vec<EditedMessage>>>,
|
||||
pub deleted_messages: Arc<Mutex<Vec<DeletedMessages>>>,
|
||||
pub forwarded_messages: Arc<Mutex<Vec<ForwardedMessages>>>,
|
||||
pub searched_queries: Arc<Mutex<Vec<SearchQuery>>>,
|
||||
pub viewed_messages: Arc<Mutex<ViewedMessages>>,
|
||||
pub chat_actions: Arc<Mutex<Vec<(i64, String)>>>,
|
||||
pub pending_view_messages: Arc<Mutex<PendingViewMessages>>,
|
||||
|
||||
pub update_tx: Arc<Mutex<Option<mpsc::UnboundedSender<TdUpdate>>>>,
|
||||
pub downloaded_files: Arc<Mutex<HashMap<i32, String>>>,
|
||||
|
||||
pub simulate_delays: bool,
|
||||
pub fail_next_operation: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct SentMessage {
|
||||
pub chat_id: i64,
|
||||
pub text: String,
|
||||
pub reply_to: Option<MessageId>,
|
||||
pub reply_info: Option<ReplyInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct EditedMessage {
|
||||
pub chat_id: i64,
|
||||
pub message_id: MessageId,
|
||||
pub new_text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct DeletedMessages {
|
||||
pub chat_id: i64,
|
||||
pub message_ids: Vec<MessageId>,
|
||||
pub revoke: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ForwardedMessages {
|
||||
pub from_chat_id: i64,
|
||||
pub to_chat_id: i64,
|
||||
pub message_ids: Vec<MessageId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct SearchQuery {
|
||||
pub chat_id: i64,
|
||||
pub query: String,
|
||||
pub results_count: usize,
|
||||
}
|
||||
|
||||
impl Default for FakeTdClient {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for FakeTdClient {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
chats: Arc::clone(&self.chats),
|
||||
messages: Arc::clone(&self.messages),
|
||||
folders: Arc::clone(&self.folders),
|
||||
user_names: Arc::clone(&self.user_names),
|
||||
profiles: Arc::clone(&self.profiles),
|
||||
drafts: Arc::clone(&self.drafts),
|
||||
available_reactions: Arc::clone(&self.available_reactions),
|
||||
network_state: Arc::clone(&self.network_state),
|
||||
typing_chat_id: Arc::clone(&self.typing_chat_id),
|
||||
current_chat_id: Arc::clone(&self.current_chat_id),
|
||||
current_pinned_message: Arc::clone(&self.current_pinned_message),
|
||||
auth_state: Arc::clone(&self.auth_state),
|
||||
sent_messages: Arc::clone(&self.sent_messages),
|
||||
edited_messages: Arc::clone(&self.edited_messages),
|
||||
deleted_messages: Arc::clone(&self.deleted_messages),
|
||||
forwarded_messages: Arc::clone(&self.forwarded_messages),
|
||||
searched_queries: Arc::clone(&self.searched_queries),
|
||||
viewed_messages: Arc::clone(&self.viewed_messages),
|
||||
chat_actions: Arc::clone(&self.chat_actions),
|
||||
pending_view_messages: Arc::clone(&self.pending_view_messages),
|
||||
downloaded_files: Arc::clone(&self.downloaded_files),
|
||||
update_tx: Arc::clone(&self.update_tx),
|
||||
simulate_delays: self.simulate_delays,
|
||||
fail_next_operation: Arc::clone(&self.fail_next_operation),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl FakeTdClient {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
chats: Arc::new(Mutex::new(vec![])),
|
||||
messages: Arc::new(Mutex::new(HashMap::new())),
|
||||
folders: Arc::new(Mutex::new(vec![FolderInfo { id: 0, name: "All".to_string() }])),
|
||||
user_names: Arc::new(Mutex::new(HashMap::new())),
|
||||
profiles: Arc::new(Mutex::new(HashMap::new())),
|
||||
drafts: Arc::new(Mutex::new(HashMap::new())),
|
||||
available_reactions: Arc::new(Mutex::new(vec![
|
||||
"👍".to_string(),
|
||||
"❤️".to_string(),
|
||||
"😂".to_string(),
|
||||
"😮".to_string(),
|
||||
"😢".to_string(),
|
||||
"🙏".to_string(),
|
||||
"👏".to_string(),
|
||||
"🔥".to_string(),
|
||||
])),
|
||||
network_state: Arc::new(Mutex::new(NetworkState::Ready)),
|
||||
typing_chat_id: Arc::new(Mutex::new(None)),
|
||||
current_chat_id: Arc::new(Mutex::new(None)),
|
||||
current_pinned_message: Arc::new(Mutex::new(None)),
|
||||
auth_state: Arc::new(Mutex::new(AuthState::Ready)),
|
||||
sent_messages: Arc::new(Mutex::new(vec![])),
|
||||
edited_messages: Arc::new(Mutex::new(vec![])),
|
||||
deleted_messages: Arc::new(Mutex::new(vec![])),
|
||||
forwarded_messages: Arc::new(Mutex::new(vec![])),
|
||||
searched_queries: Arc::new(Mutex::new(vec![])),
|
||||
viewed_messages: Arc::new(Mutex::new(vec![])),
|
||||
chat_actions: Arc::new(Mutex::new(vec![])),
|
||||
pending_view_messages: Arc::new(Mutex::new(vec![])),
|
||||
downloaded_files: Arc::new(Mutex::new(HashMap::new())),
|
||||
update_tx: Arc::new(Mutex::new(None)),
|
||||
simulate_delays: false,
|
||||
fail_next_operation: Arc::new(Mutex::new(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
358
crates/tele-tui/tests/helpers/fake_tdclient_impl.rs
Normal file
358
crates/tele-tui/tests/helpers/fake_tdclient_impl.rs
Normal file
@@ -0,0 +1,358 @@
|
||||
//! Test implementation of the TDLib client traits for FakeTdClient.
|
||||
|
||||
use super::fake_tdclient::FakeTdClient;
|
||||
use async_trait::async_trait;
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use tdlib_rs::enums::{ChatAction, Update};
|
||||
use tele_tui::tdlib::{
|
||||
AccountClient, AuthClient, ChatActionClient, ChatClient, ClientState, FileClient,
|
||||
MessageClient, ReactionClient, UpdateClient, UserClient,
|
||||
};
|
||||
use tele_tui::tdlib::{
|
||||
AuthState, ChatInfo, FolderInfo, MessageInfo, ProfileInfo, ReplyInfo, UserCache,
|
||||
UserOnlineStatus,
|
||||
};
|
||||
use tele_tui::types::{ChatId, MessageId, UserId};
|
||||
|
||||
#[async_trait]
|
||||
impl AuthClient for FakeTdClient {
|
||||
async fn send_phone_number(&self, _phone: String) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_code(&self, _code: String) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_password(&self, _password: String) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ChatClient for FakeTdClient {
|
||||
async fn load_chats(&mut self, limit: i32) -> Result<(), String> {
|
||||
let _ = FakeTdClient::load_chats(self, limit as usize).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_folder_chats(&mut self, folder_id: i32, limit: i32) -> Result<(), String> {
|
||||
FakeTdClient::load_folder_chats(self, folder_id, limit as usize).await
|
||||
}
|
||||
|
||||
async fn leave_chat(&self, _chat_id: ChatId) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo, String> {
|
||||
FakeTdClient::get_profile_info(self, chat_id).await
|
||||
}
|
||||
|
||||
fn chats(&self) -> &[ChatInfo] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn folders(&self) -> &[FolderInfo] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn main_chat_list_position(&self) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
fn set_main_chat_list_position(&mut self, _position: i32) {}
|
||||
|
||||
fn update_chats<F>(&mut self, updater: F)
|
||||
where
|
||||
F: FnOnce(&mut Vec<ChatInfo>),
|
||||
{
|
||||
updater(&mut self.chats.lock().unwrap());
|
||||
}
|
||||
|
||||
fn update_folders<F>(&mut self, updater: F)
|
||||
where
|
||||
F: FnOnce(&mut Vec<FolderInfo>),
|
||||
{
|
||||
updater(&mut self.folders.lock().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ChatActionClient for FakeTdClient {
|
||||
async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) {
|
||||
FakeTdClient::send_chat_action(self, chat_id, format!("{:?}", action)).await;
|
||||
}
|
||||
|
||||
fn clear_stale_typing_status(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn typing_status(&self) -> Option<&(UserId, String, std::time::Instant)> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_typing_status(&mut self, _status: Option<(UserId, String, std::time::Instant)>) {}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MessageClient for FakeTdClient {
|
||||
async fn get_chat_history(
|
||||
&mut self,
|
||||
chat_id: ChatId,
|
||||
limit: i32,
|
||||
) -> Result<Vec<MessageInfo>, String> {
|
||||
FakeTdClient::get_chat_history(self, chat_id, limit).await
|
||||
}
|
||||
|
||||
async fn load_older_messages(
|
||||
&mut self,
|
||||
chat_id: ChatId,
|
||||
from_message_id: MessageId,
|
||||
) -> Result<Vec<MessageInfo>, String> {
|
||||
FakeTdClient::load_older_messages(self, chat_id, from_message_id).await
|
||||
}
|
||||
|
||||
async fn get_pinned_messages(&mut self, _chat_id: ChatId) -> Result<Vec<MessageInfo>, String> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
async fn load_current_pinned_message(&mut self, _chat_id: ChatId) {}
|
||||
|
||||
async fn search_messages(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
query: &str,
|
||||
) -> Result<Vec<MessageInfo>, String> {
|
||||
FakeTdClient::search_messages(self, chat_id, query).await
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
&mut self,
|
||||
chat_id: ChatId,
|
||||
text: String,
|
||||
reply_to_message_id: Option<MessageId>,
|
||||
reply_info: Option<ReplyInfo>,
|
||||
) -> Result<MessageInfo, String> {
|
||||
FakeTdClient::send_message(self, chat_id, text, reply_to_message_id, reply_info).await
|
||||
}
|
||||
|
||||
async fn edit_message(
|
||||
&mut self,
|
||||
chat_id: ChatId,
|
||||
message_id: MessageId,
|
||||
new_text: String,
|
||||
) -> Result<MessageInfo, String> {
|
||||
FakeTdClient::edit_message(self, chat_id, message_id, new_text).await
|
||||
}
|
||||
|
||||
async fn delete_messages(
|
||||
&mut self,
|
||||
chat_id: ChatId,
|
||||
message_ids: Vec<MessageId>,
|
||||
revoke: bool,
|
||||
) -> Result<(), String> {
|
||||
FakeTdClient::delete_messages(self, chat_id, message_ids, revoke).await
|
||||
}
|
||||
|
||||
async fn forward_messages(
|
||||
&mut self,
|
||||
to_chat_id: ChatId,
|
||||
from_chat_id: ChatId,
|
||||
message_ids: Vec<MessageId>,
|
||||
) -> Result<(), String> {
|
||||
FakeTdClient::forward_messages(self, from_chat_id, to_chat_id, message_ids).await
|
||||
}
|
||||
|
||||
async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> {
|
||||
FakeTdClient::set_draft_message(self, chat_id, text).await
|
||||
}
|
||||
|
||||
fn current_chat_messages(&self) -> Cow<'_, [MessageInfo]> {
|
||||
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
|
||||
Cow::Owned(self.get_messages(chat_id))
|
||||
} else {
|
||||
Cow::Owned(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn current_chat_id(&self) -> Option<ChatId> {
|
||||
self.get_current_chat_id().map(ChatId::new)
|
||||
}
|
||||
|
||||
fn current_pinned_message(&self) -> Option<MessageInfo> {
|
||||
self.current_pinned_message.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
fn push_message(&mut self, msg: MessageInfo) {
|
||||
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
|
||||
self.messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(chat_id)
|
||||
.or_default()
|
||||
.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_current_chat_messages(&mut self) {
|
||||
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
|
||||
self.messages.lock().unwrap().remove(&chat_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_current_chat_messages(&mut self, messages: Vec<MessageInfo>) {
|
||||
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
|
||||
self.messages.lock().unwrap().insert(chat_id, messages);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_current_chat_messages<F>(&mut self, updater: F)
|
||||
where
|
||||
F: FnOnce(&mut Vec<MessageInfo>),
|
||||
{
|
||||
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
|
||||
let mut all_messages = self.messages.lock().unwrap();
|
||||
updater(all_messages.entry(chat_id).or_default());
|
||||
}
|
||||
}
|
||||
|
||||
fn set_current_chat_id(&mut self, chat_id: Option<ChatId>) {
|
||||
*self.current_chat_id.lock().unwrap() = chat_id.map(|id| id.as_i64());
|
||||
}
|
||||
|
||||
fn set_current_pinned_message(&mut self, msg: Option<MessageInfo>) {
|
||||
*self.current_pinned_message.lock().unwrap() = msg;
|
||||
}
|
||||
|
||||
fn pending_view_messages(&self) -> &[(ChatId, Vec<MessageId>)] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn enqueue_pending_view_messages(&mut self, chat_id: ChatId, message_ids: Vec<MessageId>) {
|
||||
self.pending_view_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((chat_id, message_ids));
|
||||
}
|
||||
|
||||
async fn fetch_missing_reply_info(&mut self) {}
|
||||
|
||||
async fn process_pending_view_messages(&mut self) {
|
||||
let mut pending = self.pending_view_messages.lock().unwrap();
|
||||
for (chat_id, message_ids) in pending.drain(..) {
|
||||
let ids: Vec<i64> = message_ids.iter().map(|id| id.as_i64()).collect();
|
||||
self.viewed_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((chat_id.as_i64(), ids));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserClient for FakeTdClient {
|
||||
fn get_user_status_by_chat_id(&self, _chat_id: ChatId) -> Option<&UserOnlineStatus> {
|
||||
None
|
||||
}
|
||||
|
||||
fn pending_user_ids(&self) -> &[UserId] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn user_cache(&self) -> &UserCache {
|
||||
use std::sync::OnceLock;
|
||||
static EMPTY_CACHE: OnceLock<UserCache> = OnceLock::new();
|
||||
EMPTY_CACHE.get_or_init(|| UserCache::new(0))
|
||||
}
|
||||
|
||||
fn update_user_cache<F>(&mut self, _updater: F)
|
||||
where
|
||||
F: FnOnce(&mut UserCache),
|
||||
{
|
||||
}
|
||||
|
||||
async fn process_pending_user_ids(&mut self) {}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ReactionClient for FakeTdClient {
|
||||
async fn get_message_available_reactions(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
message_id: MessageId,
|
||||
) -> Result<Vec<String>, String> {
|
||||
FakeTdClient::get_message_available_reactions(self, chat_id, message_id).await
|
||||
}
|
||||
|
||||
async fn toggle_reaction(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
message_id: MessageId,
|
||||
reaction: String,
|
||||
) -> Result<(), String> {
|
||||
FakeTdClient::toggle_reaction(self, chat_id, message_id, reaction).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FileClient for FakeTdClient {
|
||||
async fn download_file(&self, file_id: i32) -> Result<String, String> {
|
||||
FakeTdClient::download_file(self, file_id).await
|
||||
}
|
||||
|
||||
async fn download_voice_note(&self, file_id: i32) -> Result<String, String> {
|
||||
Ok(format!("/tmp/fake_voice_{}.ogg", file_id))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ClientState for FakeTdClient {
|
||||
fn client_id(&self) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
async fn get_me(&self) -> Result<i64, String> {
|
||||
Ok(12345)
|
||||
}
|
||||
|
||||
fn auth_state(&self) -> &AuthState {
|
||||
use std::sync::OnceLock;
|
||||
static AUTH_STATE_READY: AuthState = AuthState::Ready;
|
||||
static AUTH_STATE_WAIT_PHONE: OnceLock<AuthState> = OnceLock::new();
|
||||
static AUTH_STATE_WAIT_CODE: OnceLock<AuthState> = OnceLock::new();
|
||||
static AUTH_STATE_WAIT_PASSWORD: OnceLock<AuthState> = OnceLock::new();
|
||||
|
||||
let current = self.auth_state.lock().unwrap();
|
||||
match *current {
|
||||
AuthState::Ready => &AUTH_STATE_READY,
|
||||
AuthState::WaitPhoneNumber => {
|
||||
AUTH_STATE_WAIT_PHONE.get_or_init(|| AuthState::WaitPhoneNumber)
|
||||
}
|
||||
AuthState::WaitCode => AUTH_STATE_WAIT_CODE.get_or_init(|| AuthState::WaitCode),
|
||||
AuthState::WaitPassword => {
|
||||
AUTH_STATE_WAIT_PASSWORD.get_or_init(|| AuthState::WaitPassword)
|
||||
}
|
||||
_ => &AUTH_STATE_READY,
|
||||
}
|
||||
}
|
||||
|
||||
fn network_state(&self) -> tele_tui::tdlib::types::NetworkState {
|
||||
FakeTdClient::get_network_state(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AccountClient for FakeTdClient {
|
||||
async fn recreate_client(&mut self, _db_path: PathBuf) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateClient for FakeTdClient {
|
||||
fn handle_update(&mut self, _update: Update) {}
|
||||
|
||||
fn drain_incoming_message_events(&mut self) -> Vec<tele_tui::tdlib::IncomingMessageEvent> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
22
crates/tele-tui/tests/helpers/mod.rs
Normal file
22
crates/tele-tui/tests/helpers/mod.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
// Test helpers module.
|
||||
//
|
||||
// In all-features runs, integration tests exercise the same gated support module
|
||||
// used by the PTY fixture binary. Plain `cargo test` keeps the local copies so
|
||||
// existing tests do not need the internal feature enabled.
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub use tele_tui::test_support::*;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub mod app_builder;
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub mod fake_tdclient;
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
mod fake_tdclient_impl; // TdClientTrait implementation for FakeTdClient
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub mod snapshot_utils;
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub mod test_data;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub use fake_tdclient::FakeTdClient;
|
||||
144
crates/tele-tui/tests/helpers/snapshot_utils.rs
Normal file
144
crates/tele-tui/tests/helpers/snapshot_utils.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
// Snapshot testing utilities
|
||||
|
||||
use ratatui::backend::TestBackend;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
use ratatui::Terminal;
|
||||
|
||||
/// Конвертирует Buffer в читаемую строку для snapshot тестов
|
||||
pub fn buffer_to_string(buffer: &Buffer) -> String {
|
||||
let area = buffer.area();
|
||||
let mut result = String::new();
|
||||
|
||||
for y in 0..area.height {
|
||||
let mut line = String::new();
|
||||
for x in 0..area.width {
|
||||
line.push_str(buffer[(x, y)].symbol());
|
||||
}
|
||||
// Убираем trailing spaces в конце строки
|
||||
result.push_str(line.trim_end());
|
||||
if y < area.height - 1 {
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Serializes only cells with non-default style, grouped by row and style.
|
||||
pub fn buffer_to_style_snapshot(buffer: &Buffer) -> String {
|
||||
let area = buffer.area();
|
||||
let mut rows = Vec::new();
|
||||
|
||||
for y in 0..area.height {
|
||||
let mut segments = Vec::new();
|
||||
let mut x = 0;
|
||||
|
||||
while x < area.width {
|
||||
let cell = &buffer[(x, y)];
|
||||
if is_default_style(cell) {
|
||||
x += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = x;
|
||||
let fg = cell.fg;
|
||||
let bg = cell.bg;
|
||||
let modifier = cell.modifier;
|
||||
let mut text = String::new();
|
||||
|
||||
while x < area.width {
|
||||
let next = &buffer[(x, y)];
|
||||
if is_default_style(next)
|
||||
|| next.fg != fg
|
||||
|| next.bg != bg
|
||||
|| next.modifier != modifier
|
||||
{
|
||||
break;
|
||||
}
|
||||
text.push_str(next.symbol());
|
||||
x += 1;
|
||||
}
|
||||
|
||||
segments.push(format!(
|
||||
"{}..{} {:?}/{:?}/{:?}: {:?}",
|
||||
start,
|
||||
x.saturating_sub(1),
|
||||
fg,
|
||||
bg,
|
||||
modifier,
|
||||
text.trim_end()
|
||||
));
|
||||
}
|
||||
|
||||
if !segments.is_empty() {
|
||||
rows.push(format!("y={}: {}", y, segments.join(" | ")));
|
||||
}
|
||||
}
|
||||
|
||||
rows.join("\n")
|
||||
}
|
||||
|
||||
fn is_default_style(cell: &ratatui::buffer::Cell) -> bool {
|
||||
cell.fg == Color::Reset && cell.bg == Color::Reset && cell.modifier == Modifier::empty()
|
||||
}
|
||||
|
||||
/// Создаёт TestBackend с заданным размером и рендерит UI
|
||||
pub fn render_to_buffer<F>(width: u16, height: u16, render_fn: F) -> Buffer
|
||||
where
|
||||
F: FnOnce(&mut ratatui::Frame),
|
||||
{
|
||||
let backend = TestBackend::new(width, height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.draw(render_fn).unwrap();
|
||||
|
||||
terminal.backend().buffer().clone()
|
||||
}
|
||||
|
||||
/// Макрос для упрощения snapshot тестов
|
||||
#[macro_export]
|
||||
macro_rules! assert_ui_snapshot {
|
||||
($name:expr, $width:expr, $height:expr, $render_fn:expr) => {{
|
||||
use $crate::helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
|
||||
let buffer = render_to_buffer($width, $height, $render_fn);
|
||||
let output = buffer_to_string(&buffer);
|
||||
insta::assert_snapshot!($name, output);
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::{Block, Borders};
|
||||
|
||||
#[test]
|
||||
fn test_buffer_to_string_simple() {
|
||||
let buffer = render_to_buffer(10, 3, |f| {
|
||||
let block = Block::default().borders(Borders::ALL).title("Hi");
|
||||
f.render_widget(block, f.area());
|
||||
});
|
||||
|
||||
let result = buffer_to_string(&buffer);
|
||||
assert!(result.contains("Hi"));
|
||||
assert!(result.contains("┌"));
|
||||
assert!(result.contains("└"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_to_string_removes_trailing_spaces() {
|
||||
let buffer = render_to_buffer(20, 3, |f| {
|
||||
let block = Block::default().title("Test");
|
||||
f.render_widget(block, Rect::new(0, 0, 10, 3));
|
||||
});
|
||||
|
||||
let result = buffer_to_string(&buffer);
|
||||
let lines: Vec<&str> = result.lines().collect();
|
||||
|
||||
// Проверяем что trailing spaces убраны
|
||||
for line in lines {
|
||||
assert!(!line.ends_with(' ') || line.trim().is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
252
crates/tele-tui/tests/helpers/test_data.rs
Normal file
252
crates/tele-tui/tests/helpers/test_data.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
// Test data builders and fixtures
|
||||
|
||||
use tele_tui::tdlib::types::{ForwardInfo, ReactionInfo};
|
||||
use tele_tui::tdlib::{ChatInfo, MessageInfo, ProfileInfo, ReplyInfo};
|
||||
use tele_tui::types::{ChatId, MessageId};
|
||||
|
||||
/// Builder для создания тестового чата
|
||||
#[allow(dead_code)]
|
||||
pub struct TestChatBuilder {
|
||||
id: i64,
|
||||
title: String,
|
||||
username: Option<String>,
|
||||
last_message: String,
|
||||
last_message_date: i32,
|
||||
unread_count: i32,
|
||||
unread_mention_count: i32,
|
||||
is_pinned: bool,
|
||||
order: i64,
|
||||
last_read_outbox_message_id: i64,
|
||||
folder_ids: Vec<i32>,
|
||||
is_muted: bool,
|
||||
draft_text: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TestChatBuilder {
|
||||
pub fn new(title: &str, id: i64) -> Self {
|
||||
Self {
|
||||
id,
|
||||
title: title.to_string(),
|
||||
username: None,
|
||||
last_message: "".to_string(),
|
||||
last_message_date: 1640000000,
|
||||
unread_count: 0,
|
||||
unread_mention_count: 0,
|
||||
is_pinned: false,
|
||||
order: id,
|
||||
last_read_outbox_message_id: 0,
|
||||
folder_ids: vec![0],
|
||||
is_muted: false,
|
||||
draft_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn username(mut self, username: &str) -> Self {
|
||||
self.username = Some(username.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn last_message(mut self, text: &str) -> Self {
|
||||
self.last_message = text.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unread_count(mut self, count: i32) -> Self {
|
||||
self.unread_count = count;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unread_mentions(mut self, count: i32) -> Self {
|
||||
self.unread_mention_count = count;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pinned(mut self) -> Self {
|
||||
self.is_pinned = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn muted(mut self) -> Self {
|
||||
self.is_muted = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn draft(mut self, text: &str) -> Self {
|
||||
self.draft_text = Some(text.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn folder(mut self, folder_id: i32) -> Self {
|
||||
self.folder_ids = vec![folder_id];
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> ChatInfo {
|
||||
ChatInfo {
|
||||
id: ChatId::new(self.id),
|
||||
title: self.title,
|
||||
username: self.username,
|
||||
last_message: self.last_message,
|
||||
last_message_date: self.last_message_date,
|
||||
unread_count: self.unread_count,
|
||||
unread_mention_count: self.unread_mention_count,
|
||||
is_pinned: self.is_pinned,
|
||||
order: self.order,
|
||||
last_read_outbox_message_id: MessageId::new(self.last_read_outbox_message_id),
|
||||
folder_ids: self.folder_ids,
|
||||
is_muted: self.is_muted,
|
||||
draft_text: self.draft_text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder для создания тестового сообщения
|
||||
#[allow(dead_code)]
|
||||
pub struct TestMessageBuilder {
|
||||
id: i64,
|
||||
sender_name: String,
|
||||
is_outgoing: bool,
|
||||
content: String,
|
||||
entities: Vec<tdlib_rs::types::TextEntity>,
|
||||
date: i32,
|
||||
edit_date: i32,
|
||||
is_read: bool,
|
||||
can_be_edited: bool,
|
||||
can_be_deleted_only_for_self: bool,
|
||||
can_be_deleted_for_all_users: bool,
|
||||
reply_to: Option<ReplyInfo>,
|
||||
forward_from: Option<ForwardInfo>,
|
||||
reactions: Vec<ReactionInfo>,
|
||||
media_album_id: i64,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TestMessageBuilder {
|
||||
pub fn new(content: &str, id: i64) -> Self {
|
||||
Self {
|
||||
id,
|
||||
sender_name: "User".to_string(),
|
||||
is_outgoing: false,
|
||||
content: content.to_string(),
|
||||
entities: vec![],
|
||||
date: 1640000000,
|
||||
edit_date: 0,
|
||||
is_read: true,
|
||||
can_be_edited: false,
|
||||
can_be_deleted_only_for_self: true,
|
||||
can_be_deleted_for_all_users: false,
|
||||
reply_to: None,
|
||||
forward_from: None,
|
||||
reactions: vec![],
|
||||
media_album_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn outgoing(mut self) -> Self {
|
||||
self.is_outgoing = true;
|
||||
self.sender_name = "You".to_string();
|
||||
self.can_be_edited = true;
|
||||
self.can_be_deleted_for_all_users = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn sender(mut self, name: &str) -> Self {
|
||||
self.sender_name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn date(mut self, timestamp: i32) -> Self {
|
||||
self.date = timestamp;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn edited(mut self) -> Self {
|
||||
self.edit_date = self.date + 60;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unread(mut self) -> Self {
|
||||
self.is_read = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn reply_to(mut self, message_id: i64, sender: &str, text: &str) -> Self {
|
||||
self.reply_to = Some(ReplyInfo {
|
||||
message_id: MessageId::new(message_id),
|
||||
sender_name: sender.to_string(),
|
||||
text: text.to_string(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn forwarded_from(mut self, sender: &str) -> Self {
|
||||
self.forward_from = Some(ForwardInfo { sender_name: sender.to_string() });
|
||||
self
|
||||
}
|
||||
|
||||
pub fn reaction(mut self, emoji: &str, count: i32, chosen: bool) -> Self {
|
||||
self.reactions
|
||||
.push(ReactionInfo { emoji: emoji.to_string(), count, is_chosen: chosen });
|
||||
self
|
||||
}
|
||||
|
||||
pub fn media_album_id(mut self, id: i64) -> Self {
|
||||
self.media_album_id = id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> MessageInfo {
|
||||
let mut msg = MessageInfo::new(
|
||||
MessageId::new(self.id),
|
||||
self.sender_name,
|
||||
self.is_outgoing,
|
||||
self.content,
|
||||
self.entities,
|
||||
self.date,
|
||||
self.edit_date,
|
||||
self.is_read,
|
||||
self.can_be_edited,
|
||||
self.can_be_deleted_only_for_self,
|
||||
self.can_be_deleted_for_all_users,
|
||||
self.reply_to,
|
||||
self.forward_from,
|
||||
self.reactions,
|
||||
);
|
||||
msg.metadata.media_album_id = self.media_album_id;
|
||||
msg
|
||||
}
|
||||
}
|
||||
|
||||
/// Хелперы для быстрого создания тестовых данных
|
||||
pub fn create_test_chat(title: &str, id: i64) -> ChatInfo {
|
||||
TestChatBuilder::new(title, id).build()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn create_test_message(content: &str, id: i64) -> MessageInfo {
|
||||
TestMessageBuilder::new(content, id).build()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn create_test_user(name: &str, id: i64) -> (i64, String) {
|
||||
(id, name.to_string())
|
||||
}
|
||||
|
||||
/// Хелпер для создания профиля
|
||||
#[allow(dead_code)]
|
||||
pub fn create_test_profile(title: &str, chat_id: i64) -> ProfileInfo {
|
||||
ProfileInfo {
|
||||
chat_id: ChatId::new(chat_id),
|
||||
title: title.to_string(),
|
||||
username: None,
|
||||
bio: None,
|
||||
phone_number: None,
|
||||
chat_type: "Личный чат".to_string(),
|
||||
member_count: None,
|
||||
description: None,
|
||||
invite_link: None,
|
||||
is_group: false,
|
||||
online_status: None,
|
||||
}
|
||||
}
|
||||
154
crates/tele-tui/tests/input_field.rs
Normal file
154
crates/tele-tui/tests/input_field.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
// Input Field UI snapshot tests
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::app_builder::TestAppBuilder;
|
||||
use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
|
||||
use helpers::test_data::{create_test_chat, TestMessageBuilder};
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn snapshot_empty_input() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("empty_input", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_input_with_text() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.selected_chat(123)
|
||||
.insert_mode()
|
||||
.message_input("Hello, how are you?")
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("input_with_text", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_input_long_text_2_lines() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
// Text that wraps to 2 lines
|
||||
let long_text = "This is a longer message that will wrap to multiple lines in the input field for testing purposes.";
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.selected_chat(123)
|
||||
.insert_mode()
|
||||
.message_input(long_text)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("input_long_text_2_lines", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_input_long_text_max_lines() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
// Very long text that reaches maximum 10 lines
|
||||
let very_long_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.";
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.selected_chat(123)
|
||||
.insert_mode()
|
||||
.message_input(very_long_text)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("input_long_text_max_lines", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_input_editing_mode() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("Original message text", 1)
|
||||
.outgoing()
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.insert_mode()
|
||||
.editing_message(1, 0)
|
||||
.message_input("Edited text here")
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("input_editing_mode", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_input_reply_mode() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let original_msg = TestMessageBuilder::new("What do you think about this?", 1)
|
||||
.sender("Mom")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, original_msg)
|
||||
.selected_chat(123)
|
||||
.insert_mode()
|
||||
.replying_to(1)
|
||||
.message_input("I think it's great!")
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("input_reply_mode", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_input_search_mode() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.selected_chat(123)
|
||||
.searching("hello")
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("input_search_mode", output);
|
||||
}
|
||||
451
crates/tele-tui/tests/input_navigation.rs
Normal file
451
crates/tele-tui/tests/input_navigation.rs
Normal 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));
|
||||
}
|
||||
504
crates/tele-tui/tests/messages.rs
Normal file
504
crates/tele-tui/tests/messages.rs
Normal file
@@ -0,0 +1,504 @@
|
||||
// Messages UI snapshot tests
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::app_builder::TestAppBuilder;
|
||||
use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
|
||||
use helpers::test_data::{create_test_chat, TestChatBuilder, TestMessageBuilder};
|
||||
use insta::assert_snapshot;
|
||||
use tele_tui::types::{ChatId, MessageId};
|
||||
|
||||
#[test]
|
||||
fn snapshot_empty_chat() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("empty_chat", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_single_incoming_message() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("Hello there!", 1)
|
||||
.sender("Mom")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("single_incoming_message", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_single_outgoing_message() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("Hi mom!", 1).outgoing().build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("single_outgoing_message", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_date_separator_old_date() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
// Use a fixed old date (20 Dec 2021) - will show as date separator
|
||||
let message = TestMessageBuilder::new("Message from the past", 1)
|
||||
.date(1640000000) // 20 Dec 2021
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("date_separator_old_date", output);
|
||||
}
|
||||
|
||||
// NOTE: Tests for "Сегодня" and "Вчера" date separators are skipped
|
||||
// because they depend on current date and cannot be stable snapshots.
|
||||
// The date formatting logic is tested manually and through the old_date test above.
|
||||
|
||||
#[test]
|
||||
fn snapshot_sender_grouping() {
|
||||
let chat = create_test_chat("Group Chat", 123);
|
||||
let msg1 = TestMessageBuilder::new("First message", 1)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
let msg2 = TestMessageBuilder::new("Second message", 2)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
let msg3 = TestMessageBuilder::new("Third message", 3)
|
||||
.sender("Bob")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(123, vec![msg1, msg2, msg3])
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("sender_grouping", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_outgoing_sent() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("Just sent", 1).outgoing().build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("outgoing_sent", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_outgoing_read() {
|
||||
let chat = TestChatBuilder::new("Mom", 123)
|
||||
.last_message("Read message")
|
||||
.build();
|
||||
|
||||
// Message with id < last_read_outbox_message_id means it's been read
|
||||
let message = TestMessageBuilder::new("Read message", 1)
|
||||
.outgoing()
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
// Set last_read_outbox to simulate message being read
|
||||
if let Some(chat) = app.chats.iter_mut().find(|c| c.id == ChatId::new(123)) {
|
||||
chat.last_read_outbox_message_id = MessageId::new(2);
|
||||
}
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("outgoing_read", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_edited_message() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("Edited text", 1).edited().build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("edited_message", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_long_message_wrap() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let long_text = "This is a very long message that should wrap across multiple lines when rendered in the terminal UI. Let's make it even longer to ensure we test the wrapping behavior properly.";
|
||||
let message = TestMessageBuilder::new(long_text, 1).build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("long_message_wrap", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_markdown_bold_italic_code() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("**bold** *italic* `code`", 1).build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("markdown_bold_italic_code", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_markdown_link_mention() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message =
|
||||
TestMessageBuilder::new("Check [this](https://example.com) and @username", 1).build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("markdown_link_mention", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_markdown_spoiler() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("Spoiler: ||hidden text||", 1).build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("markdown_spoiler", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_media_placeholder() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("[Фото]", 1).build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("media_placeholder", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_reply_message() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("This is a reply", 2)
|
||||
.reply_to(1, "Mom", "Original message text")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("reply_message", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_forwarded_message() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("Forwarded content", 1)
|
||||
.forwarded_from("Alice")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("forwarded_message", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_single_reaction() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("Great!", 1)
|
||||
.reaction("👍", 1, true)
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("single_reaction", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_multiple_reactions() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("Popular message", 1)
|
||||
.reaction("👍", 5, true)
|
||||
.reaction("👎", 3, false)
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("multiple_reactions", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_selected_message() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("Selected message", 1).build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.selecting_message(1)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("selected_message", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_album_incoming() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let msg1 = TestMessageBuilder::new("📷 [Фото]", 1)
|
||||
.sender("Alice")
|
||||
.media_album_id(12345)
|
||||
.build();
|
||||
let msg2 = TestMessageBuilder::new("Caption for album", 2)
|
||||
.sender("Alice")
|
||||
.media_album_id(12345)
|
||||
.build();
|
||||
let msg3 = TestMessageBuilder::new("📷 [Фото]", 3)
|
||||
.sender("Alice")
|
||||
.media_album_id(12345)
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(123, vec![msg1, msg2, msg3])
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("album_incoming", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_album_outgoing() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let msg1 = TestMessageBuilder::new("📷 [Фото]", 1)
|
||||
.outgoing()
|
||||
.media_album_id(99999)
|
||||
.build();
|
||||
let msg2 = TestMessageBuilder::new("My vacation photos", 2)
|
||||
.outgoing()
|
||||
.media_album_id(99999)
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(123, vec![msg1, msg2])
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("album_outgoing", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_album_with_regular_messages() {
|
||||
let chat = create_test_chat("Group Chat", 123);
|
||||
let msg1 = TestMessageBuilder::new("Regular message before", 1)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
let msg2 = TestMessageBuilder::new("📷 [Фото]", 2)
|
||||
.sender("Alice")
|
||||
.media_album_id(555)
|
||||
.build();
|
||||
let msg3 = TestMessageBuilder::new("Album caption", 3)
|
||||
.sender("Alice")
|
||||
.media_album_id(555)
|
||||
.build();
|
||||
let msg4 = TestMessageBuilder::new("Regular message after", 4)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(123, vec![msg1, msg2, msg3, msg4])
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("album_with_regular_messages", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_album_selected() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let msg1 = TestMessageBuilder::new("📷 [Фото]", 1)
|
||||
.sender("Alice")
|
||||
.media_album_id(777)
|
||||
.build();
|
||||
let msg2 = TestMessageBuilder::new("📷 [Фото]", 2)
|
||||
.sender("Alice")
|
||||
.media_album_id(777)
|
||||
.build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(123, vec![msg1, msg2])
|
||||
.selected_chat(123)
|
||||
.selecting_message(1) // Выбираем одно из сообщений альбома
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("album_selected", output);
|
||||
}
|
||||
217
crates/tele-tui/tests/modals.rs
Normal file
217
crates/tele-tui/tests/modals.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
// Modals UI snapshot tests
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::app_builder::TestAppBuilder;
|
||||
use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
|
||||
use helpers::test_data::{
|
||||
create_test_chat, create_test_profile, TestChatBuilder, TestMessageBuilder,
|
||||
};
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn snapshot_delete_confirmation_modal() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("Delete me", 1).outgoing().build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.delete_confirmation(1)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("delete_confirmation_modal", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_emoji_picker_default() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("React to this", 1).build();
|
||||
|
||||
let reactions = vec![
|
||||
"👍".to_string(),
|
||||
"👎".to_string(),
|
||||
"❤️".to_string(),
|
||||
"🔥".to_string(),
|
||||
"😊".to_string(),
|
||||
"😢".to_string(),
|
||||
"😮".to_string(),
|
||||
"🎉".to_string(),
|
||||
];
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.reaction_picker(1, reactions)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("emoji_picker_default", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_emoji_picker_with_selection() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message = TestMessageBuilder::new("React to this", 1).build();
|
||||
|
||||
let reactions = vec![
|
||||
"👍".to_string(),
|
||||
"👎".to_string(),
|
||||
"❤️".to_string(),
|
||||
"🔥".to_string(),
|
||||
"😊".to_string(),
|
||||
"😢".to_string(),
|
||||
"😮".to_string(),
|
||||
"🎉".to_string(),
|
||||
];
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.reaction_picker(1, reactions)
|
||||
.build();
|
||||
|
||||
// Выбираем 5-ю реакцию (индекс 4)
|
||||
if let tele_tui::app::ChatState::ReactionPicker { selected_index, .. } = &mut app.chat_state {
|
||||
*selected_index = 4;
|
||||
}
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("emoji_picker_with_selection", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_profile_personal_chat() {
|
||||
let chat = create_test_chat("Alice", 123);
|
||||
let profile = create_test_profile("Alice", 123);
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.selected_chat(123)
|
||||
.profile_mode(profile)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("profile_personal_chat", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_profile_group_chat() {
|
||||
let chat = TestChatBuilder::new("Work Group", 456).build();
|
||||
|
||||
let mut profile = create_test_profile("Work Group", 456);
|
||||
profile.is_group = true;
|
||||
profile.chat_type = "Группа".to_string();
|
||||
profile.member_count = Some(25);
|
||||
profile.description = Some("Work discussion group".to_string());
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.selected_chat(456)
|
||||
.profile_mode(profile)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("profile_group_chat", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_pinned_message() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let message1 = TestMessageBuilder::new("Regular message", 1).build();
|
||||
let pinned_msg = TestMessageBuilder::new("Important pinned message!", 2).build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(123, vec![message1])
|
||||
.selected_chat(123)
|
||||
.build();
|
||||
|
||||
// Устанавливаем закреплённое сообщение
|
||||
app.td_client.set_current_pinned_message(Some(pinned_msg));
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("pinned_message", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_search_in_chat() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let msg1 = TestMessageBuilder::new("Hello world", 1).build();
|
||||
let msg2 = TestMessageBuilder::new("World is beautiful", 2).build();
|
||||
let msg3 = TestMessageBuilder::new("Beautiful day", 3).build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chat(chat)
|
||||
.with_messages(123, vec![msg1.clone(), msg2.clone(), msg3])
|
||||
.selected_chat(123)
|
||||
.message_search("world")
|
||||
.build();
|
||||
|
||||
// Устанавливаем результаты поиска
|
||||
if let tele_tui::app::ChatState::SearchInChat { results, selected_index, .. } =
|
||||
&mut app.chat_state
|
||||
{
|
||||
*results = vec![msg1, msg2];
|
||||
*selected_index = 0;
|
||||
}
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::messages::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("search_in_chat", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_forward_mode() {
|
||||
let chat1 = create_test_chat("Mom", 123);
|
||||
let chat2 = create_test_chat("Dad", 456);
|
||||
let chat3 = create_test_chat("Work Group", 789);
|
||||
|
||||
let message = TestMessageBuilder::new("Forward this message", 1).build();
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_chats(vec![chat1.clone(), chat2, chat3])
|
||||
.with_message(123, message)
|
||||
.selected_chat(123)
|
||||
.forward_mode(1)
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
// В forward mode показывается chat_list для выбора чата
|
||||
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("forward_mode", output);
|
||||
}
|
||||
228
crates/tele-tui/tests/navigation.rs
Normal file
228
crates/tele-tui/tests/navigation.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
// Integration tests for navigation flow
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::fake_tdclient::FakeTdClient;
|
||||
use helpers::test_data::{create_test_chat, TestMessageBuilder};
|
||||
|
||||
/// Test: Навигация вверх/вниз по списку чатов
|
||||
#[tokio::test]
|
||||
async fn test_navigate_chat_list_up_down() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let chat1 = create_test_chat("Mom", 123);
|
||||
let chat2 = create_test_chat("Boss", 456);
|
||||
let chat3 = create_test_chat("Friend", 789);
|
||||
|
||||
let client = client.with_chats(vec![chat1, chat2, chat3]);
|
||||
|
||||
let chats = client.get_chats();
|
||||
|
||||
// Начинаем с индекса 0
|
||||
let mut selected_index = 0;
|
||||
assert_eq!(chats[selected_index].title, "Mom");
|
||||
|
||||
// ↓ - вниз
|
||||
selected_index = (selected_index + 1).min(chats.len() - 1);
|
||||
assert_eq!(selected_index, 1);
|
||||
assert_eq!(chats[selected_index].title, "Boss");
|
||||
|
||||
// ↓ - ещё вниз
|
||||
selected_index = (selected_index + 1).min(chats.len() - 1);
|
||||
assert_eq!(selected_index, 2);
|
||||
assert_eq!(chats[selected_index].title, "Friend");
|
||||
|
||||
// ↓ - на границе (не должно выйти за пределы)
|
||||
selected_index = (selected_index + 1).min(chats.len() - 1);
|
||||
assert_eq!(selected_index, 2); // Остался на последнем
|
||||
|
||||
// ↑ - вверх
|
||||
selected_index = selected_index.saturating_sub(1);
|
||||
assert_eq!(selected_index, 1);
|
||||
assert_eq!(chats[selected_index].title, "Boss");
|
||||
|
||||
// ↑ - ещё вверх
|
||||
selected_index = selected_index.saturating_sub(1);
|
||||
assert_eq!(selected_index, 0);
|
||||
assert_eq!(chats[selected_index].title, "Mom");
|
||||
|
||||
// ↑ - на границе (не должно выйти за пределы)
|
||||
selected_index = selected_index.saturating_sub(1);
|
||||
assert_eq!(selected_index, 0); // Остался на первом
|
||||
}
|
||||
|
||||
/// Test: Enter открывает чат
|
||||
#[tokio::test]
|
||||
async fn test_enter_opens_chat() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let _client = client.with_chat(chat);
|
||||
|
||||
// Состояние: список чатов, выбран чат 123
|
||||
let selected_chat_id: Option<i64> = None;
|
||||
|
||||
// Пользователь нажал Enter
|
||||
let new_selected_chat_id = Some(123);
|
||||
|
||||
assert_eq!(selected_chat_id, None);
|
||||
assert_eq!(new_selected_chat_id, Some(123));
|
||||
}
|
||||
|
||||
/// Test: Esc закрывает чат
|
||||
#[tokio::test]
|
||||
async fn test_esc_closes_chat() {
|
||||
// Состояние: открыт чат 123
|
||||
let _selected_chat_id = Some(123);
|
||||
|
||||
// Пользователь нажал Esc
|
||||
let selected_chat_id: Option<i64> = None;
|
||||
|
||||
assert_eq!(selected_chat_id, None);
|
||||
}
|
||||
|
||||
/// Test: Скролл сообщений в чате
|
||||
#[tokio::test]
|
||||
async fn test_scroll_messages_in_chat() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let messages = vec![
|
||||
TestMessageBuilder::new("Msg 1", 1).build(),
|
||||
TestMessageBuilder::new("Msg 2", 2).build(),
|
||||
TestMessageBuilder::new("Msg 3", 3).build(),
|
||||
TestMessageBuilder::new("Msg 4", 4).build(),
|
||||
TestMessageBuilder::new("Msg 5", 5).build(),
|
||||
];
|
||||
|
||||
let client = client.with_messages(123, messages);
|
||||
|
||||
let _msgs = client.get_messages(123);
|
||||
|
||||
// Скролл начинается снизу (последнее сообщение видно)
|
||||
let mut scroll_offset: usize = 0;
|
||||
|
||||
// ↑ - скролл вверх (увеличиваем offset)
|
||||
scroll_offset += 1;
|
||||
assert_eq!(scroll_offset, 1);
|
||||
|
||||
// ↑ - ещё вверх
|
||||
scroll_offset += 1;
|
||||
assert_eq!(scroll_offset, 2);
|
||||
|
||||
// ↓ - скролл вниз (уменьшаем offset)
|
||||
scroll_offset = scroll_offset.saturating_sub(1);
|
||||
assert_eq!(scroll_offset, 1);
|
||||
|
||||
// ↓ - к низу
|
||||
scroll_offset = scroll_offset.saturating_sub(1);
|
||||
assert_eq!(scroll_offset, 0);
|
||||
|
||||
// ↓ - на границе
|
||||
scroll_offset = scroll_offset.saturating_sub(1);
|
||||
assert_eq!(scroll_offset, 0); // Не уходим в минус
|
||||
}
|
||||
|
||||
/// Test: Переключение между папками (1-9)
|
||||
#[tokio::test]
|
||||
async fn test_switch_folders() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Добавляем папки (FakeTdClient уже создаёт "All" с id=0)
|
||||
let client = client.with_folder(1, "Personal").with_folder(2, "Work");
|
||||
|
||||
let folders = client.get_folders();
|
||||
|
||||
// Проверяем что папки на месте
|
||||
assert_eq!(folders.len(), 3);
|
||||
assert_eq!(folders[0].name, "All");
|
||||
assert_eq!(folders[1].name, "Personal");
|
||||
assert_eq!(folders[2].name, "Work");
|
||||
|
||||
// Начинаем с папки 0 (All)
|
||||
let mut selected_folder_index = 0;
|
||||
assert_eq!(folders[selected_folder_index].name, "All");
|
||||
|
||||
// Нажали '1' - папка Personal (индекс 1)
|
||||
selected_folder_index = 1;
|
||||
assert_eq!(folders[selected_folder_index].name, "Personal");
|
||||
|
||||
// Нажали '2' - папка Work (индекс 2)
|
||||
selected_folder_index = 2;
|
||||
assert_eq!(folders[selected_folder_index].name, "Work");
|
||||
|
||||
// Нажали '0' (или Esc из папки) - обратно в All (индекс 0)
|
||||
selected_folder_index = 0;
|
||||
assert_eq!(folders[selected_folder_index].name, "All");
|
||||
}
|
||||
|
||||
/// Test: Русская раскладка для навигации (р/о/л/д)
|
||||
#[tokio::test]
|
||||
async fn test_russian_layout_navigation() {
|
||||
// В реальном App: к/j/h/l маппятся на р/о/л/д для русской раскладки
|
||||
|
||||
// Mapping:
|
||||
// j (down) <-> о
|
||||
// k (up) <-> л
|
||||
// h (left) <-> р
|
||||
// l (right) <-> д
|
||||
|
||||
let mut selected_index = 1;
|
||||
|
||||
// Симулируем нажатие 'о' (как 'j' - вниз)
|
||||
selected_index += 1;
|
||||
assert_eq!(selected_index, 2);
|
||||
|
||||
// 'л' (как 'k' - вверх)
|
||||
selected_index -= 1;
|
||||
assert_eq!(selected_index, 1);
|
||||
|
||||
// Реальный end-to-end тест этого mapping живет в input handler.
|
||||
}
|
||||
|
||||
/// Test: Подгрузка старых сообщений при скролле вверх
|
||||
#[tokio::test]
|
||||
async fn test_load_older_messages_on_scroll_up() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Начальные сообщения (последние 10)
|
||||
let initial_messages = vec![
|
||||
TestMessageBuilder::new("Msg 91", 91).build(),
|
||||
TestMessageBuilder::new("Msg 92", 92).build(),
|
||||
TestMessageBuilder::new("Msg 93", 93).build(),
|
||||
TestMessageBuilder::new("Msg 94", 94).build(),
|
||||
TestMessageBuilder::new("Msg 95", 95).build(),
|
||||
TestMessageBuilder::new("Msg 96", 96).build(),
|
||||
TestMessageBuilder::new("Msg 97", 97).build(),
|
||||
TestMessageBuilder::new("Msg 98", 98).build(),
|
||||
TestMessageBuilder::new("Msg 99", 99).build(),
|
||||
TestMessageBuilder::new("Msg 100", 100).build(),
|
||||
];
|
||||
|
||||
let client = client.with_messages(123, initial_messages);
|
||||
|
||||
assert_eq!(client.get_messages(123).len(), 10);
|
||||
|
||||
// Пользователь скроллит до самого верха (дошёл до Msg 91)
|
||||
// Триггерим подгрузку старых сообщений
|
||||
|
||||
// Симулируем подгрузку (добавляем старые сообщения в начало)
|
||||
let older_messages = vec![
|
||||
TestMessageBuilder::new("Msg 81", 81).build(),
|
||||
TestMessageBuilder::new("Msg 82", 82).build(),
|
||||
TestMessageBuilder::new("Msg 83", 83).build(),
|
||||
TestMessageBuilder::new("Msg 84", 84).build(),
|
||||
TestMessageBuilder::new("Msg 85", 85).build(),
|
||||
];
|
||||
|
||||
// Добавляем к существующим (в реальности - prepend)
|
||||
let mut all_messages = older_messages;
|
||||
all_messages.extend(client.get_messages(123));
|
||||
|
||||
let client = client.with_messages(123, all_messages);
|
||||
|
||||
// Теперь должно быть 15 сообщений
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 15);
|
||||
assert_eq!(messages[0].content.text, "Msg 81");
|
||||
assert_eq!(messages[14].content.text, "Msg 100");
|
||||
}
|
||||
176
crates/tele-tui/tests/network_typing.rs
Normal file
176
crates/tele-tui/tests/network_typing.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
// Integration tests for network and typing flow
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::fake_tdclient::FakeTdClient;
|
||||
use helpers::test_data::create_test_chat;
|
||||
use tele_tui::tdlib::NetworkState;
|
||||
use tele_tui::types::ChatId;
|
||||
|
||||
/// Test: Смена состояния сети отображается в UI
|
||||
#[tokio::test]
|
||||
async fn test_network_state_changes() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Начальное состояние - Ready
|
||||
assert_eq!(client.get_network_state(), NetworkState::Ready);
|
||||
|
||||
// Сеть пропала
|
||||
client.simulate_network_change(NetworkState::WaitingForNetwork);
|
||||
assert_eq!(client.get_network_state(), NetworkState::WaitingForNetwork);
|
||||
// В UI: "⚠ Нет сети"
|
||||
|
||||
// Подключаемся к прокси
|
||||
client.simulate_network_change(NetworkState::ConnectingToProxy);
|
||||
assert_eq!(client.get_network_state(), NetworkState::ConnectingToProxy);
|
||||
// В UI: "⏳ Прокси..."
|
||||
|
||||
// Подключаемся к серверам
|
||||
client.simulate_network_change(NetworkState::Connecting);
|
||||
assert_eq!(client.get_network_state(), NetworkState::Connecting);
|
||||
// В UI: "⏳ Подключение..."
|
||||
|
||||
// Соединение восстановлено
|
||||
client.simulate_network_change(NetworkState::Ready);
|
||||
assert_eq!(client.get_network_state(), NetworkState::Ready);
|
||||
// В UI: индикатор скрывается
|
||||
}
|
||||
|
||||
/// Test: WaitingForNetwork - нет подключения
|
||||
#[tokio::test]
|
||||
async fn test_network_waiting_for_network() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
client.simulate_network_change(NetworkState::WaitingForNetwork);
|
||||
|
||||
assert_eq!(client.get_network_state(), NetworkState::WaitingForNetwork);
|
||||
|
||||
// В этом состоянии:
|
||||
// - Показывается предупреждение "⚠ Нет сети"
|
||||
// - Отправка сообщений заблокирована
|
||||
// - Updates не приходят
|
||||
}
|
||||
|
||||
/// Test: ConnectingToProxy - подключение через прокси
|
||||
#[tokio::test]
|
||||
async fn test_network_connecting_to_proxy() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
client.simulate_network_change(NetworkState::ConnectingToProxy);
|
||||
|
||||
assert_eq!(client.get_network_state(), NetworkState::ConnectingToProxy);
|
||||
|
||||
// В UI: "⏳ Прокси..."
|
||||
}
|
||||
|
||||
/// Test: Connecting - подключение к серверам Telegram
|
||||
#[tokio::test]
|
||||
async fn test_network_connecting() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
client.simulate_network_change(NetworkState::Connecting);
|
||||
|
||||
assert_eq!(client.get_network_state(), NetworkState::Connecting);
|
||||
|
||||
// В UI: "⏳ Подключение..."
|
||||
}
|
||||
|
||||
/// Test: Updating - обновление данных
|
||||
#[tokio::test]
|
||||
async fn test_network_updating() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
client.simulate_network_change(NetworkState::Updating);
|
||||
|
||||
assert_eq!(client.get_network_state(), NetworkState::Updating);
|
||||
|
||||
// В UI: "⏳ Обновление..."
|
||||
}
|
||||
|
||||
/// Test: Typing indicator - пользователь печатает
|
||||
#[tokio::test]
|
||||
async fn test_typing_indicator_on() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let chat = create_test_chat("Alice", 123);
|
||||
let client = client.with_chat(chat);
|
||||
|
||||
// Alice начала печатать в чате 123
|
||||
// Симулируем через send_chat_action
|
||||
client
|
||||
.send_chat_action(ChatId::new(123), "Typing".to_string())
|
||||
.await;
|
||||
|
||||
assert_eq!(*client.typing_chat_id.lock().unwrap(), Some(123));
|
||||
|
||||
// В UI: под сообщениями отображается "Alice печатает..."
|
||||
}
|
||||
|
||||
/// Test: Typing indicator - пользователь перестал печатать
|
||||
#[tokio::test]
|
||||
async fn test_typing_indicator_off() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Изначально Alice печатала
|
||||
client
|
||||
.send_chat_action(ChatId::new(123), "Typing".to_string())
|
||||
.await;
|
||||
assert_eq!(*client.typing_chat_id.lock().unwrap(), Some(123));
|
||||
|
||||
// Alice перестала печатать
|
||||
client
|
||||
.send_chat_action(ChatId::new(123), "Cancel".to_string())
|
||||
.await;
|
||||
|
||||
assert_eq!(*client.typing_chat_id.lock().unwrap(), None);
|
||||
|
||||
// В UI: индикатор "печатает..." исчезает
|
||||
}
|
||||
|
||||
/// Test: Отправка своего typing status
|
||||
#[tokio::test]
|
||||
async fn test_send_own_typing_status() {
|
||||
let _client = FakeTdClient::new();
|
||||
|
||||
// Пользователь начал печатать в чате 456
|
||||
// В реальном App вызывается client.send_chat_action(chat_id, ChatAction::Typing)
|
||||
|
||||
// Симулируем: устанавливаем что мы печатаем
|
||||
let our_typing_chat_id = Some(456);
|
||||
|
||||
assert_eq!(our_typing_chat_id, Some(456));
|
||||
|
||||
// Собеседник видит что мы печатаем
|
||||
|
||||
// Через некоторое время (или при отправке сообщения) - отменяем
|
||||
// client.send_chat_action(chat_id, ChatAction::Cancel)
|
||||
let our_typing_chat_id: Option<i64> = None;
|
||||
|
||||
assert_eq!(our_typing_chat_id, None);
|
||||
}
|
||||
|
||||
/// Test: Множественные переходы состояний сети
|
||||
#[tokio::test]
|
||||
async fn test_multiple_network_state_transitions() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Цикл переходов состояний
|
||||
let states = vec![
|
||||
NetworkState::Ready,
|
||||
NetworkState::Connecting,
|
||||
NetworkState::Ready,
|
||||
NetworkState::WaitingForNetwork,
|
||||
NetworkState::ConnectingToProxy,
|
||||
NetworkState::Connecting,
|
||||
NetworkState::Updating,
|
||||
NetworkState::Ready,
|
||||
];
|
||||
|
||||
for state in states {
|
||||
client.simulate_network_change(state.clone());
|
||||
assert_eq!(client.get_network_state(), state);
|
||||
}
|
||||
|
||||
// Финальное состояние - Ready
|
||||
assert_eq!(client.get_network_state(), NetworkState::Ready);
|
||||
}
|
||||
134
crates/tele-tui/tests/profile.rs
Normal file
134
crates/tele-tui/tests/profile.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
// Integration tests for profile flow
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::fake_tdclient::FakeTdClient;
|
||||
use helpers::test_data::create_test_chat;
|
||||
use tele_tui::tdlib::ProfileInfo;
|
||||
use tele_tui::types::ChatId;
|
||||
|
||||
/// Test: Открытие профиля в личном чате (i)
|
||||
#[tokio::test]
|
||||
async fn test_open_profile_in_private_chat() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let chat = create_test_chat("Alice", 123);
|
||||
let _client = client.with_chat(chat);
|
||||
|
||||
// Пользователь открыл чат и нажал 'i'
|
||||
let profile_mode = true;
|
||||
|
||||
assert!(profile_mode);
|
||||
|
||||
// В реальном App загрузится ProfileInfo для этого чата
|
||||
}
|
||||
|
||||
/// Test: Профиль показывает имя, username, телефон
|
||||
#[tokio::test]
|
||||
async fn test_profile_shows_user_info() {
|
||||
let profile = ProfileInfo {
|
||||
chat_id: ChatId::new(123),
|
||||
title: "Alice Johnson".to_string(),
|
||||
username: Some("alice".to_string()),
|
||||
phone_number: Some("+1234567890".to_string()),
|
||||
bio: None,
|
||||
chat_type: "Личный чат".to_string(),
|
||||
member_count: None,
|
||||
description: None,
|
||||
invite_link: None,
|
||||
is_group: false,
|
||||
online_status: Some("Online".to_string()),
|
||||
};
|
||||
|
||||
assert_eq!(profile.title, "Alice Johnson");
|
||||
assert_eq!(profile.username.as_ref().unwrap(), "alice");
|
||||
assert_eq!(profile.phone_number.as_ref().unwrap(), "+1234567890");
|
||||
assert_eq!(profile.chat_type, "Личный чат");
|
||||
}
|
||||
|
||||
/// Test: Профиль в группе показывает количество участников
|
||||
#[tokio::test]
|
||||
async fn test_profile_shows_group_member_count() {
|
||||
let profile = ProfileInfo {
|
||||
chat_id: ChatId::new(456),
|
||||
title: "Work Team".to_string(),
|
||||
username: None,
|
||||
phone_number: None,
|
||||
bio: Some("Our work group".to_string()),
|
||||
chat_type: "Группа".to_string(),
|
||||
member_count: Some(25),
|
||||
description: None,
|
||||
invite_link: None,
|
||||
is_group: true,
|
||||
online_status: None,
|
||||
};
|
||||
|
||||
assert_eq!(profile.title, "Work Team");
|
||||
assert_eq!(profile.chat_type, "Группа");
|
||||
assert_eq!(profile.member_count, Some(25));
|
||||
assert_eq!(profile.bio.as_ref().unwrap(), "Our work group");
|
||||
}
|
||||
|
||||
/// Test: Профиль в канале
|
||||
#[tokio::test]
|
||||
async fn test_profile_shows_channel_info() {
|
||||
let profile = ProfileInfo {
|
||||
chat_id: ChatId::new(789),
|
||||
title: "News Channel".to_string(),
|
||||
username: Some("news_channel".to_string()),
|
||||
phone_number: None,
|
||||
bio: Some("Latest news updates".to_string()),
|
||||
chat_type: "Канал".to_string(),
|
||||
member_count: Some(1000),
|
||||
description: Some("Latest news updates".to_string()),
|
||||
invite_link: Some("t.me/news_channel".to_string()),
|
||||
is_group: false,
|
||||
online_status: None,
|
||||
};
|
||||
|
||||
assert_eq!(profile.title, "News Channel");
|
||||
assert_eq!(profile.username.as_ref().unwrap(), "news_channel");
|
||||
assert_eq!(profile.chat_type, "Канал");
|
||||
assert_eq!(profile.member_count, Some(1000)); // Subscribers
|
||||
}
|
||||
|
||||
/// Test: Закрытие профиля (Esc)
|
||||
#[tokio::test]
|
||||
async fn test_close_profile_with_esc() {
|
||||
// Профиль открыт
|
||||
let _profile_mode = true;
|
||||
|
||||
// Пользователь нажал Esc
|
||||
let profile_mode = false;
|
||||
|
||||
assert!(!profile_mode);
|
||||
}
|
||||
|
||||
/// Test: Профиль без username и phone
|
||||
#[tokio::test]
|
||||
async fn test_profile_without_optional_fields() {
|
||||
let profile = ProfileInfo {
|
||||
chat_id: ChatId::new(999),
|
||||
title: "Anonymous User".to_string(),
|
||||
username: None,
|
||||
phone_number: None,
|
||||
bio: None,
|
||||
chat_type: "Личный чат".to_string(),
|
||||
member_count: None,
|
||||
description: None,
|
||||
invite_link: None,
|
||||
is_group: false,
|
||||
online_status: None,
|
||||
};
|
||||
|
||||
// Обязательные поля заполнены
|
||||
assert_eq!(profile.title, "Anonymous User");
|
||||
assert_eq!(profile.chat_type, "Личный чат");
|
||||
|
||||
// Опциональные поля None
|
||||
assert_eq!(profile.username, None);
|
||||
assert_eq!(profile.phone_number, None);
|
||||
assert_eq!(profile.bio, None);
|
||||
|
||||
// В UI будут отображаться только доступные поля
|
||||
}
|
||||
299
crates/tele-tui/tests/reactions.rs
Normal file
299
crates/tele-tui/tests/reactions.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
// Integration tests for reactions flow
|
||||
|
||||
mod helpers;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use helpers::app_builder::TestAppBuilder;
|
||||
use helpers::fake_tdclient::FakeTdClient;
|
||||
use helpers::test_data::TestMessageBuilder;
|
||||
use tele_tui::config::Command;
|
||||
use tele_tui::input::handlers::chat::handle_message_selection;
|
||||
use tele_tui::types::ChatId;
|
||||
|
||||
/// Test: Добавление реакции к сообщению
|
||||
#[tokio::test]
|
||||
async fn test_add_reaction_to_message() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Отправляем сообщение
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "React to this!".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Добавляем реакцию
|
||||
client
|
||||
.toggle_reaction(ChatId::new(123), msg.id(), "👍".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что реакция записалась
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0].reactions().len(), 1);
|
||||
assert_eq!(messages[0].reactions()[0].emoji, "👍");
|
||||
assert_eq!(messages[0].reactions()[0].count, 1);
|
||||
assert!(messages[0].reactions()[0].is_chosen);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_react_without_selected_chat_does_not_panic() {
|
||||
let msg = TestMessageBuilder::new("React safely", 100).build();
|
||||
let mut app = TestAppBuilder::new()
|
||||
.with_message(123, msg)
|
||||
.selecting_message(0)
|
||||
.build();
|
||||
*app.td_client.current_chat_id.lock().unwrap() = Some(123);
|
||||
|
||||
handle_message_selection(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
||||
Some(Command::ReactMessage),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(app.error_message.as_deref(), Some("Чат не выбран"));
|
||||
}
|
||||
|
||||
/// Test: Удаление реакции (toggle) - вторичное нажатие
|
||||
#[tokio::test]
|
||||
async fn test_toggle_reaction_removes_it() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Создаём сообщение с нашей реакцией
|
||||
let msg = TestMessageBuilder::new("Message", 100)
|
||||
.reaction("👍", 1, true) // chosen=true - наша реакция
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, msg);
|
||||
|
||||
// Проверяем что реакция есть
|
||||
let messages_before = client.get_messages(123);
|
||||
assert_eq!(messages_before[0].reactions().len(), 1);
|
||||
assert!(messages_before[0].reactions()[0].is_chosen);
|
||||
|
||||
let msg_id = messages_before[0].id();
|
||||
|
||||
// Toggle - удаляем свою реакцию
|
||||
client
|
||||
.toggle_reaction(ChatId::new(123), msg_id, "👍".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let messages_after = client.get_messages(123);
|
||||
assert_eq!(messages_after[0].reactions().len(), 0);
|
||||
}
|
||||
|
||||
/// Test: Множественные реакции на одно сообщение
|
||||
#[tokio::test]
|
||||
async fn test_multiple_reactions_on_one_message() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "Many reactions".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Добавляем несколько разных реакций
|
||||
client
|
||||
.toggle_reaction(ChatId::new(123), msg.id(), "👍".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.toggle_reaction(ChatId::new(123), msg.id(), "❤️".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.toggle_reaction(ChatId::new(123), msg.id(), "😂".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.toggle_reaction(ChatId::new(123), msg.id(), "🔥".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что все 4 реакции записались
|
||||
let messages = client.get_messages(123);
|
||||
let reactions = &messages[0].reactions();
|
||||
assert_eq!(reactions.len(), 4);
|
||||
assert_eq!(reactions[0].emoji, "👍");
|
||||
assert_eq!(reactions[1].emoji, "❤️");
|
||||
assert_eq!(reactions[2].emoji, "😂");
|
||||
assert_eq!(reactions[3].emoji, "🔥");
|
||||
}
|
||||
|
||||
/// Test: Реакции от разных пользователей (count > 1)
|
||||
#[tokio::test]
|
||||
async fn test_reactions_from_multiple_users() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Создаём сообщение с реакцией от 3 пользователей
|
||||
let msg = TestMessageBuilder::new("Popular message", 100)
|
||||
.reaction("👍", 3, false) // 3 человека, но не мы
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, msg);
|
||||
|
||||
let messages = client.get_messages(123);
|
||||
let reaction = &messages[0].reactions()[0];
|
||||
|
||||
assert_eq!(reaction.emoji, "👍");
|
||||
assert_eq!(reaction.count, 3);
|
||||
assert!(!reaction.is_chosen);
|
||||
}
|
||||
|
||||
/// Test: Своя реакция (is_chosen = true)
|
||||
#[tokio::test]
|
||||
async fn test_own_reaction_is_chosen() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Создаём сообщение с нашей реакцией
|
||||
let msg = TestMessageBuilder::new("I reacted", 100)
|
||||
.reaction("❤️", 1, true) // chosen=true
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, msg);
|
||||
|
||||
let messages = client.get_messages(123);
|
||||
let reaction = &messages[0].reactions()[0];
|
||||
|
||||
assert!(reaction.is_chosen);
|
||||
// В UI это будет отображаться в рамках: [❤️]
|
||||
}
|
||||
|
||||
/// Test: Чужая реакция (is_chosen = false)
|
||||
#[tokio::test]
|
||||
async fn test_other_reaction_not_chosen() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Создаём сообщение с чужой реакцией
|
||||
let msg = TestMessageBuilder::new("They reacted", 100)
|
||||
.reaction("😂", 2, false) // chosen=false
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, msg);
|
||||
|
||||
let messages = client.get_messages(123);
|
||||
let reaction = &messages[0].reactions()[0];
|
||||
|
||||
assert!(!reaction.is_chosen);
|
||||
// В UI это будет отображаться без рамок: 😂 2
|
||||
}
|
||||
|
||||
/// Test: Счётчик реакций увеличивается
|
||||
#[tokio::test]
|
||||
async fn test_reaction_counter_increases() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Начальное сообщение с 1 реакцией от кого-то
|
||||
let msg = TestMessageBuilder::new("Growing", 100)
|
||||
.reaction("👍", 1, false)
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, msg);
|
||||
|
||||
let messages_before = client.get_messages(123);
|
||||
assert_eq!(messages_before[0].reactions()[0].count, 1);
|
||||
|
||||
let msg_id = messages_before[0].id();
|
||||
|
||||
// Мы добавляем свою реакцию - счётчик должен увеличиться
|
||||
client
|
||||
.toggle_reaction(ChatId::new(123), msg_id, "👍".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages[0].reactions()[0].count, 2);
|
||||
assert!(messages[0].reactions()[0].is_chosen);
|
||||
}
|
||||
|
||||
/// Test: Обновление реакции - мы добавили свою к существующим
|
||||
#[tokio::test]
|
||||
async fn test_update_reaction_we_add_ours() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Изначально: 2 человека, но не мы
|
||||
let msg_before = TestMessageBuilder::new("Update", 100)
|
||||
.reaction("🔥", 2, false)
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, msg_before);
|
||||
|
||||
let messages_before = client.get_messages(123);
|
||||
assert_eq!(messages_before[0].reactions()[0].count, 2);
|
||||
assert!(!messages_before[0].reactions()[0].is_chosen);
|
||||
|
||||
let msg_id = messages_before[0].id();
|
||||
|
||||
// Добавляем нашу реакцию
|
||||
client
|
||||
.toggle_reaction(ChatId::new(123), msg_id, "🔥".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let messages = client.get_messages(123);
|
||||
let reaction = &messages[0].reactions()[0];
|
||||
|
||||
assert_eq!(reaction.count, 3);
|
||||
assert!(reaction.is_chosen);
|
||||
}
|
||||
|
||||
/// Test: Реакция с count=1 отображается только emoji
|
||||
#[tokio::test]
|
||||
async fn test_single_reaction_shows_only_emoji() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg = TestMessageBuilder::new("Single", 100)
|
||||
.reaction("❤️", 1, true)
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, msg);
|
||||
|
||||
let messages = client.get_messages(123);
|
||||
let reaction = &messages[0].reactions()[0];
|
||||
|
||||
assert_eq!(reaction.count, 1);
|
||||
// В UI: если count=1, показываем только emoji без цифры
|
||||
// Логика рендеринга: count > 1 ? "emoji count" : "emoji"
|
||||
}
|
||||
|
||||
/// Test: Реакции на несколько сообщений
|
||||
#[tokio::test]
|
||||
async fn test_reactions_on_multiple_messages() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg1 = TestMessageBuilder::new("First", 100)
|
||||
.reaction("👍", 2, false)
|
||||
.build();
|
||||
|
||||
let msg2 = TestMessageBuilder::new("Second", 101)
|
||||
.reaction("❤️", 1, true)
|
||||
.build();
|
||||
|
||||
let msg3 = TestMessageBuilder::new("Third", 102)
|
||||
.reaction("😂", 5, false)
|
||||
.reaction("🔥", 3, true) // Две разные реакции
|
||||
.build();
|
||||
|
||||
let client = client
|
||||
.with_message(123, msg1)
|
||||
.with_message(123, msg2)
|
||||
.with_message(123, msg3);
|
||||
|
||||
let messages = client.get_messages(123);
|
||||
|
||||
// Первое: 1 реакция
|
||||
assert_eq!(messages[0].reactions().len(), 1);
|
||||
assert_eq!(messages[0].reactions()[0].emoji, "👍");
|
||||
|
||||
// Второе: 1 реакция
|
||||
assert_eq!(messages[1].reactions().len(), 1);
|
||||
assert_eq!(messages[1].reactions()[0].emoji, "❤️");
|
||||
|
||||
// Третье: 2 реакции
|
||||
assert_eq!(messages[2].reactions().len(), 2);
|
||||
assert_eq!(messages[2].reactions()[0].emoji, "😂");
|
||||
assert_eq!(messages[2].reactions()[1].emoji, "🔥");
|
||||
assert!(messages[2].reactions()[1].is_chosen);
|
||||
}
|
||||
235
crates/tele-tui/tests/reply_forward.rs
Normal file
235
crates/tele-tui/tests/reply_forward.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
// Integration tests for reply and forward flow
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::fake_tdclient::FakeTdClient;
|
||||
use helpers::test_data::TestMessageBuilder;
|
||||
use tele_tui::tdlib::ReplyInfo;
|
||||
use tele_tui::types::{ChatId, MessageId};
|
||||
|
||||
/// Test: Reply создаёт сообщение с reply_to
|
||||
#[tokio::test]
|
||||
async fn test_reply_creates_message_with_reply_to() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Входящее сообщение от собеседника
|
||||
let original_msg = TestMessageBuilder::new("Question?", 100)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, original_msg);
|
||||
|
||||
// Создаём reply info
|
||||
let reply_info = ReplyInfo {
|
||||
message_id: MessageId::new(100),
|
||||
sender_name: "Alice".to_string(),
|
||||
text: "Question?".to_string(),
|
||||
};
|
||||
|
||||
// Отвечаем на него
|
||||
let reply_msg = client
|
||||
.send_message(
|
||||
ChatId::new(123),
|
||||
"Answer!".to_string(),
|
||||
Some(MessageId::new(100)),
|
||||
Some(reply_info),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что ответ отправлен с reply_to
|
||||
assert_eq!(client.get_sent_messages().len(), 1);
|
||||
assert_eq!(client.get_sent_messages()[0].reply_to, Some(MessageId::new(100)));
|
||||
|
||||
// Проверяем что в списке 2 сообщения
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 2);
|
||||
assert_eq!(messages[1].id(), reply_msg.id());
|
||||
assert_eq!(messages[1].content.text, "Answer!");
|
||||
}
|
||||
|
||||
/// Test: Reply отображает превью оригинального сообщения
|
||||
#[tokio::test]
|
||||
async fn test_reply_shows_original_preview() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Создаём сообщение с reply info
|
||||
let reply_msg = TestMessageBuilder::new("Reply text", 101)
|
||||
.outgoing()
|
||||
.reply_to(100, "Alice", "Original")
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, reply_msg);
|
||||
|
||||
// Проверяем что reply_to сохранено
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert!(messages[0].reply_to().is_some());
|
||||
|
||||
let reply = messages[0].reply_to().unwrap();
|
||||
assert_eq!(reply.message_id, MessageId::new(100));
|
||||
assert_eq!(reply.sender_name, "Alice");
|
||||
assert_eq!(reply.text, "Original");
|
||||
}
|
||||
|
||||
/// Test: Отмена reply mode (Esc) - сообщение отправляется без reply_to
|
||||
#[tokio::test]
|
||||
async fn test_cancel_reply_sends_without_reply_to() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Входящее сообщение
|
||||
let original = TestMessageBuilder::new("Question?", 100)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, original);
|
||||
|
||||
// Пользователь начал reply (r), потом отменил (Esc), затем отправил
|
||||
// Это эмулируется отправкой без reply_to
|
||||
client
|
||||
.send_message(ChatId::new(123), "Regular message".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что отправилось без reply_to
|
||||
assert_eq!(client.get_sent_messages()[0].reply_to, None);
|
||||
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages[1].content.text, "Regular message");
|
||||
}
|
||||
|
||||
/// Test: Forward создаёт сообщение с forward_from
|
||||
#[tokio::test]
|
||||
async fn test_forward_creates_message_with_forward_from() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Создаём пересланное сообщение
|
||||
let forwarded_msg = TestMessageBuilder::new("Forwarded text", 200)
|
||||
.forwarded_from("Bob")
|
||||
.build();
|
||||
|
||||
let client = client.with_message(456, forwarded_msg);
|
||||
|
||||
// Проверяем что forward_from сохранено
|
||||
let messages = client.get_messages(456);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert!(messages[0].forward_from().is_some());
|
||||
|
||||
let forward = messages[0].forward_from().unwrap();
|
||||
assert_eq!(forward.sender_name, "Bob");
|
||||
}
|
||||
|
||||
/// Test: Forward показывает "↪ Переслано от ..."
|
||||
/// Проверяем что у пересланного сообщения есть forward_from
|
||||
#[tokio::test]
|
||||
async fn test_forward_displays_sender_name() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg = TestMessageBuilder::new("Important info", 300)
|
||||
.forwarded_from("Charlie")
|
||||
.build();
|
||||
|
||||
let client = client.with_message(789, msg);
|
||||
|
||||
let messages = client.get_messages(789);
|
||||
let forward = messages[0].forward_from().unwrap();
|
||||
|
||||
// В UI это будет отображаться как "↪ Переслано от Charlie"
|
||||
assert_eq!(forward.sender_name, "Charlie");
|
||||
}
|
||||
|
||||
/// Test: Forward в другой чат
|
||||
#[tokio::test]
|
||||
async fn test_forward_to_different_chat() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Исходное сообщение в чате 123
|
||||
let original = TestMessageBuilder::new("Share this", 100)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, original);
|
||||
|
||||
// Пересылаем в чат 456
|
||||
let forwarded = TestMessageBuilder::new("Share this", 101)
|
||||
.forwarded_from("Alice")
|
||||
.build();
|
||||
|
||||
let client = client.with_message(456, forwarded);
|
||||
|
||||
// Проверяем что в первом чате 1 сообщение
|
||||
assert_eq!(client.get_messages(123).len(), 1);
|
||||
|
||||
// Проверяем что во втором чате тоже 1 сообщение (пересланное)
|
||||
assert_eq!(client.get_messages(456).len(), 1);
|
||||
assert!(client.get_messages(456)[0].forward_from().is_some());
|
||||
}
|
||||
|
||||
/// Test: Reply + Forward комбинация (ответ на пересланное сообщение)
|
||||
#[tokio::test]
|
||||
async fn test_reply_to_forwarded_message() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Пересланное сообщение
|
||||
let forwarded = TestMessageBuilder::new("Forwarded", 100)
|
||||
.forwarded_from("Bob")
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, forwarded);
|
||||
|
||||
// Создаём reply info
|
||||
let reply_info = ReplyInfo {
|
||||
message_id: MessageId::new(100),
|
||||
sender_name: "Bob".to_string(),
|
||||
text: "Forwarded".to_string(),
|
||||
};
|
||||
|
||||
// Отвечаем на пересланное сообщение
|
||||
let reply_msg = client
|
||||
.send_message(
|
||||
ChatId::new(123),
|
||||
"Thanks for sharing!".to_string(),
|
||||
Some(MessageId::new(100)),
|
||||
Some(reply_info),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что reply содержит reply_to
|
||||
assert_eq!(client.get_sent_messages()[0].reply_to, Some(MessageId::new(100)));
|
||||
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 2);
|
||||
assert_eq!(messages[1].id(), reply_msg.id());
|
||||
}
|
||||
|
||||
/// Test: Forward множества сообщений (batch forward)
|
||||
#[tokio::test]
|
||||
async fn test_forward_multiple_messages() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Создаём 3 пересланных сообщения
|
||||
let msg1 = TestMessageBuilder::new("Message 1", 100)
|
||||
.forwarded_from("Alice")
|
||||
.build();
|
||||
|
||||
let msg2 = TestMessageBuilder::new("Message 2", 101)
|
||||
.forwarded_from("Alice")
|
||||
.build();
|
||||
|
||||
let msg3 = TestMessageBuilder::new("Message 3", 102)
|
||||
.forwarded_from("Alice")
|
||||
.build();
|
||||
|
||||
let client = client
|
||||
.with_message(456, msg1)
|
||||
.with_message(456, msg2)
|
||||
.with_message(456, msg3);
|
||||
|
||||
// Проверяем что все 3 сообщения пересланы
|
||||
let messages = client.get_messages(456);
|
||||
assert_eq!(messages.len(), 3);
|
||||
assert!(messages[0].forward_from().is_some());
|
||||
assert!(messages[1].forward_from().is_some());
|
||||
assert!(messages[2].forward_from().is_some());
|
||||
}
|
||||
228
crates/tele-tui/tests/screens.rs
Normal file
228
crates/tele-tui/tests/screens.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
// Screen snapshot tests
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::app_builder::TestAppBuilder;
|
||||
use helpers::snapshot_utils::{buffer_to_string, render_to_buffer};
|
||||
use helpers::test_data::{create_test_chat, TestChatBuilder, TestMessageBuilder};
|
||||
use insta::assert_snapshot;
|
||||
use tele_tui::accounts::AccountProfile;
|
||||
use tele_tui::app::AccountSwitcherState;
|
||||
use tele_tui::app::AppScreen;
|
||||
use tele_tui::tdlib::AuthState;
|
||||
|
||||
#[test]
|
||||
fn snapshot_loading_screen_default() {
|
||||
let mut app = TestAppBuilder::new().screen(AppScreen::Loading).build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::render(f, &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("loading_screen_default", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_loading_screen_with_status() {
|
||||
let mut app = TestAppBuilder::new()
|
||||
.screen(AppScreen::Loading)
|
||||
.status_message("Подключение к Telegram...")
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::render(f, &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("loading_screen_with_status", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_auth_screen_phone() {
|
||||
let mut app = TestAppBuilder::new()
|
||||
.screen(AppScreen::Auth)
|
||||
.auth_state(AuthState::WaitPhoneNumber)
|
||||
.phone_input("+7")
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::render(f, &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("auth_screen_phone", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_auth_screen_code() {
|
||||
let mut app = TestAppBuilder::new()
|
||||
.screen(AppScreen::Auth)
|
||||
.auth_state(AuthState::WaitCode)
|
||||
.code_input("1234")
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::render(f, &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("auth_screen_code", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_auth_screen_password() {
|
||||
let mut app = TestAppBuilder::new()
|
||||
.screen(AppScreen::Auth)
|
||||
.auth_state(AuthState::WaitPassword)
|
||||
.password_input("pass")
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::render(f, &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("auth_screen_password", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_main_screen_empty() {
|
||||
let mut app = TestAppBuilder::new().screen(AppScreen::Main).build();
|
||||
|
||||
let buffer = render_to_buffer(80, 24, |f| {
|
||||
tele_tui::ui::render(f, &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("main_screen_empty", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_main_screen_terminal_too_small() {
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
|
||||
let mut app = TestAppBuilder::new()
|
||||
.screen(AppScreen::Main)
|
||||
.with_chat(chat)
|
||||
.build();
|
||||
|
||||
// Use smaller terminal size (30x8) - below minimum 40x10
|
||||
let buffer = render_to_buffer(30, 8, |f| {
|
||||
tele_tui::ui::render(f, &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("main_screen_terminal_too_small", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_main_screen_chat_list_loaded() {
|
||||
let mut app = TestAppBuilder::new()
|
||||
.screen(AppScreen::Main)
|
||||
.with_chats(sample_chats())
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(100, 30, |f| {
|
||||
tele_tui::ui::render(f, &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("main_screen_chat_list_loaded", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_main_screen_chat_open_with_messages() {
|
||||
let mut app = TestAppBuilder::new()
|
||||
.screen(AppScreen::Main)
|
||||
.with_chats(sample_chats())
|
||||
.selected_chat(102)
|
||||
.with_messages(102, sample_work_messages())
|
||||
.message_input("Draft reply")
|
||||
.insert_mode()
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(100, 30, |f| {
|
||||
tele_tui::ui::render(f, &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("main_screen_chat_open_with_messages", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_main_screen_chat_open_narrow_valid() {
|
||||
let mut app = TestAppBuilder::new()
|
||||
.screen(AppScreen::Main)
|
||||
.with_chats(sample_chats())
|
||||
.selected_chat(102)
|
||||
.with_messages(102, sample_work_messages())
|
||||
.build();
|
||||
|
||||
let buffer = render_to_buffer(60, 16, |f| {
|
||||
tele_tui::ui::render(f, &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("main_screen_chat_open_narrow_valid", output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_main_screen_account_switcher_overlay() {
|
||||
let mut app = TestAppBuilder::new()
|
||||
.screen(AppScreen::Main)
|
||||
.with_chats(sample_chats())
|
||||
.build();
|
||||
app.current_account_name = "personal".to_string();
|
||||
app.account_switcher = Some(AccountSwitcherState::SelectAccount {
|
||||
accounts: vec![
|
||||
AccountProfile {
|
||||
name: "personal".to_string(),
|
||||
display_name: "Personal".to_string(),
|
||||
},
|
||||
AccountProfile {
|
||||
name: "work".to_string(),
|
||||
display_name: "Work".to_string(),
|
||||
},
|
||||
],
|
||||
selected_index: 1,
|
||||
current_account: "personal".to_string(),
|
||||
});
|
||||
|
||||
let buffer = render_to_buffer(100, 30, |f| {
|
||||
tele_tui::ui::render(f, &mut app);
|
||||
});
|
||||
|
||||
let output = buffer_to_string(&buffer);
|
||||
assert_snapshot!("main_screen_account_switcher_overlay", output);
|
||||
}
|
||||
|
||||
fn sample_chats() -> Vec<tele_tui::tdlib::ChatInfo> {
|
||||
vec![
|
||||
TestChatBuilder::new("Mom", 101)
|
||||
.last_message("Dinner at 7?")
|
||||
.unread_count(2)
|
||||
.build(),
|
||||
TestChatBuilder::new("Work Group", 102)
|
||||
.last_message("Standup notes are ready")
|
||||
.unread_mentions(1)
|
||||
.build(),
|
||||
TestChatBuilder::new("Boss", 103)
|
||||
.last_message("Please review the deck")
|
||||
.build(),
|
||||
]
|
||||
}
|
||||
|
||||
fn sample_work_messages() -> Vec<tele_tui::tdlib::MessageInfo> {
|
||||
vec![
|
||||
TestMessageBuilder::new("Morning, team", 201)
|
||||
.sender("Alice")
|
||||
.build(),
|
||||
TestMessageBuilder::new("Standup notes are ready", 202)
|
||||
.sender("Bob")
|
||||
.build(),
|
||||
TestMessageBuilder::new("Thanks, I will review them after lunch", 203)
|
||||
.outgoing()
|
||||
.build(),
|
||||
]
|
||||
}
|
||||
236
crates/tele-tui/tests/search.rs
Normal file
236
crates/tele-tui/tests/search.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
// Integration tests for search flow
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::fake_tdclient::FakeTdClient;
|
||||
use helpers::test_data::{create_test_chat, TestChatBuilder, TestMessageBuilder};
|
||||
|
||||
/// Test: Поиск по чатам фильтрует по названию
|
||||
#[tokio::test]
|
||||
async fn test_search_chats_by_title() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let chat1 = create_test_chat("Mom", 123);
|
||||
let chat2 = create_test_chat("Boss", 456);
|
||||
let chat3 = create_test_chat("Mom's Work", 789);
|
||||
|
||||
let client = client.with_chats(vec![chat1, chat2, chat3]);
|
||||
|
||||
// Ищем "mom" - должно найти "Mom" и "Mom's Work"
|
||||
let query = "mom".to_lowercase();
|
||||
let chats = client.get_chats();
|
||||
let filtered: Vec<_> = chats
|
||||
.iter()
|
||||
.filter(|c| c.title.to_lowercase().contains(&query))
|
||||
.collect();
|
||||
|
||||
assert_eq!(filtered.len(), 2);
|
||||
assert_eq!(filtered[0].title, "Mom");
|
||||
assert_eq!(filtered[1].title, "Mom's Work");
|
||||
}
|
||||
|
||||
/// Test: Поиск по чатам фильтрует по @username
|
||||
#[tokio::test]
|
||||
async fn test_search_chats_by_username() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let chat1 = TestChatBuilder::new("Alice", 123).username("alice").build();
|
||||
|
||||
let chat2 = TestChatBuilder::new("Bob", 456).username("bobby").build();
|
||||
|
||||
let chat3 = TestChatBuilder::new("Charlie", 789).build(); // Без username
|
||||
|
||||
let client = client.with_chats(vec![chat1, chat2, chat3]);
|
||||
|
||||
// Ищем "bob" - должно найти "Bob" (@bobby)
|
||||
let query = "bob".to_lowercase();
|
||||
let chats = client.get_chats();
|
||||
let filtered: Vec<_> = chats
|
||||
.iter()
|
||||
.filter(|c| {
|
||||
c.title.to_lowercase().contains(&query)
|
||||
|| c.username
|
||||
.as_ref()
|
||||
.map(|u| u.to_lowercase().contains(&query))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(filtered.len(), 1);
|
||||
assert_eq!(filtered[0].title, "Bob");
|
||||
}
|
||||
|
||||
/// Test: Пустой поисковый запрос возвращает все чаты
|
||||
#[tokio::test]
|
||||
async fn test_search_empty_query_returns_all() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let chat1 = create_test_chat("Mom", 123);
|
||||
let chat2 = create_test_chat("Boss", 456);
|
||||
let chat3 = create_test_chat("Friend", 789);
|
||||
|
||||
let client = client.with_chats(vec![chat1, chat2, chat3]);
|
||||
|
||||
// Пустой запрос
|
||||
let query = "";
|
||||
let chats = client.get_chats();
|
||||
let filtered: Vec<_> = chats
|
||||
.iter()
|
||||
.filter(|c| c.title.to_lowercase().contains(query))
|
||||
.collect();
|
||||
|
||||
// Все чаты проходят фильтр (пустая строка содержится в любой строке)
|
||||
assert_eq!(filtered.len(), 3);
|
||||
}
|
||||
|
||||
/// Test: Поиск внутри чата по тексту сообщений
|
||||
#[tokio::test]
|
||||
async fn test_search_messages_in_chat() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg1 = TestMessageBuilder::new("Hello world", 100).build();
|
||||
let msg2 = TestMessageBuilder::new("How are you?", 101).build();
|
||||
let msg3 = TestMessageBuilder::new("Hello again", 102).build();
|
||||
|
||||
let client = client.with_messages(123, vec![msg1, msg2, msg3]);
|
||||
|
||||
// Ищем "hello"
|
||||
let query = "hello".to_lowercase();
|
||||
let messages = client.get_messages(123);
|
||||
let found: Vec<_> = messages
|
||||
.iter()
|
||||
.filter(|m| m.text().to_lowercase().contains(&query))
|
||||
.collect();
|
||||
|
||||
assert_eq!(found.len(), 2);
|
||||
assert_eq!(found[0].text(), "Hello world");
|
||||
assert_eq!(found[1].text(), "Hello again");
|
||||
}
|
||||
|
||||
/// Test: Навигация по результатам поиска (n/N)
|
||||
#[tokio::test]
|
||||
async fn test_navigate_search_results() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg1 = TestMessageBuilder::new("First match", 100).build();
|
||||
let msg2 = TestMessageBuilder::new("Second match", 101).build();
|
||||
let msg3 = TestMessageBuilder::new("Third match", 102).build();
|
||||
|
||||
let client = client.with_messages(123, vec![msg1, msg2, msg3]);
|
||||
|
||||
// Ищем "match"
|
||||
let query = "match".to_lowercase();
|
||||
let messages = client.get_messages(123);
|
||||
let results: Vec<_> = messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, m)| m.text().to_lowercase().contains(&query))
|
||||
.collect();
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
|
||||
// Навигация: начинаем с индекса 0
|
||||
let mut current_index = 0;
|
||||
|
||||
// n - следующий результат
|
||||
current_index = (current_index + 1) % results.len();
|
||||
assert_eq!(current_index, 1);
|
||||
assert_eq!(results[current_index].1.text(), "Second match");
|
||||
|
||||
// n - ещё один
|
||||
current_index = (current_index + 1) % results.len();
|
||||
assert_eq!(current_index, 2);
|
||||
assert_eq!(results[current_index].1.text(), "Third match");
|
||||
|
||||
// n - wrap around к первому
|
||||
current_index = (current_index + 1) % results.len();
|
||||
assert_eq!(current_index, 0);
|
||||
assert_eq!(results[current_index].1.text(), "First match");
|
||||
|
||||
// N - предыдущий (wrap to last)
|
||||
current_index = if current_index == 0 {
|
||||
results.len() - 1
|
||||
} else {
|
||||
current_index - 1
|
||||
};
|
||||
assert_eq!(current_index, 2);
|
||||
assert_eq!(results[current_index].1.text(), "Third match");
|
||||
}
|
||||
|
||||
/// Test: Поиск с учётом регистра (case-insensitive)
|
||||
#[tokio::test]
|
||||
async fn test_search_case_insensitive() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg1 = TestMessageBuilder::new("HELLO", 100).build();
|
||||
let msg2 = TestMessageBuilder::new("hello", 101).build();
|
||||
let msg3 = TestMessageBuilder::new("HeLLo", 102).build();
|
||||
|
||||
let client = client.with_messages(123, vec![msg1, msg2, msg3]);
|
||||
|
||||
// Ищем "hello" (lowercase)
|
||||
let query = "hello".to_lowercase();
|
||||
let messages = client.get_messages(123);
|
||||
let found: Vec<_> = messages
|
||||
.iter()
|
||||
.filter(|m| m.text().to_lowercase().contains(&query))
|
||||
.collect();
|
||||
|
||||
// Все 3 варианта должны найтись
|
||||
assert_eq!(found.len(), 3);
|
||||
}
|
||||
|
||||
/// Test: Поиск не находит ничего
|
||||
#[tokio::test]
|
||||
async fn test_search_no_results() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let msg1 = TestMessageBuilder::new("Hello", 100).build();
|
||||
let msg2 = TestMessageBuilder::new("World", 101).build();
|
||||
|
||||
let client = client.with_messages(123, vec![msg1, msg2]);
|
||||
|
||||
// Ищем "xyz" - не должно найтись
|
||||
let query = "xyz".to_lowercase();
|
||||
let messages = client.get_messages(123);
|
||||
let found: Vec<_> = messages
|
||||
.iter()
|
||||
.filter(|m| m.text().to_lowercase().contains(&query))
|
||||
.collect();
|
||||
|
||||
assert_eq!(found.len(), 0);
|
||||
}
|
||||
|
||||
/// Test: Отмена поиска (Esc) восстанавливает обычный режим
|
||||
#[tokio::test]
|
||||
async fn test_cancel_search_restores_normal_mode() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let chat1 = create_test_chat("Mom", 123);
|
||||
let chat2 = create_test_chat("Boss", 456);
|
||||
|
||||
let client = client.with_chats(vec![chat1, chat2]);
|
||||
|
||||
// Симулируем: пользователь начал поиск
|
||||
let mut search_query = "mom".to_string();
|
||||
|
||||
// Фильтруем
|
||||
let query = search_query.to_lowercase();
|
||||
let chats = client.get_chats();
|
||||
let filtered: Vec<_> = chats
|
||||
.iter()
|
||||
.filter(|c| c.title.to_lowercase().contains(&query))
|
||||
.collect();
|
||||
|
||||
assert_eq!(filtered.len(), 1);
|
||||
|
||||
// Пользователь нажал Esc
|
||||
let is_searching = false;
|
||||
search_query.clear();
|
||||
|
||||
// После отмены видим все чаты
|
||||
let all_chats = client.get_chats();
|
||||
assert_eq!(all_chats.len(), 2);
|
||||
assert!(!is_searching);
|
||||
assert_eq!(search_query, "");
|
||||
}
|
||||
177
crates/tele-tui/tests/send_message.rs
Normal file
177
crates/tele-tui/tests/send_message.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
// Integration tests for send message flow
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::fake_tdclient::FakeTdClient;
|
||||
use helpers::test_data::{create_test_chat, TestMessageBuilder};
|
||||
use tele_tui::types::ChatId;
|
||||
|
||||
/// Test: Отправка текстового сообщения
|
||||
#[tokio::test]
|
||||
async fn test_send_text_message() {
|
||||
let client = FakeTdClient::new();
|
||||
let chat = create_test_chat("Mom", 123);
|
||||
let client = client.with_chat(chat);
|
||||
|
||||
// Отправляем сообщение
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "Hello, Mom!".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что сообщение было отправлено
|
||||
assert_eq!(client.get_sent_messages().len(), 1);
|
||||
assert_eq!(client.get_sent_messages()[0].chat_id, 123);
|
||||
assert_eq!(client.get_sent_messages()[0].text, "Hello, Mom!");
|
||||
assert_eq!(client.get_sent_messages()[0].reply_to, None);
|
||||
|
||||
// Проверяем что сообщение добавилось в список
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0].id(), msg.id());
|
||||
assert_eq!(messages[0].text(), "Hello, Mom!");
|
||||
assert!(messages[0].is_outgoing());
|
||||
}
|
||||
|
||||
/// Test: Отправка нескольких сообщений обновляет список
|
||||
#[tokio::test]
|
||||
async fn test_send_multiple_messages_updates_list() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Отправляем первое сообщение
|
||||
let msg1 = client
|
||||
.send_message(ChatId::new(123), "Message 1".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Отправляем второе сообщение
|
||||
let msg2 = client
|
||||
.send_message(ChatId::new(123), "Message 2".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Отправляем третье сообщение
|
||||
let msg3 = client
|
||||
.send_message(ChatId::new(123), "Message 3".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что все 3 сообщения отслеживаются
|
||||
assert_eq!(client.get_sent_messages().len(), 3);
|
||||
|
||||
// Проверяем что все сообщения в списке
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 3);
|
||||
assert_eq!(messages[0].id(), msg1.id());
|
||||
assert_eq!(messages[1].id(), msg2.id());
|
||||
assert_eq!(messages[2].id(), msg3.id());
|
||||
assert_eq!(messages[0].text(), "Message 1");
|
||||
assert_eq!(messages[1].text(), "Message 2");
|
||||
assert_eq!(messages[2].text(), "Message 3");
|
||||
}
|
||||
|
||||
/// Test: Отправка пустого сообщения (должно быть игнорировано на уровне App)
|
||||
/// Здесь мы тестируем что FakeTdClient технически может отправить пустое сообщение,
|
||||
/// но в реальном App это должно фильтроваться
|
||||
#[tokio::test]
|
||||
async fn test_send_empty_message_technical() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// FakeTdClient технически может отправить пустое сообщение
|
||||
let msg = client
|
||||
.send_message(ChatId::new(123), "".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что оно отправилось (в реальном App это должно фильтроваться)
|
||||
assert_eq!(client.get_sent_messages().len(), 1);
|
||||
assert_eq!(client.get_sent_messages()[0].text, "");
|
||||
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0].id(), msg.id());
|
||||
assert_eq!(messages[0].text(), "");
|
||||
}
|
||||
|
||||
/// Test: Отправка сообщения с форматированием (markdown сущности)
|
||||
/// В данном случае мы не проверяем парсинг markdown, только что текст сохраняется
|
||||
#[tokio::test]
|
||||
async fn test_send_message_with_markdown() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
let text = "**Bold** *italic* `code`";
|
||||
client
|
||||
.send_message(ChatId::new(123), text.to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем что текст сохранился как есть (парсинг markdown - отдельная логика)
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0].text(), text);
|
||||
}
|
||||
|
||||
/// Test: Отправка сообщения в разные чаты
|
||||
#[tokio::test]
|
||||
async fn test_send_messages_to_different_chats() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Отправляем в чат 123
|
||||
client
|
||||
.send_message(ChatId::new(123), "Hello Mom".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Отправляем в чат 456
|
||||
client
|
||||
.send_message(ChatId::new(456), "Hello Boss".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Отправляем ещё одно в чат 123
|
||||
client
|
||||
.send_message(ChatId::new(123), "How are you?".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Проверяем общее количество отправленных
|
||||
assert_eq!(client.get_sent_messages().len(), 3);
|
||||
|
||||
// Проверяем что сообщения распределены по чатам
|
||||
let chat123_messages = client.get_messages(123);
|
||||
assert_eq!(chat123_messages.len(), 2);
|
||||
assert_eq!(chat123_messages[0].text(), "Hello Mom");
|
||||
assert_eq!(chat123_messages[1].text(), "How are you?");
|
||||
|
||||
let chat456_messages = client.get_messages(456);
|
||||
assert_eq!(chat456_messages.len(), 1);
|
||||
assert_eq!(chat456_messages[0].text(), "Hello Boss");
|
||||
}
|
||||
|
||||
/// Test: Новое сообщение появляется в реальном времени (симуляция)
|
||||
/// Тестируем что когда приходит новое входящее сообщение, оно добавляется в список
|
||||
#[tokio::test]
|
||||
async fn test_receive_incoming_message() {
|
||||
let client = FakeTdClient::new();
|
||||
|
||||
// Добавляем существующее сообщение
|
||||
client
|
||||
.send_message(ChatId::new(123), "My outgoing".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Симулируем входящее сообщение от собеседника
|
||||
let incoming_msg = TestMessageBuilder::new("Hey there!", 2000)
|
||||
.sender("Alice")
|
||||
.build();
|
||||
|
||||
let client = client.with_message(123, incoming_msg);
|
||||
|
||||
// Проверяем что в списке 2 сообщения
|
||||
let messages = client.get_messages(123);
|
||||
assert_eq!(messages.len(), 2);
|
||||
assert!(messages[0].is_outgoing()); // Наше сообщение
|
||||
assert!(!messages[1].is_outgoing()); // Входящее
|
||||
assert_eq!(messages[1].text(), "Hey there!");
|
||||
assert_eq!(messages[1].sender_name(), "Alice");
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/chat_list.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Mom │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/chat_list.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Mom │
|
||||
│ Boss │
|
||||
│ Rust Community │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/chat_list.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Very Long Chat Title That Should Be Truncated │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
28
crates/tele-tui/tests/snapshots/chat_list__chat_muted.snap
Normal file
28
crates/tele-tui/tests/snapshots/chat_list__chat_muted.snap
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/chat_list.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 🔇 Spam Group (99) │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
28
crates/tele-tui/tests/snapshots/chat_list__chat_pinned.snap
Normal file
28
crates/tele-tui/tests/snapshots/chat_list__chat_pinned.snap
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/chat_list.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 📌 Important Chat │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/chat_list.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│▌ Mom │
|
||||
│ Boss │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/chat_list.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Work Group @ (10) │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/chat_list.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│▌ Alice │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/chat_list.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Mom (5) │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/chat_list.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 22
|
||||
expression: output
|
||||
---
|
||||
[default] Инициализация TDLib...
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 90
|
||||
expression: output
|
||||
---
|
||||
[default] ⏳ Подключение... | Инициализация TDLib...
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 73
|
||||
expression: output
|
||||
---
|
||||
[default] ⏳ Прокси... | Инициализация TDLib...
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 56
|
||||
expression: output
|
||||
---
|
||||
[default] ⚠ Нет сети | Инициализация TDLib...
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 39
|
||||
expression: output
|
||||
---
|
||||
[default] Инициализация TDLib...
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 107
|
||||
expression: output
|
||||
---
|
||||
[default] Инициализация TDLib...
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/input_field.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│Нет сообщений │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/input_field.rs
|
||||
assertion_line: 111
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ ▶ Original message text (14:33 ✓✓) │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌ Редактирование (Esc отмена) ─────────────────────────────────────────────────┐
|
||||
│✏ Edited text here │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/input_field.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│Нет сообщений │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> This is a longer message that will wrap to multiple lines in the input field│
|
||||
│for testing purposes. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/input_field.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│Нет сообщений │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod │
|
||||
│tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, │
|
||||
│quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo │
|
||||
│consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse │
|
||||
│cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non │
|
||||
│proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed │
|
||||
│ut perspiciatis unde omnis iste natus error sit voluptatem accusantium │
|
||||
│doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/input_field.rs
|
||||
assertion_line: 135
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Mom ──────────────── │
|
||||
│ (14:33) What do you think about this? │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌ Ответ (Esc отмена) ──────────────────────────────────────────────────────────┐
|
||||
│↪ Mom: What do yo > I think it's great! │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/input_field.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 hello │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/input_field.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│Нет сообщений │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Hello, how are you? │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 418
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Alice ──────────────── │
|
||||
│ (14:33) 📷 [Фото] │
|
||||
│ (14:33) Caption for album │
|
||||
│ (14:33) 📷 [Фото] │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 444
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ 📷 [Фото] (14:33 ✓✓)│
|
||||
│ My vacation photos (14:33 ✓✓) │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 503
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Alice ──────────────── │
|
||||
│ (14:33) 📷 [Фото] │
|
||||
│▶ (14:33) 📷 [Фото] │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌ Выбор сообщения ─────────────────────────────────────────────────────────────┐
|
||||
│↑↓ · r ответ · f переслать · y копир. · d удалить · Esc │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 476
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Group Chat │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Alice ──────────────── │
|
||||
│ (14:33) Regular message before │
|
||||
│ (14:33) 📷 [Фото] │
|
||||
│ (14:33) Album caption │
|
||||
│ (14:33) Regular message after │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 87
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Message from the past │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 186
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33 ✎) Edited text │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
28
crates/tele-tui/tests/snapshots/messages__empty_chat.snap
Normal file
28
crates/tele-tui/tests/snapshots/messages__empty_chat.snap
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│Нет сообщений │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 325
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│↪ Переслано от Alice │
|
||||
│ (14:33) Forwarded content │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 206
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) This is a very long message that should wrap across multiple lines │
|
||||
│ when rendered in the terminal UI. Let's make it even longer to │
|
||||
│ ensure we test the wrapping behavior properly. │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 225
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) **bold** *italic* `code` │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 245
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Check [this](https://example.com) and @username │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 264
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Spoiler: ||hidden text|| │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 283
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) [Фото] │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 368
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Popular message │
|
||||
│[👍 ] 5 👎 3 │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
29
crates/tele-tui/tests/snapshots/messages__outgoing_read.snap
Normal file
29
crates/tele-tui/tests/snapshots/messages__outgoing_read.snap
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 167
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ Read message (14:33 ✓✓) │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
29
crates/tele-tui/tests/snapshots/messages__outgoing_sent.snap
Normal file
29
crates/tele-tui/tests/snapshots/messages__outgoing_sent.snap
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 137
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ Just sent (14:33 ✓✓) │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
29
crates/tele-tui/tests/snapshots/messages__reply_message.snap
Normal file
29
crates/tele-tui/tests/snapshots/messages__reply_message.snap
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 304
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│┌ Mom: Original message text │
|
||||
│ (14:33) This is a reply │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 388
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Selected message │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌ Выбор сообщения ─────────────────────────────────────────────────────────────┐
|
||||
│↑↓ · r ответить · f переслать · y копировать · Esc │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 118
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Group Chat │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Alice ──────────────── │
|
||||
│ (14:33) First message │
|
||||
│ (14:33) Second message │
|
||||
│ │
|
||||
│Bob ──────────────── │
|
||||
│ (14:33) Third message │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 46
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│Mom ──────────────── │
|
||||
│ (14:33) Hello there! │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 65
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ Hi mom! (14:33 ✓✓) │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/messages.rs
|
||||
assertion_line: 346
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Great! │
|
||||
│[👍 ] │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
assertion_line: 30
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ Delete me (14:33 ✓✓) │
|
||||
│ ┌ Подтверждение ───────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ Удалить сообщение? │ │
|
||||
│ │ │ │
|
||||
│ │ [y/Enter] Да [n/Esc] Нет │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
assertion_line: 61
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) React to this │
|
||||
│ │
|
||||
│ ┌ Выбери реакцию ────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ 👍 👎 ❤️ 🔥 😊 😢 😮 🎉 │ │
|
||||
│ │ │ │
|
||||
│ └────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
assertion_line: 97
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) React to this │
|
||||
│ │
|
||||
│ ┌ Выбери реакцию ────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ 👍 👎 ❤️ 🔥 😊 😢 😮 🎉 │ │
|
||||
│ │ │ │
|
||||
│ └────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
28
crates/tele-tui/tests/snapshots/modals__forward_mode.snap
Normal file
28
crates/tele-tui/tests/snapshots/modals__forward_mode.snap
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌ ↪ Выберите чат ──────────────────────────────────────────────────────────────┐
|
||||
│▌ Mom │
|
||||
│ Dad │
|
||||
│ Work Group │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
29
crates/tele-tui/tests/snapshots/modals__pinned_message.snap
Normal file
29
crates/tele-tui/tests/snapshots/modals__pinned_message.snap
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
assertion_line: 163
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 Mom │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
📌 20.12.2021 14:33 Important pinned message! Ctrl+P
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ──────── 20.12.2021 ──────── │
|
||||
│ │
|
||||
│User ──────────────── │
|
||||
│ (14:33) Regular message │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 ПРОФИЛЬ: Work Group │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│Тип: Группа │
|
||||
│ │
|
||||
│ID: 456 │
|
||||
│ │
|
||||
│Участников: 25 │
|
||||
│ │
|
||||
│Описание: │
|
||||
│Work discussion group │
|
||||
│ │
|
||||
│──────────────────────────────── │
|
||||
│ │
|
||||
│Действия: │
|
||||
│ │
|
||||
│▶ Скопировать ID │
|
||||
│ Покинуть группу │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ↑↓ навигация Enter выбрать Esc выход │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
expression: output
|
||||
---
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│👤 ПРОФИЛЬ: Alice │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│Тип: Личный чат │
|
||||
│ │
|
||||
│ID: 123 │
|
||||
│ │
|
||||
│──────────────────────────────── │
|
||||
│ │
|
||||
│Действия: │
|
||||
│ │
|
||||
│▶ Скопировать ID │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ↑↓ навигация Enter выбрать Esc выход │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
29
crates/tele-tui/tests/snapshots/modals__search_in_chat.snap
Normal file
29
crates/tele-tui/tests/snapshots/modals__search_in_chat.snap
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tests/modals.rs
|
||||
assertion_line: 193
|
||||
expression: output
|
||||
---
|
||||
┌ Поиск по сообщениям ─────────────────────────────────────────────────────────┐
|
||||
│🔍 world█ (1/2) │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│▶ User (20.12.2021 14:33) │
|
||||
│ Hello world │
|
||||
│ │
|
||||
│ User (20.12.2021 14:33) │
|
||||
│ World is beautiful │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ↑↓ навигация n/N след./пред. Enter перейти Esc выход │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: tests/screens.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
┌──────────────────────────────────────┐
|
||||
│ TTUI - Telegram Authentication │
|
||||
└──────────────────────────────────────┘
|
||||
Введите код подтверждения из Telegram
|
||||
Код был отправлен на ваш номер
|
||||
|
||||
|
||||
┌ Verification Code ───────────────────┐
|
||||
│ 🔐 1234 │
|
||||
└──────────────────────────────────────┘
|
||||
Инициализация TDLib...
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: tests/screens.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
┌──────────────────────────────────────┐
|
||||
│ TTUI - Telegram Authentication │
|
||||
└──────────────────────────────────────┘
|
||||
Введите пароль двухфакторной аутентифика
|
||||
|
||||
|
||||
|
||||
┌ Password ────────────────────────────┐
|
||||
│ 🔒 **** │
|
||||
└──────────────────────────────────────┘
|
||||
Инициализация TDLib...
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: tests/screens.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
┌──────────────────────────────────────┐
|
||||
│ TTUI - Telegram Authentication │
|
||||
└──────────────────────────────────────┘
|
||||
Введите номер телефона в международном ф
|
||||
Пример: +79991111111
|
||||
|
||||
|
||||
┌ Phone Number ────────────────────────┐
|
||||
│ 📱 +7 │
|
||||
└──────────────────────────────────────┘
|
||||
Инициализация TDLib...
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: tests/screens.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
┌ TTUI ────────────────────────────────────────────────────────────────────────┐
|
||||
│ Инициализация TDLib... │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: tests/screens.rs
|
||||
expression: output
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
┌ TTUI ────────────────────────────────────────────────────────────────────────┐
|
||||
│ Подключение к Telegram... │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: tests/screens.rs
|
||||
assertion_line: 197
|
||||
expression: output
|
||||
---
|
||||
┌ TTUI ────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 1:All │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
┌────────────────────────────┐┌────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска ││ Выберите чат │
|
||||
└────────────────────────────┘│ │
|
||||
┌────────────────────────────┐│ │
|
||||
│ Mom (2) ││ │
|
||||
│ Work Group @ ││ │
|
||||
│ Boss ││ │
|
||||
│ │┌ АККАУНТЫ ────────────────────────────┐ │
|
||||
│ ││ │ │
|
||||
│ ││ ● personal (Personal) (текущий) │ │
|
||||
│ ││ work (Work) │ │
|
||||
│ ││ ────────────────────── │ │
|
||||
│ ││ + Добавить аккаунт │ │
|
||||
│ ││ │ │
|
||||
│ ││ j/k Nav Enter Select a Add Esc │ │
|
||||
│ │└──────────────────────────────────────┘ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
└────────────────────────────┘│ │
|
||||
┌────────────────────────────┐│ │
|
||||
│ ││ │
|
||||
└────────────────────────────┘└────────────────────────────────────────────────────────────────────┘
|
||||
[personal] Инициализация TDLib...
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: tests/screens.rs
|
||||
assertion_line: 131
|
||||
expression: output
|
||||
---
|
||||
┌ TTUI ────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 1:All │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
┌────────────────────────────┐┌────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска ││ Выберите чат │
|
||||
└────────────────────────────┘│ │
|
||||
┌────────────────────────────┐│ │
|
||||
│ Mom (2) ││ │
|
||||
│ Work Group @ ││ │
|
||||
│ Boss ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
└────────────────────────────┘│ │
|
||||
┌────────────────────────────┐│ │
|
||||
│ ││ │
|
||||
└────────────────────────────┘└────────────────────────────────────────────────────────────────────┘
|
||||
[default] Инициализация TDLib...
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: tests/screens.rs
|
||||
assertion_line: 167
|
||||
expression: output
|
||||
---
|
||||
┌ TTUI ────────────────────────────────────────────────────┐
|
||||
│ 1:All │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│👤 Work Group │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ (14:33) Standup notes are ready │
|
||||
│ │
|
||||
│ Вы ──────────────── │
|
||||
│ Thanks, I will review them after lunch (14:33 ✓✓) │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│> Press i to type... │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
[default] Инициализация TDLib...
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: tests/screens.rs
|
||||
assertion_line: 150
|
||||
expression: output
|
||||
---
|
||||
┌ TTUI ────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 1:All │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
┌────────────────────────────┐┌────────────────────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска ││👤 Work Group │
|
||||
└────────────────────────────┘└────────────────────────────────────────────────────────────────────┘
|
||||
┌────────────────────────────┐┌────────────────────────────────────────────────────────────────────┐
|
||||
│ Mom (2) ││ ──────── 20.12.2021 ──────── │
|
||||
│▌ Work Group @ ││ │
|
||||
│ Boss ││Alice ──────────────── │
|
||||
│ ││ (14:33) Morning, team │
|
||||
│ ││ │
|
||||
│ ││Bob ──────────────── │
|
||||
│ ││ (14:33) Standup notes are ready │
|
||||
│ ││ │
|
||||
│ ││ Вы ──────────────── │
|
||||
│ ││ Thanks, I will review them after lunch (14:33 ✓✓) │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
└────────────────────────────┘└────────────────────────────────────────────────────────────────────┘
|
||||
┌────────────────────────────┐┌────────────────────────────────────────────────────────────────────┐
|
||||
│ ││> Draft reply │
|
||||
└────────────────────────────┘└────────────────────────────────────────────────────────────────────┘
|
||||
[default] Инициализация TDLib...
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: tests/screens.rs
|
||||
expression: output
|
||||
---
|
||||
┌ TTUI ────────────────────────────────────────────────────────────────────────┐
|
||||
│ 1:All │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────┐┌──────────────────────────────────────────────────────┐
|
||||
│🔍 Ctrl+S для поиска ││ Выберите чат │
|
||||
└──────────────────────┘│ │
|
||||
┌──────────────────────┐│ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
└──────────────────────┘│ │
|
||||
┌──────────────────────┐│ │
|
||||
│ ││ │
|
||||
└──────────────────────┘└──────────────────────────────────────────────────────┘
|
||||
[default] Инициализация TDLib...
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/screens.rs
|
||||
expression: output
|
||||
---
|
||||
30x8
|
||||
Минимум: 40x10
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
source: tests/style_snapshots.rs
|
||||
assertion_line: 78
|
||||
expression: buffer_to_style_snapshot(&buffer)
|
||||
---
|
||||
y=1: 1..1 Cyan/Reset/BOLD: "👤" | 3..6 Cyan/Reset/BOLD: " Mom"
|
||||
y=4: 21..48 Gray/Reset/NONE: "──────── 20.12.2021 ────────"
|
||||
y=6: 1..4 Cyan/Reset/BOLD: "Mom" | 5..9 Gray/Reset/NONE: "─────" | 10..10 Yellow/Reset/NONE: "┌" | 11..26 Yellow/Reset/BOLD: " Выбери реакцию" | 27..59 Yellow/Reset/NONE: "────────────────────────────────┐"
|
||||
y=7: 1..2 Yellow/Reset/BOLD: "" | 3..9 Gray/Reset/NONE: " (14:33" | 10..10 Yellow/Reset/NONE: "│" | 59..59 Yellow/Reset/NONE: "│"
|
||||
y=8: 10..10 Yellow/Reset/NONE: "│" | 26..27 White/Reset/NONE: " 👍" | 29..29 White/Reset/NONE: "" | 31..32 White/Reset/NONE: " ❤\u{fe0f}" | 34..34 White/Reset/NONE: "" | 36..37 Yellow/Reset/BOLD | REVERSED: " 😂" | 39..39 Yellow/Reset/BOLD | REVERSED: "" | 41..42 White/Reset/NONE: " 🔥" | 44..44 White/Reset/NONE: "" | 59..59 Yellow/Reset/NONE: "│"
|
||||
y=9: 10..10 Yellow/Reset/NONE: "│" | 59..59 Yellow/Reset/NONE: "│"
|
||||
y=10: 10..59 Yellow/Reset/NONE: "└────────────────────────────────────────────────┘"
|
||||
y=15: 0..69 DarkGray/Reset/NONE: "┌────────────────────────────────────────────────────────────────────┐"
|
||||
y=16: 0..20 DarkGray/Reset/NONE: "│> Press i to type..." | 69..69 DarkGray/Reset/NONE: "│"
|
||||
y=17: 0..69 DarkGray/Reset/NONE: "└────────────────────────────────────────────────────────────────────┘"
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: tests/style_snapshots.rs
|
||||
assertion_line: 24
|
||||
expression: buffer_to_style_snapshot(&buffer)
|
||||
---
|
||||
y=0: 0..35 Rgb(160, 160, 160)/Reset/NONE: "┌──────────────────────────────────┐"
|
||||
y=1: 0..1 Rgb(160, 160, 160)/Reset/NONE: "│🔍" | 3..35 Rgb(160, 160, 160)/Reset/NONE: " Ctrl+S для поиска │"
|
||||
y=2: 0..35 Rgb(160, 160, 160)/Reset/NONE: "└──────────────────────────────────┘"
|
||||
y=4: 1..34 White/Reset/NONE: " Mom"
|
||||
y=5: 1..34 Yellow/Reset/ITALIC: " Work Group"
|
||||
y=6: 1..34 White/Reset/NONE: " Boss"
|
||||
y=9: 0..35 DarkGray/Reset/NONE: "┌──────────────────────────────────┐"
|
||||
y=10: 0..35 DarkGray/Reset/NONE: "│ │"
|
||||
y=11: 0..35 DarkGray/Reset/NONE: "└──────────────────────────────────┘"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user