Проблема: - При открытии чата видно только последнее сообщение - TDLib возвращал 1 сообщение при первом запросе - Не было retry логики для ожидания синхронизации с сервером Решение: 1. Динамическая загрузка с retry (до 20 попыток на чанк) 2. Загрузка всей доступной истории (без лимита) 3. Retry при получении малого количества сообщений 4. Корректная чанковая загрузка по 50 сообщений Алгоритм: - При открытии чата: get_chat_history(i32::MAX) - загружает всё - Чанками по 50: TDLIB_MESSAGE_LIMIT - Retry если получено < 50 при первой загрузке - Остановка если 3 раза подряд пусто - Порядок: старые чанки вставляются в начало (splice) - При скролле: load_older_messages_if_needed() подгружает автоматически Изменения: src/tdlib/messages.rs: - Убрана фиксированная задержка 100ms после open_chat - Добавлен счетчик consecutive_empty_results - Retry логика без искусственных sleep() - Проверка: если получено мало - продолжить попытки src/input/main_input.rs: - limit: 100 → i32::MAX (без ограничений) - timeout: 10s → 30s tests/chat_list.rs: - test_chat_history_chunked_loading: проверка 100, 120, 200 сообщений - test_chat_history_loads_all_without_limit: загрузка 200 без лимита - test_load_older_messages_pagination: подгрузка при скролле Все тесты: 104/104 ✅ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
497 lines
17 KiB
Rust
497 lines
17 KiB
Rust
// 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() {
|
||
use tele_tui::tdlib::ChatInfo;
|
||
use tele_tui::types::ChatId;
|
||
|
||
// Создаём чат БЕЗ непрочитанных сообщений
|
||
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::tdlib::TdClientTrait;
|
||
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::tdlib::TdClientTrait;
|
||
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 mut 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::tdlib::TdClientTrait;
|
||
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 mut 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::tdlib::TdClientTrait;
|
||
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 mut 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::tdlib::TdClientTrait;
|
||
use tele_tui::types::{ChatId, MessageId};
|
||
|
||
// Создаём чат со 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 mut 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() {
|
||
use tele_tui::tdlib::UserOnlineStatus;
|
||
use tele_tui::types::ChatId;
|
||
|
||
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);
|
||
}
|
||
|