Refactor TDLib facade and local time handling

This commit is contained in:
Mikhail Kilin
2026-05-17 17:58:29 +03:00
parent e09b83be69
commit 2e510dc932
38 changed files with 1025 additions and 862 deletions

View File

@@ -3,7 +3,6 @@
mod helpers;
use helpers::app_builder::TestAppBuilder;
use helpers::test_data::create_test_chat;
use tele_tui::app::AccountSwitcherState;
// ============ Open/Close Tests ============

View File

@@ -56,8 +56,6 @@ fn snapshot_chat_with_unread_count() {
#[test]
fn test_incoming_message_shows_unread_badge() {
use tele_tui::tdlib::ChatInfo;
use tele_tui::types::ChatId;
// Создаём чат БЕЗ непрочитанных сообщений
let chat = TestChatBuilder::new("Friend", 999)
@@ -97,7 +95,7 @@ fn test_incoming_message_shows_unread_badge() {
#[tokio::test]
async fn test_opening_chat_clears_unread_badge() {
use helpers::test_data::TestMessageBuilder;
use tele_tui::tdlib::TdClientTrait;
use tele_tui::types::{ChatId, MessageId};
// Создаём чат с 3 непрочитанными сообщениями
@@ -188,7 +186,7 @@ async fn test_opening_chat_clears_unread_badge() {
#[tokio::test]
async fn test_opening_chat_loads_many_messages() {
use helpers::test_data::TestMessageBuilder;
use tele_tui::tdlib::TdClientTrait;
use tele_tui::types::ChatId;
// Создаём чат с 50 сообщениями
@@ -205,7 +203,7 @@ async fn test_opening_chat_loads_many_messages() {
})
.collect();
let mut app = TestAppBuilder::new()
let app = TestAppBuilder::new()
.with_chat(chat)
.with_messages(888, messages)
.build();
@@ -230,7 +228,6 @@ async fn test_opening_chat_loads_many_messages() {
#[tokio::test]
async fn test_chat_history_chunked_loading() {
use tele_tui::tdlib::TdClientTrait;
use tele_tui::types::ChatId;
// Создаём чат с 120 сообщениями (больше чем TDLIB_MESSAGE_LIMIT = 50)
@@ -247,7 +244,7 @@ async fn test_chat_history_chunked_loading() {
})
.collect();
let mut app = TestAppBuilder::new()
let app = TestAppBuilder::new()
.with_chat(chat)
.with_messages(999, messages)
.build();
@@ -295,7 +292,6 @@ async fn test_chat_history_chunked_loading() {
#[tokio::test]
async fn test_chat_history_loads_all_without_limit() {
use tele_tui::tdlib::TdClientTrait;
use tele_tui::types::ChatId;
// Создаём чат с 200 сообщениями (4 чанка по 50)
@@ -311,7 +307,7 @@ async fn test_chat_history_loads_all_without_limit() {
})
.collect();
let mut app = TestAppBuilder::new()
let app = TestAppBuilder::new()
.with_chat(chat)
.with_messages(1001, messages)
.build();
@@ -331,8 +327,7 @@ async fn test_chat_history_loads_all_without_limit() {
#[tokio::test]
async fn test_load_older_messages_pagination() {
use tele_tui::tdlib::TdClientTrait;
use tele_tui::types::{ChatId, MessageId};
use tele_tui::types::ChatId;
// Создаём чат со 150 сообщениями
let chat = TestChatBuilder::new("Paginated Chat", 1002)
@@ -347,7 +342,7 @@ async fn test_load_older_messages_pagination() {
})
.collect();
let mut app = TestAppBuilder::new()
let app = TestAppBuilder::new()
.with_chat(chat)
.with_messages(1002, messages)
.build();
@@ -490,8 +485,6 @@ fn snapshot_chat_search_mode() {
#[test]
fn snapshot_chat_with_online_status() {
use tele_tui::tdlib::UserOnlineStatus;
use tele_tui::types::ChatId;
let chat = TestChatBuilder::new("Alice", 123)
.last_message("Hey there!")

View File

@@ -167,8 +167,7 @@ mod credentials_tests {
// Примечание: этот тест может зафейлиться если есть credentials файл,
// так как он имеет приоритет. Для полноценного тестирования нужно
// моковать файловую систему или использовать временные директории.
if result.is_ok() {
let (api_id, api_hash) = result.unwrap();
if let Ok((api_id, api_hash)) = result {
// Может быть либо из файла, либо из env
assert!(api_id > 0);
assert!(!api_hash.is_empty());
@@ -210,14 +209,13 @@ mod credentials_tests {
let result = Config::load_credentials();
// Должна быть ошибка
if result.is_ok() {
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");
} else {
// Проверяем формат ошибки
let err_msg = result.unwrap_err();
assert!(!err_msg.is_empty(), "Error message should not be empty");
}
// Восстанавливаем env переменные

View File

@@ -113,7 +113,6 @@ fn format_message_for_test(msg: &tele_tui::tdlib::MessageInfo) -> String {
#[cfg(all(test, feature = "clipboard"))]
mod clipboard_tests {
use super::*;
/// Test: Проверка что clipboard функции не падают
/// Примечание: Реальное тестирование clipboard требует GUI окружения

View File

@@ -96,12 +96,12 @@ async fn test_can_only_delete_own_messages_for_all() {
// Проверяем флаги удаления
let messages = client.get_messages(123);
assert_eq!(messages[0].can_be_deleted_for_all_users(), true); // Наше
assert_eq!(messages[1].can_be_deleted_for_all_users(), false); // Чужое
assert!(messages[0].can_be_deleted_for_all_users()); // Наше
assert!(!messages[1].can_be_deleted_for_all_users()); // Чужое
// Оба можно удалить для себя
assert_eq!(messages[0].can_be_deleted_only_for_self(), true);
assert_eq!(messages[1].can_be_deleted_only_for_self(), true);
assert!(messages[0].can_be_deleted_only_for_self());
assert!(messages[1].can_be_deleted_only_for_self());
}
/// Test: Удаление несуществующего сообщения (ничего не происходит)

View File

@@ -4,7 +4,6 @@ mod helpers;
use helpers::test_data::{create_test_chat, TestChatBuilder};
use std::collections::HashMap;
use tele_tui::types::{ChatId, MessageId};
/// Простая структура для хранения черновиков (как в реальном App)
struct DraftManager {
@@ -105,11 +104,11 @@ async fn test_draft_indicator_in_chat_list() {
let mut drafts = DraftManager::new();
// Создаём несколько чатов
let chat1 = create_test_chat("Mom", 123);
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);
let _chat3 = create_test_chat("Friend", 789);
// В реальном App: chat.draft_text устанавливается из DraftManager
// Здесь просто проверяем что у chat2 есть draft_text поле

View File

@@ -34,8 +34,10 @@ fn test_minimum_terminal_size() {
const MIN_HEIGHT: u16 = 20;
// Проверяем что константы установлены разумно
assert!(MIN_WIDTH >= 80, "Минимальная ширина должна быть >= 80");
assert!(MIN_HEIGHT >= 20, "Минимальная высота должна быть >= 20");
const {
assert!(MIN_WIDTH >= 80, "Минимальная ширина должна быть >= 80");
assert!(MIN_HEIGHT >= 20, "Минимальная высота должна быть >= 20");
};
// Проверяем граничные случаи
let too_small_width = MIN_WIDTH - 1;
@@ -51,13 +53,13 @@ fn test_app_constants() {
use tele_tui::constants::*;
// Проверяем что лимиты установлены
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, "Слишком большой лимит чатов");
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 флаг

View File

@@ -5,7 +5,7 @@ mod helpers;
use helpers::fake_tdclient::{FakeTdClient, TdUpdate};
use helpers::test_data::{TestChatBuilder, TestMessageBuilder};
use tele_tui::tdlib::NetworkState;
use tele_tui::types::{ChatId, MessageId};
use tele_tui::types::ChatId;
/// Тест 1: App Launch → Auth → Chat List
/// Симулирует полный путь пользователя от запуска до загрузки чатов

View File

@@ -81,8 +81,8 @@ async fn test_can_only_edit_own_messages() {
// Проверяем флаги
let messages = client.get_messages(123);
assert_eq!(messages[0].can_be_edited(), true); // Наше сообщение
assert_eq!(messages[1].can_be_edited(), false); // Чужое сообщение
assert!(messages[0].can_be_edited()); // Наше сообщение
assert!(!messages[1].can_be_edited()); // Чужое сообщение
}
/// Test: Множественные редактирования одного сообщения

View File

@@ -26,7 +26,7 @@ fn snapshot_footer_chat_list() {
fn snapshot_footer_open_chat() {
let chat = create_test_chat("Mom", 123);
let mut app = TestAppBuilder::new()
let app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(123)
.build();
@@ -43,7 +43,7 @@ fn snapshot_footer_open_chat() {
fn snapshot_footer_network_waiting() {
let chat = create_test_chat("Mom", 123);
let mut app = TestAppBuilder::new().with_chat(chat).build();
let app = TestAppBuilder::new().with_chat(chat).build();
// Set network state to WaitingForNetwork
*app.td_client.network_state.lock().unwrap() = NetworkState::WaitingForNetwork;
@@ -60,7 +60,7 @@ fn snapshot_footer_network_waiting() {
fn snapshot_footer_network_connecting_proxy() {
let chat = create_test_chat("Mom", 123);
let mut app = TestAppBuilder::new().with_chat(chat).build();
let app = TestAppBuilder::new().with_chat(chat).build();
// Set network state to ConnectingToProxy
*app.td_client.network_state.lock().unwrap() = NetworkState::ConnectingToProxy;
@@ -77,7 +77,7 @@ fn snapshot_footer_network_connecting_proxy() {
fn snapshot_footer_network_connecting() {
let chat = create_test_chat("Mom", 123);
let mut app = TestAppBuilder::new().with_chat(chat).build();
let app = TestAppBuilder::new().with_chat(chat).build();
// Set network state to Connecting
*app.td_client.network_state.lock().unwrap() = NetworkState::Connecting;
@@ -94,7 +94,7 @@ fn snapshot_footer_network_connecting() {
fn snapshot_footer_search_mode() {
let chat = create_test_chat("Mom", 123);
let mut app = TestAppBuilder::new()
let app = TestAppBuilder::new()
.with_chat(chat)
.searching("query")
.build();

View File

@@ -146,7 +146,7 @@ impl TestAppBuilder {
pub fn with_message(mut self, chat_id: i64, message: MessageInfo) -> Self {
self.messages
.entry(chat_id)
.or_insert_with(Vec::new)
.or_default()
.push(message);
self
}
@@ -155,7 +155,7 @@ impl TestAppBuilder {
pub fn with_messages(mut self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
self.messages
.entry(chat_id)
.or_insert_with(Vec::new)
.or_default()
.extend(messages);
self
}

View File

@@ -7,13 +7,16 @@ use tele_tui::tdlib::{AuthState, ChatInfo, MessageInfo, NetworkState, ProfileInf
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 события от TDLib (упрощённая версия)
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum TdUpdate {
NewMessage {
chat_id: ChatId,
message: MessageInfo,
message: Box<MessageInfo>,
},
MessageContent {
chat_id: ChatId,
@@ -72,9 +75,9 @@ pub struct FakeTdClient {
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<Vec<(i64, Vec<i64>)>>>, // (chat_id, message_ids)
pub viewed_messages: Arc<Mutex<ViewedMessages>>, // (chat_id, message_ids)
pub chat_actions: Arc<Mutex<Vec<(i64, String)>>>, // (chat_id, action)
pub pending_view_messages: Arc<Mutex<Vec<(ChatId, Vec<MessageId>)>>>, // Очередь для отметки как прочитанные
pub pending_view_messages: Arc<Mutex<PendingViewMessages>>, // Очередь для отметки как прочитанные
// Update channel для симуляции событий
pub update_tx: Arc<Mutex<Option<mpsc::UnboundedSender<TdUpdate>>>>,
@@ -238,7 +241,7 @@ impl FakeTdClient {
.lock()
.unwrap()
.entry(chat_id)
.or_insert_with(Vec::new)
.or_default()
.push(message);
self
}
@@ -424,11 +427,11 @@ impl FakeTdClient {
.lock()
.unwrap()
.entry(chat_id.as_i64())
.or_insert_with(Vec::new)
.or_default()
.push(message.clone());
// Отправляем Update::NewMessage
self.send_update(TdUpdate::NewMessage { chat_id, message: message.clone() });
self.send_update(TdUpdate::NewMessage { chat_id, message: Box::new(message.clone()) });
Ok(message)
}
@@ -759,11 +762,11 @@ impl FakeTdClient {
.lock()
.unwrap()
.entry(chat_id.as_i64())
.or_insert_with(Vec::new)
.or_default()
.push(message.clone());
// Отправляем Update
self.send_update(TdUpdate::NewMessage { chat_id, message });
self.send_update(TdUpdate::NewMessage { chat_id, message: Box::new(message) });
}
/// Симулировать typing от собеседника
@@ -852,6 +855,21 @@ impl FakeTdClient {
*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));
}
}
/// Установить update channel для получения событий
pub fn set_update_channel(&self, tx: mpsc::UnboundedSender<TdUpdate>) {
*self.update_tx.lock().unwrap() = Some(tx);

View File

@@ -1,10 +1,14 @@
//! Implementation of TdClientTrait for FakeTdClient
//! 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::TdClientTrait;
use tele_tui::tdlib::{
AccountClient, AuthClient, ChatActionClient, ChatClient, ClientState, FileClient,
MessageClient, NotificationClient, ReactionClient, UpdateClient, UserClient,
};
use tele_tui::tdlib::{
AuthState, ChatInfo, FolderInfo, MessageInfo, ProfileInfo, ReplyInfo, UserCache,
UserOnlineStatus,
@@ -12,8 +16,7 @@ use tele_tui::tdlib::{
use tele_tui::types::{ChatId, MessageId, UserId};
#[async_trait]
impl TdClientTrait for FakeTdClient {
// ============ Auth methods (not implemented for fake) ============
impl AuthClient for FakeTdClient {
async fn send_phone_number(&self, _phone: String) -> Result<(), String> {
Ok(())
}
@@ -25,10 +28,11 @@ impl TdClientTrait for FakeTdClient {
async fn send_password(&self, _password: String) -> Result<(), String> {
Ok(())
}
}
// ============ Chat methods ============
#[async_trait]
impl ChatClient for FakeTdClient {
async fn load_chats(&mut self, limit: i32) -> Result<(), String> {
// FakeTdClient loads chats but returns void
let _ = FakeTdClient::load_chats(self, limit as usize).await?;
Ok(())
}
@@ -38,7 +42,6 @@ impl TdClientTrait for FakeTdClient {
}
async fn leave_chat(&self, _chat_id: ChatId) -> Result<(), String> {
// Not implemented for fake client
Ok(())
}
@@ -46,18 +49,54 @@ impl TdClientTrait for FakeTdClient {
FakeTdClient::get_profile_info(self, chat_id).await
}
// ============ Chat actions ============
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) {
let action_str = format!("{:?}", action);
FakeTdClient::send_chat_action(self, chat_id, action_str).await;
FakeTdClient::send_chat_action(self, chat_id, format!("{:?}", action)).await;
}
fn clear_stale_typing_status(&mut self) -> bool {
// Not implemented for fake
false
}
// ============ Message methods ============
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,
@@ -75,13 +114,10 @@ impl TdClientTrait for FakeTdClient {
}
async fn get_pinned_messages(&mut self, _chat_id: ChatId) -> Result<Vec<MessageInfo>, String> {
// Not implemented for fake
Ok(vec![])
}
async fn load_current_pinned_message(&mut self, _chat_id: ChatId) {
// Not implemented for fake
}
async fn load_current_pinned_message(&mut self, _chat_id: ChatId) {}
async fn search_messages(
&self,
@@ -132,16 +168,77 @@ impl TdClientTrait for FakeTdClient {
FakeTdClient::set_draft_message(self, chat_id, text).await
}
fn push_message(&mut self, _msg: MessageInfo) {
// Not used in fake client
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())
}
}
async fn fetch_missing_reply_info(&mut self) {
// Not used in fake client
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) {
// Перемещаем pending в viewed для проверки в тестах
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();
@@ -151,18 +248,35 @@ impl TdClientTrait for FakeTdClient {
.push((chat_id.as_i64(), ids));
}
}
}
// ============ User methods ============
#[async_trait]
impl UserClient for FakeTdClient {
fn get_user_status_by_chat_id(&self, _chat_id: ChatId) -> Option<&UserOnlineStatus> {
// Not implemented for fake
None
}
async fn process_pending_user_ids(&mut self) {
// Not used in fake client
fn pending_user_ids(&self) -> &[UserId] {
&[]
}
// ============ Reaction methods ============
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,
@@ -179,29 +293,30 @@ impl TdClientTrait for FakeTdClient {
) -> Result<(), String> {
FakeTdClient::toggle_reaction(self, chat_id, message_id, reaction).await
}
}
// ============ File methods ============
#[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> {
// Fake implementation: return a fake path
Ok(format!("/tmp/fake_voice_{}.ogg", file_id))
}
}
// ============ Getters (immutable) ============
#[async_trait]
impl ClientState for FakeTdClient {
fn client_id(&self) -> i32 {
0 // Fake client ID
0
}
async fn get_me(&self) -> Result<i64, String> {
Ok(12345) // Fake user ID
Ok(12345)
}
fn auth_state(&self) -> &AuthState {
// Can't return reference from Arc<Mutex>, need to use a different approach
// For now, return a static reference based on the current state
use std::sync::OnceLock;
static AUTH_STATE_READY: AuthState = AuthState::Ready;
static AUTH_STATE_WAIT_PHONE: OnceLock<AuthState> = OnceLock::new();
@@ -222,133 +337,24 @@ impl TdClientTrait for FakeTdClient {
}
}
fn chats(&self) -> &[ChatInfo] {
// FakeTdClient uses Arc<Mutex>, can't return direct reference
// This is a limitation - we'll need to work around it
&[]
}
fn folders(&self) -> &[FolderInfo] {
&[]
}
fn current_chat_messages(&self) -> Vec<MessageInfo> {
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
return self.get_messages(chat_id);
}
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 typing_status(&self) -> Option<&(UserId, String, std::time::Instant)> {
None
}
fn pending_view_messages(&self) -> &[(ChatId, Vec<MessageId>)] {
&[]
}
fn pending_user_ids(&self) -> &[UserId] {
&[]
}
fn main_chat_list_position(&self) -> i32 {
0
}
fn user_cache(&self) -> &UserCache {
// Not implemented for fake - return empty cache
use std::sync::OnceLock;
static EMPTY_CACHE: OnceLock<UserCache> = OnceLock::new();
EMPTY_CACHE.get_or_init(|| UserCache::new(0))
}
fn network_state(&self) -> tele_tui::tdlib::types::NetworkState {
FakeTdClient::get_network_state(self)
}
}
// ============ Setters (mutable) ============
fn chats_mut(&mut self) -> &mut Vec<ChatInfo> {
// Can't return mutable reference from Arc<Mutex>
// This is a design limitation - we need a different approach
panic!("chats_mut not supported for FakeTdClient - use get_chats() instead")
}
impl NotificationClient for FakeTdClient {
fn configure_notifications(&mut self, _config: &tele_tui::config::NotificationsConfig) {}
fn folders_mut(&mut self) -> &mut Vec<FolderInfo> {
panic!("folders_mut not supported for FakeTdClient")
}
fn sync_notification_muted_chats(&mut self) {}
}
fn current_chat_messages_mut(&mut self) -> &mut Vec<MessageInfo> {
panic!("current_chat_messages_mut not supported for FakeTdClient")
}
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 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 set_typing_status(&mut self, _status: Option<(UserId, String, std::time::Instant)>) {
// Not implemented
}
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));
}
fn pending_user_ids_mut(&mut self) -> &mut Vec<UserId> {
panic!("pending_user_ids_mut not supported for FakeTdClient")
}
fn set_main_chat_list_position(&mut self, _position: i32) {
// Not implemented
}
fn user_cache_mut(&mut self) -> &mut UserCache {
panic!("user_cache_mut not supported for FakeTdClient")
}
// ============ Notification methods ============
fn configure_notifications(&mut self, _config: &tele_tui::config::NotificationsConfig) {
// Not implemented for fake client (notifications are not tested)
}
fn sync_notification_muted_chats(&mut self) {
// Not implemented for fake client (notifications are not tested)
}
// ============ Account switching ============
#[async_trait]
impl AccountClient for FakeTdClient {
async fn recreate_client(&mut self, _db_path: PathBuf) -> Result<(), String> {
// No-op for fake client
Ok(())
}
// ============ Update handling ============
fn handle_update(&mut self, _update: Update) {
// Not implemented for fake client
}
}
impl UpdateClient for FakeTdClient {
fn handle_update(&mut self, _update: Update) {}
}

View File

@@ -6,7 +6,4 @@ mod fake_tdclient_impl; // TdClientTrait implementation for FakeTdClient
pub mod snapshot_utils;
pub mod test_data;
pub use app_builder::TestAppBuilder;
pub use fake_tdclient::FakeTdClient;
pub use snapshot_utils::{buffer_to_string, render_to_buffer};
pub use test_data::{create_test_chat, create_test_message, create_test_user};

View File

@@ -219,20 +219,22 @@ impl TestMessageBuilder {
}
/// Хелперы для быстрого создания тестовых данных
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),

View File

@@ -8,7 +8,6 @@ use helpers::test_data::{
create_test_chat, create_test_profile, TestChatBuilder, TestMessageBuilder,
};
use insta::assert_snapshot;
use tele_tui::tdlib::TdClientTrait;
#[test]
fn snapshot_delete_confirmation_modal() {

View File

@@ -4,7 +4,6 @@ mod helpers;
use helpers::fake_tdclient::FakeTdClient;
use helpers::test_data::{create_test_chat, TestMessageBuilder};
use tele_tui::types::{ChatId, MessageId};
/// Test: Навигация вверх/вниз по списку чатов
#[tokio::test]
@@ -177,8 +176,7 @@ async fn test_russian_layout_navigation() {
selected_index -= 1;
assert_eq!(selected_index, 1);
// Проверяем что логика работает одинаково
assert!(true); // Реальный тест был бы в input handler
// Реальный end-to-end тест этого mapping живет в input handler.
}
/// Test: Подгрузка старых сообщений при скролле вверх

View File

@@ -5,7 +5,7 @@ mod helpers;
use helpers::fake_tdclient::FakeTdClient;
use helpers::test_data::create_test_chat;
use tele_tui::tdlib::ProfileInfo;
use tele_tui::types::{ChatId, MessageId};
use tele_tui::types::ChatId;
/// Test: Открытие профиля в личном чате (i)
#[tokio::test]
@@ -96,7 +96,7 @@ async fn test_profile_shows_channel_info() {
#[tokio::test]
async fn test_close_profile_with_esc() {
// Профиль открыт
let profile_mode = true;
let _profile_mode = true;
// Пользователь нажал Esc
let profile_mode = false;

View File

@@ -29,7 +29,7 @@ async fn test_add_reaction_to_message() {
assert_eq!(messages[0].reactions().len(), 1);
assert_eq!(messages[0].reactions()[0].emoji, "👍");
assert_eq!(messages[0].reactions()[0].count, 1);
assert_eq!(messages[0].reactions()[0].is_chosen, true);
assert!(messages[0].reactions()[0].is_chosen);
}
/// Test: Удаление реакции (toggle) - вторичное нажатие
@@ -47,7 +47,7 @@ async fn test_toggle_reaction_removes_it() {
// Проверяем что реакция есть
let messages_before = client.get_messages(123);
assert_eq!(messages_before[0].reactions().len(), 1);
assert_eq!(messages_before[0].reactions()[0].is_chosen, true);
assert!(messages_before[0].reactions()[0].is_chosen);
let msg_id = messages_before[0].id();
@@ -116,7 +116,7 @@ async fn test_reactions_from_multiple_users() {
assert_eq!(reaction.emoji, "👍");
assert_eq!(reaction.count, 3);
assert_eq!(reaction.is_chosen, false);
assert!(!reaction.is_chosen);
}
/// Test: Своя реакция (is_chosen = true)
@@ -134,7 +134,7 @@ async fn test_own_reaction_is_chosen() {
let messages = client.get_messages(123);
let reaction = &messages[0].reactions()[0];
assert_eq!(reaction.is_chosen, true);
assert!(reaction.is_chosen);
// В UI это будет отображаться в рамках: [❤️]
}
@@ -153,7 +153,7 @@ async fn test_other_reaction_not_chosen() {
let messages = client.get_messages(123);
let reaction = &messages[0].reactions()[0];
assert_eq!(reaction.is_chosen, false);
assert!(!reaction.is_chosen);
// В UI это будет отображаться без рамок: 😂 2
}
@@ -182,7 +182,7 @@ async fn test_reaction_counter_increases() {
let messages = client.get_messages(123);
assert_eq!(messages[0].reactions()[0].count, 2);
assert_eq!(messages[0].reactions()[0].is_chosen, true);
assert!(messages[0].reactions()[0].is_chosen);
}
/// Test: Обновление реакции - мы добавили свою к существующим
@@ -199,7 +199,7 @@ async fn test_update_reaction_we_add_ours() {
let messages_before = client.get_messages(123);
assert_eq!(messages_before[0].reactions()[0].count, 2);
assert_eq!(messages_before[0].reactions()[0].is_chosen, false);
assert!(!messages_before[0].reactions()[0].is_chosen);
let msg_id = messages_before[0].id();
@@ -213,7 +213,7 @@ async fn test_update_reaction_we_add_ours() {
let reaction = &messages[0].reactions()[0];
assert_eq!(reaction.count, 3);
assert_eq!(reaction.is_chosen, true);
assert!(reaction.is_chosen);
}
/// Test: Реакция с count=1 отображается только emoji
@@ -272,5 +272,5 @@ async fn test_reactions_on_multiple_messages() {
assert_eq!(messages[2].reactions().len(), 2);
assert_eq!(messages[2].reactions()[0].emoji, "😂");
assert_eq!(messages[2].reactions()[1].emoji, "🔥");
assert_eq!(messages[2].reactions()[1].is_chosen, true);
assert!(messages[2].reactions()[1].is_chosen);
}

View File

@@ -4,7 +4,6 @@ mod helpers;
use helpers::fake_tdclient::FakeTdClient;
use helpers::test_data::TestMessageBuilder;
use tele_tui::tdlib::types::ForwardInfo;
use tele_tui::tdlib::ReplyInfo;
use tele_tui::types::{ChatId, MessageId};

View File

@@ -4,7 +4,6 @@ mod helpers;
use helpers::fake_tdclient::FakeTdClient;
use helpers::test_data::{create_test_chat, TestChatBuilder, TestMessageBuilder};
use tele_tui::types::{ChatId, MessageId};
/// Test: Поиск по чатам фильтрует по названию
#[tokio::test]
@@ -213,7 +212,6 @@ async fn test_cancel_search_restores_normal_mode() {
let client = client.with_chats(vec![chat1, chat2]);
// Симулируем: пользователь начал поиск
let mut is_searching = true;
let mut search_query = "mom".to_string();
// Фильтруем
@@ -227,7 +225,7 @@ async fn test_cancel_search_restores_normal_mode() {
assert_eq!(filtered.len(), 1);
// Пользователь нажал Esc
is_searching = false;
let is_searching = false;
search_query.clear();
// После отмены видим все чаты

View File

@@ -30,7 +30,7 @@ async fn test_send_text_message() {
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].id(), msg.id());
assert_eq!(messages[0].text(), "Hello, Mom!");
assert_eq!(messages[0].is_outgoing(), true);
assert!(messages[0].is_outgoing());
}
/// Test: Отправка нескольких сообщений обновляет список
@@ -170,8 +170,8 @@ async fn test_receive_incoming_message() {
// Проверяем что в списке 2 сообщения
let messages = client.get_messages(123);
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].is_outgoing(), true); // Наше сообщение
assert_eq!(messages[1].is_outgoing(), false); // Входящее
assert!(messages[0].is_outgoing()); // Наше сообщение
assert!(!messages[1].is_outgoing()); // Входящее
assert_eq!(messages[1].text(), "Hey there!");
assert_eq!(messages[1].sender_name(), "Alice");
}