Files
telegram-tui/tests/chat_list.rs
Mikhail Kilin 72c4a886fa fix: implement dynamic message history loading with retry logic
Проблема:
- При открытии чата видно только последнее сообщение
- 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>
2026-02-04 03:56:58 +03:00

497 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}