Refactor TDLib facade and local time handling
This commit is contained in:
@@ -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 ============
|
||||
|
||||
@@ -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!")
|
||||
|
||||
@@ -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 переменные
|
||||
|
||||
@@ -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 окружения
|
||||
|
||||
@@ -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: Удаление несуществующего сообщения (ничего не происходит)
|
||||
|
||||
@@ -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 поле
|
||||
|
||||
@@ -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 флаг
|
||||
|
||||
@@ -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
|
||||
/// Симулирует полный путь пользователя от запуска до загрузки чатов
|
||||
|
||||
@@ -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: Множественные редактирования одного сообщения
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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: Подгрузка старых сообщений при скролле вверх
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
// После отмены видим все чаты
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user