This commit is contained in:
Mikhail Kilin
2026-01-28 11:39:21 +03:00
parent 051c4a0265
commit 68a2b7a982
56 changed files with 4424 additions and 5 deletions

5
.gitignore vendored
View File

@@ -10,3 +10,8 @@
# Local config files (if created in project root) # Local config files (if created in project root)
config.toml config.toml
credentials credentials
# Insta snapshot testing
# Commit snapshots, but not the .new files
tests/**/*.snap.new
*.snap.new

View File

@@ -1,6 +1,6 @@
# Текущий контекст проекта # Текущий контекст проекта
## Статус: Фаза 9 — ЗАВЕРШЕНО ## Статус: Фаза 9 — ЗАВЕРШЕНО + Тестирование (19%)
### Что сделано ### Что сделано
@@ -127,6 +127,7 @@
``` ```
src/ src/
├── main.rs # Точка входа, event loop, TDLib инициализация, graceful shutdown ├── main.rs # Точка входа, event loop, TDLib инициализация, graceful shutdown
├── lib.rs # Библиотечный интерфейс (для тестов)
├── config.rs # Конфигурация (TOML), загрузка credentials ├── config.rs # Конфигурация (TOML), загрузка credentials
├── app/ ├── app/
│ ├── mod.rs # App структура и состояние (needs_redraw флаг) │ ├── mod.rs # App структура и состояние (needs_redraw флаг)
@@ -147,8 +148,42 @@ src/
└── tdlib/ └── tdlib/
├── mod.rs # Модуль экспорта (TdClient, UserOnlineStatus, NetworkState) ├── mod.rs # Модуль экспорта (TdClient, UserOnlineStatus, NetworkState)
└── client.rs # TdClient: авторизация, чаты, сообщения, кеш, NetworkState, ReactionInfo └── client.rs # TdClient: авторизация, чаты, сообщения, кеш, NetworkState, ReactionInfo
tests/
├── helpers/
│ ├── mod.rs # Экспорт тестовых утилит
│ ├── app_builder.rs # TestAppBuilder для создания тестовых App
│ ├── fake_tdclient.rs # FakeTdClient (mock TDLib клиент, для будущих интеграционных тестов)
│ ├── snapshot_utils.rs # Утилиты для snapshot тестов (render_to_buffer, buffer_to_string)
│ └── test_data.rs # Builders для тестовых данных (TestChatBuilder, TestMessageBuilder)
├── chat_list.rs # Snapshot тесты для списка чатов (9 тестов)
└── messages.rs # Snapshot тесты для сообщений (19 тестов)
``` ```
### Тестирование
**Статус**: В процессе (19% завершено)
**Стратегия**: Комбо подход — 70% snapshot tests, 25% integration tests, 5% e2e smoke tests
**Инфраструктура (Фаза 0)**: ✅ Завершена
- Добавлены зависимости: `insta = "1.34"`, `tokio-test = "0.4"`
- Создан `src/lib.rs` для экспорта модулей в тесты
- Созданы test helpers:
- `TestAppBuilder` — fluent builder для создания тестовых App
- `TestChatBuilder` / `TestMessageBuilder` — builders для тестовых данных
- `FakeTdClient` — in-memory mock TDLib клиента
- `render_to_buffer` / `buffer_to_string` — утилиты для snapshot тестов
**Snapshot Tests (Фаза 1)**: 28/57 (49%)
-**1.1 Chat List** (9/10): пустой список, множественные чаты, unread, pinned, muted, mentions, selected, long title, search mode
-**1.2 Messages** (19/19): empty chat, incoming/outgoing, date separators, sender grouping, read receipts, edited, long message wrap, markdown (bold/italic/code/links/mentions/spoiler), media placeholder, reply, forwarded, reactions (single/multiple), selected message
- [ ] **1.3-1.6**: Modals, Input Field, Footer, Screens (0/29)
**Прогресс**: 28/151 тестов (19%)
Подробный план и roadmap: см. [TESTING_ROADMAP.md](TESTING_ROADMAP.md)
### Ключевые решения ### Ключевые решения
1. **Неблокирующий receive**: TDLib updates приходят в отдельном потоке и передаются в main loop через `mpsc::channel`. Это позволяет UI оставаться отзывчивым. 1. **Неблокирующий receive**: TDLib updates приходят в отдельном потоке и передаются в main loop через `mpsc::channel`. Это позволяет UI оставаться отзывчивым.
@@ -236,9 +271,39 @@ reaction_chosen = "yellow"
reaction_other = "gray" reaction_other = "gray"
``` ```
## Последние обновления (2026-01-28)
### Тестирование — Фаза 1.3 завершена
**Добавлено**:
- 📝 8 snapshot тестов для модальных окон (`tests/modals.rs`)
- 🔧 Обновлён `TestAppBuilder` с методами: `with_chats()`, `message_search()`, `forward_mode()`
- 🐛 Исправлены нестабильные date separator тесты (заменены на фиксированную дату)
- 📚 Обновлена документация тестирования
**Покрытие**: 35/151 тестов (23%)
- ✅ Chat List: 9 тестов
- ✅ Messages: 18 тестов (empty chat, incoming/outgoing, date separators, grouping, read receipts, editing, wrap, markdown, media, reply, forward, reactions, selection)
- ✅ Modals: 8 тестов (delete confirmation, emoji picker x2, profile x2, pinned message, search, forward mode)
**Все тесты проходят**: `cargo test` → 71 passed ✅
Подробности: [TESTING_PROGRESS.md](TESTING_PROGRESS.md)
## Что НЕ сделано / TODO ## Что НЕ сделано / TODO
Все пункты Фазы 9 завершены! Можно переходить к следующей фазе разработки. Все пункты Фазы 9 завершены! Можно переходить к следующей фазе разработки или продолжить написание тестов.
## Технический долг
См. [REFACTORING_ROADMAP.md](REFACTORING_ROADMAP.md) для детального плана рефакторинга.
Основные области для улучшения:
1. **ChatState enum** — схлопнуть boolean состояния в type-safe enum
2. **Разделение TdClient** — слишком много ответственности в одном модуле
3. **Типобезопасность** — newtype pattern для ID, error enum
4. **UI компоненты** — выделить переиспользуемые компоненты
5. **Тестирование** — добавить юнит-тесты для критичных функций
## Известные проблемы ## Известные проблемы

60
Cargo.lock generated
View File

@@ -225,6 +225,18 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "constant_time_eq" name = "constant_time_eq"
version = "0.3.1" version = "0.3.1"
@@ -515,6 +527,12 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -1108,6 +1126,18 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "insta"
version = "1.46.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248b42847813a1550dafd15296fd9748c651d0c32194559dbc05d804d54b21e8"
dependencies = [
"console",
"once_cell",
"similar",
"tempfile",
]
[[package]] [[package]]
name = "instability" name = "instability"
version = "0.3.11" version = "0.3.11"
@@ -2032,6 +2062,12 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.11" version = "0.4.11"
@@ -2195,12 +2231,14 @@ dependencies = [
"crossterm", "crossterm",
"dirs 5.0.1", "dirs 5.0.1",
"dotenvy", "dotenvy",
"insta",
"open", "open",
"ratatui", "ratatui",
"serde", "serde",
"serde_json", "serde_json",
"tdlib-rs", "tdlib-rs",
"tokio", "tokio",
"tokio-test",
"toml", "toml",
] ]
@@ -2360,6 +2398,28 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-stream"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-test"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545"
dependencies = [
"futures-core",
"tokio",
"tokio-stream",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.18" version = "0.7.18"

View File

@@ -23,5 +23,9 @@ arboard = "3.4"
toml = "0.8" toml = "0.8"
dirs = "5.0" dirs = "5.0"
[dev-dependencies]
insta = "1.34"
tokio-test = "0.4"
[build-dependencies] [build-dependencies]
tdlib-rs = { version = "1.1", features = ["download-tdlib"] } tdlib-rs = { version = "1.1", features = ["download-tdlib"] }

View File

@@ -223,6 +223,8 @@ XDG config directory:
- **DEVELOPMENT.md** — правила разработки - **DEVELOPMENT.md** — правила разработки
- **PROJECT_STRUCTURE.md** — этот файл - **PROJECT_STRUCTURE.md** — этот файл
- **ROADMAP.md** — план развития - **ROADMAP.md** — план развития
- **REFACTORING_ROADMAP.md** — план рефакторинга
- **TESTING_ROADMAP.md** — план покрытия тестами
- **CONTEXT.md** — текущий статус, архитектурные решения - **CONTEXT.md** — текущий статус, архитектурные решения
### Спецификации ### Спецификации

View File

@@ -144,6 +144,97 @@ src/
- `clipboard` 0.5 — clipboard access - `clipboard` 0.5 — clipboard access
- `chrono` 0.4 — date/time formatting - `chrono` 0.4 — date/time formatting
## Тестирование
tele-tui использует **snapshot тестирование** для UI и интеграционные тесты для логики.
### Запуск всех тестов
```bash
cargo test
```
### Snapshot тесты
Snapshot тесты проверяют отображение UI компонентов через виртуальный терминал:
```bash
# Прогнать snapshot тесты
cargo test --test chat_list
cargo test --test messages
# Посмотреть изменения в snapshots
cargo insta review
# Принять все новые snapshots
cargo insta accept
# Отклонить все изменения
cargo insta reject
```
### Установка cargo-insta
Для работы со snapshot тестами нужен `cargo-insta`:
```bash
cargo install cargo-insta
```
### Структура тестов
```
tests/
├── helpers/ # Тестовые утилиты
│ ├── app_builder.rs # TestAppBuilder для создания тестовых App
│ ├── test_data.rs # Builders для чатов и сообщений
│ ├── snapshot_utils.rs # Утилиты для snapshot тестов
│ └── fake_tdclient.rs # Mock TDLib клиент (для будущих integration тестов)
├── chat_list.rs # Snapshot тесты для списка чатов (9 тестов)
└── messages.rs # Snapshot тесты для сообщений (19 тестов)
```
### Создание snapshot теста
```rust
use helpers::test_data::TestChatBuilder;
use helpers::app_builder::TestAppBuilder;
use helpers::snapshot_utils::{render_to_buffer, buffer_to_string};
use insta::assert_snapshot;
#[test]
fn snapshot_my_feature() {
let chat = TestChatBuilder::new("Test Chat", 123)
.unread_count(5)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.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!("my_feature", output);
}
```
### Покрытие тестами
**Текущий прогресс**: 35/151 тестов (23%)
- ✅ Фаза 0: Инфраструктура (100%)
- ✅ Фаза 1.1: Chat List snapshots (90%)
- ✅ Фаза 1.2: Messages snapshots (95%)
- ✅ Фаза 1.3: Modals snapshots (100%)
- 🔄 Фаза 1.4-1.6: Input, Footer, Screens (0%)
- 📋 Фаза 2: Integration тесты для логики (0%)
Подробный план: [TESTING_ROADMAP.md](TESTING_ROADMAP.md)
## Документация ## Документация
- [INSTALL.md](INSTALL.md) — подробная инструкция по установке - [INSTALL.md](INSTALL.md) — подробная инструкция по установке
@@ -156,6 +247,9 @@ src/
- [REQUIREMENTS.md](REQUIREMENTS.md) — функциональные требования - [REQUIREMENTS.md](REQUIREMENTS.md) — функциональные требования
- [DEVELOPMENT.md](DEVELOPMENT.md) — правила разработки - [DEVELOPMENT.md](DEVELOPMENT.md) — правила разработки
- [ROADMAP.md](ROADMAP.md) — план развития проекта - [ROADMAP.md](ROADMAP.md) — план развития проекта
- [REFACTORING_ROADMAP.md](REFACTORING_ROADMAP.md) — план рефакторинга кода
- [TESTING_ROADMAP.md](TESTING_ROADMAP.md) — план покрытия тестами
- [TESTING_PROGRESS.md](TESTING_PROGRESS.md) — прогресс тестирования
- [CONTEXT.md](CONTEXT.md) — текущий статус разработки - [CONTEXT.md](CONTEXT.md) — текущий статус разработки
## Лицензия ## Лицензия

664
REFACTORING_ROADMAP.md Normal file
View File

@@ -0,0 +1,664 @@
# Refactoring Roadmap
Этот документ содержит список технического долга и планов по рефакторингу кодовой базы.
## Приоритет 1: Критичные улучшения
### 1. Схлопнуть состояния чата в enum
**Проблема**: Сейчас состояния чата хранятся как отдельные boolean поля в `App`:
```rust
is_message_selection_mode: bool,
is_editing_mode: bool,
is_reply_mode: bool,
is_forward_mode: bool,
is_delete_confirmation: bool,
is_reaction_picker_mode: bool,
is_profile_mode: bool,
is_search_in_chat_mode: bool,
```
**Решение**: Создать enum `ChatState`:
```rust
enum ChatState {
Normal,
MessageSelection {
selected_message_id: i64,
},
Editing {
message_id: i64,
original_text: String,
},
Reply {
message_id: i64,
preview_text: String,
},
Forward {
message_id: i64,
selected_chat_index: usize,
},
DeleteConfirmation {
message_id: i64,
},
ReactionPicker {
message_id: i64,
available_reactions: Vec<String>,
selected_index: usize,
},
Profile {
info: ProfileInfo,
},
SearchInChat {
query: String,
results: Vec<i64>,
current_index: usize,
},
}
```
**Преимущества**:
- Невозможно иметь несколько состояний одновременно (type-safe)
- Проще обрабатывать переходы между состояниями
- Меньше полей в `App`
- Данные, связанные с состоянием, хранятся вместе с ним
**Затронутые файлы**:
- `src/app/mod.rs` (добавить enum, убрать boolean поля)
- `src/input/main_input.rs` (изменить логику обработки на match)
- `src/ui/messages.rs` (изменить рендеринг на match)
---
### 2. Разделить TdClient на несколько модулей
**Проблема**: `TdClient` в `src/tdlib/client.rs` (~1500+ строк) делает слишком много:
- Авторизация
- Управление чатами
- Управление сообщениями
- Кеширование пользователей
- Реакции
- Network state
**Решение**: Разделить на модули:
```
src/tdlib/
├── mod.rs # Экспорт публичных типов
├── client.rs # Основной TdClient
├── auth.rs # AuthManager
├── chats.rs # ChatManager
├── messages.rs # MessageManager
├── users.rs # UserCache
└── reactions.rs # ReactionManager
```
**Преимущества**:
- Принцип единственной ответственности
- Проще тестировать отдельные модули
- Легче найти и изменить код
---
### 3. Вынести константы в отдельный модуль
**Проблема**: Магические числа разбросаны по всему коду:
```rust
// В разных местах:
500 // MAX_MESSAGES_IN_CHAT
500 // MAX_USER_CACHE_SIZE
200 // MAX_CHATS
8 // Emoji picker columns
10 // Max input height
16 // Poll timeout (60 FPS)
```
**Решение**: Создать `src/constants.rs`:
```rust
// Memory limits
pub const MAX_MESSAGES_IN_CHAT: usize = 500;
pub const MAX_USER_CACHE_SIZE: usize = 500;
pub const MAX_CHATS: usize = 200;
pub const MAX_CHAT_USER_IDS: usize = 500;
// UI constants
pub const EMOJI_PICKER_COLUMNS: usize = 8;
pub const EMOJI_PICKER_ROWS: usize = 6;
pub const MAX_INPUT_HEIGHT: usize = 10;
pub const MIN_TERMINAL_WIDTH: u16 = 80;
pub const MIN_TERMINAL_HEIGHT: u16 = 20;
// Performance
pub const POLL_TIMEOUT_MS: u64 = 16; // 60 FPS
pub const SHUTDOWN_TIMEOUT_SECS: u64 = 2;
pub const LAZY_LOAD_USERS_PER_TICK: usize = 5;
// TDLib
pub const TDLIB_CHAT_LIMIT: i32 = 50;
pub const TDLIB_MESSAGE_LIMIT: i32 = 50;
```
**Преимущества**:
- Единое место для всех констант
- Проще изменить значения
- Самодокументирующийся код
---
## Приоритет 2: Улучшение типобезопасности
### 4. Newtype pattern для ID
**Проблема**: Везде используется `i64` для `chat_id`, `message_id`, `user_id` — легко перепутать.
**Решение**: Создать `src/types.rs`:
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChatId(pub i64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MessageId(pub i64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UserId(pub i64);
impl From<i64> for ChatId {
fn from(id: i64) -> Self {
ChatId(id)
}
}
// Аналогично для MessageId и UserId
```
**Преимущества**:
- Невозможно случайно передать message_id вместо chat_id
- Компилятор поймает ошибки
- Улучшенная читаемость
---
### 5. Создать enum для ошибок
**Проблема**: Везде используется `Result<T, String>` — теряется контекст ошибок.
**Решение**: Создать `src/error.rs`:
```rust
#[derive(Debug, thiserror::Error)]
pub enum TeletuiError {
#[error("TDLib error: {0}")]
TdLib(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Network error: {0}")]
Network(String),
#[error("Authentication error: {0}")]
Auth(String),
#[error("Invalid timezone format: {0}")]
InvalidTimezone(String),
#[error("Invalid color: {0}")]
InvalidColor(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, TeletuiError>;
```
**Зависимости**: `thiserror = "1.0"`
**Преимущества**:
- Типобезопасная обработка ошибок
- Понятные сообщения об ошибках
- Возможность pattern matching
---
### 6. Группировка полей MessageInfo
**Проблема**: `MessageInfo` имеет слишком много плоских полей (~15+).
**Решение**: Группировать в логические структуры:
```rust
pub struct MessageInfo {
pub metadata: MessageMetadata,
pub content: MessageContent,
pub state: MessageState,
pub interactions: MessageInteractions,
}
pub struct MessageMetadata {
pub id: MessageId,
pub chat_id: ChatId,
pub sender_id: UserId,
pub date: i32,
}
pub struct MessageContent {
pub text: String,
pub formatted_text: Option<FormattedText>,
pub media_type: Option<String>,
}
pub struct MessageState {
pub is_outgoing: bool,
pub is_edited: bool,
pub is_pinned: bool,
}
pub struct MessageInteractions {
pub reply_to_message_id: Option<MessageId>,
pub forward_info: Option<ForwardInfo>,
pub reactions: Vec<ReactionInfo>,
pub read_count: i32,
}
```
**Преимущества**:
- Логическая группировка данных
- Проще добавлять новые поля
- Меньше параметров в конструкторах
---
## Приоритет 3: Архитектурные улучшения
### 7. Выделить UI компоненты
**Проблема**: Код рендеринга дублируется, сложно переиспользовать.
**Решение**: Создать `src/ui/components/`:
```
src/ui/components/
├── mod.rs
├── modal.rs # Базовый компонент модалки
├── input_field.rs # Поле ввода с курсором
├── message_bubble.rs # Пузырь сообщения
├── chat_list_item.rs # Элемент списка чатов
└── emoji_picker.rs # Picker эмодзи
```
Каждый компонент — функция:
```rust
pub fn render_modal<F>(
frame: &mut Frame,
area: Rect,
title: &str,
render_content: F,
) where
F: FnOnce(&mut Frame, Rect),
{
// Общий код для всех модалок
}
```
**Преимущества**:
- Переиспользуемые компоненты
- Консистентный UI
- Проще тестировать
---
### 8. Вынести форматирование в отдельный модуль
**Проблема**: Markdown форматирование захардкожено в `messages.rs` (~200+ строк).
**Решение**: Создать `src/formatting.rs`:
```rust
pub struct FormattedSpan {
pub text: String,
pub style: Style,
}
pub fn format_text_entities(
text: &str,
entities: &[TextEntity],
) -> Vec<FormattedSpan> {
// Вся логика форматирования
}
```
**Преимущества**:
- Разделение ответственности
- Можно тестировать отдельно
- Переиспользование в других местах
---
### 9. Вынести логику группировки сообщений
**Проблема**: Логика группировки сообщений смешана с рендерингом в `messages.rs`.
**Решение**: Создать `src/message_grouping.rs`:
```rust
pub enum MessageGroup {
DateSeparator(String),
SenderHeader(String),
Message(MessageInfo),
}
pub fn group_messages(messages: &[MessageInfo]) -> Vec<MessageGroup> {
// Логика группировки по дате и отправителю
}
```
**Преимущества**:
- Чистое разделение логики и представления
- Легче тестировать группировку
- Можно переиспользовать
---
### 10. Hotkey mapping в конфиг
**Проблема**: Хоткеи захардкожены в коде, нельзя настроить.
**Решение**: Добавить в `config.toml`:
```toml
[hotkeys]
# Навигация
up = ["k", "р", "Up"]
down = ["j", "о", "Down"]
left = ["h", "р", "Left"]
right = ["l", "д", "Right"]
# Действия
reply = ["r", "к"]
forward = ["f", "а"]
delete = ["d", "в", "Delete"]
copy = ["y", "н"]
react = ["e", "у"]
```
Парсить в `src/config.rs`:
```rust
pub struct Hotkeys {
pub up: Vec<char>,
pub down: Vec<char>,
// ...
}
impl Hotkeys {
pub fn matches(&self, key: KeyCode, action: &str) -> bool {
// Проверка совпадения
}
}
```
**Преимущества**:
- Пользовательская настройка хоткеев
- Проще добавлять новые действия
- Документация хоткеев в конфиге
---
## Приоритет 4: Качество кода
### 11. Добавить юнит-тесты
**Проблема**: Нет тестов, сложно убедиться в корректности.
**Решение**: Добавить тесты для:
```rust
// tests/utils_test.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_timestamp_with_tz() {
let timestamp = 1640000000; // 2021-12-20 09:33:20 UTC
assert_eq!(
format_timestamp_with_tz(timestamp, "+03:00"),
"12:33"
);
}
#[test]
fn test_parse_timezone_offset() {
assert_eq!(parse_timezone_offset("+03:00"), 3);
assert_eq!(parse_timezone_offset("-05:00"), -5);
assert_eq!(parse_timezone_offset("invalid"), 3); // fallback
}
}
// tests/config_test.rs
#[test]
fn test_parse_color() {
let config = Config::default();
assert_eq!(config.parse_color("red"), Color::Red);
assert_eq!(config.parse_color("invalid"), Color::White); // fallback
}
// tests/grouping_test.rs
#[test]
fn test_message_grouping_by_date() {
// ...
}
```
**Запуск**: `cargo test`
---
### 12. Добавить rustdoc комментарии
**Проблема**: Публичное API не документировано.
**Решение**: Добавить doc-комментарии:
```rust
/// TDLib client wrapper for Telegram integration.
///
/// Handles authentication, chat management, message operations,
/// and user caching.
///
/// # Examples
///
/// ```no_run
/// let mut client = TdClient::new(api_id, api_hash).await?;
/// client.start_authorization().await?;
/// ```
pub struct TdClient {
// ...
}
/// Loads configuration from ~/.config/tele-tui/config.toml
///
/// Creates default config if file doesn't exist.
///
/// # Returns
///
/// Always returns a valid `Config`, using defaults if loading fails.
pub fn load() -> Self {
// ...
}
```
**Генерация**: `cargo doc --open`
---
### 13. Config валидация
**Проблема**: Невалидные значения в конфиге молча игнорируются.
**Решение**: Добавить валидацию:
```rust
impl Config {
pub fn validate(&self) -> Result<(), TeletuiError> {
// Проверка timezone
if !self.general.timezone.starts_with('+')
&& !self.general.timezone.starts_with('-') {
return Err(TeletuiError::InvalidTimezone(
format!("Timezone must start with + or -: {}", self.general.timezone)
));
}
// Проверка цветов
let valid_colors = [
"black", "red", "green", "yellow", "blue", "magenta",
"cyan", "gray", "white", "darkgray", "lightred",
"lightgreen", "lightyellow", "lightblue",
"lightmagenta", "lightcyan"
];
for color_name in [
&self.colors.incoming_message,
&self.colors.outgoing_message,
&self.colors.selected_message,
&self.colors.reaction_chosen,
&self.colors.reaction_other,
] {
if !valid_colors.contains(&color_name.to_lowercase().as_str()) {
return Err(TeletuiError::InvalidColor(
format!("Unknown color: {}", color_name)
));
}
}
Ok(())
}
}
```
Вызывать при загрузке:
```rust
pub fn load() -> Self {
let config = // ... загрузка из файла
if let Err(e) = config.validate() {
eprintln!("Config validation error: {}", e);
return Self::default();
}
config
}
```
---
### 14. Async/await консистентность
**Проблема**: Местами блокирующие вызовы в async контексте.
**Решение**: Ревью и исправление:
- Использовать `tokio::fs` вместо `std::fs` для файловых операций в async
- Использовать `tokio::time::sleep` вместо `std::thread::sleep`
- Обернуть блокирующие вызовы в `spawn_blocking`
---
## Приоритет 5: Опциональные улучшения
### 15. Feature flags для зависимостей
**Проблема**: Все зависимости всегда включены.
**Решение**: В `Cargo.toml`:
```toml
[features]
default = ["clipboard", "url-open"]
clipboard = ["dep:arboard"]
url-open = ["dep:open"]
```
**Преимущества**:
- Уменьшение размера бинарника
- Опциональная функциональность
---
### 16. LRU cache обобщение
**Проблема**: Отдельные LRU кеши для `user_names` и `user_statuses`.
**Решение**: Создать обобщённый `LruCache<K, V>` или использовать готовый крейт `lru = "0.12"`.
---
### 17. Tracing вместо println!
**Проблема**: Используется `eprintln!` для логов.
**Решение**: Использовать `tracing`:
```rust
use tracing::{info, warn, error, debug};
// Вместо
eprintln!("Warning: Could not load config: {}", e);
// Использовать
warn!("Could not load config: {}", e);
```
Добавить в `Cargo.toml`:
```toml
tracing = "0.1"
tracing-subscriber = "0.3"
```
---
## Метрики прогресса
- [ ] Priority 1: 0/3 задач
- [ ] Priority 2: 0/3 задач
- [ ] Priority 3: 0/4 задач
- [ ] Priority 4: 0/4 задач
- [ ] Priority 5: 0/3 задач
**Всего**: 0/17 задач
---
## Предусловие: Тесты
**ВАЖНО**: Перед началом рефакторинга необходимо написать тесты!
См. [TESTING_ROADMAP.md](TESTING_ROADMAP.md) для плана покрытия тестами.
Минимальное покрытие для начала рефакторинга:
- ✅ Фаза 0: Инфраструктура (helpers, fake client)
- ✅ Snapshot тесты для основных экранов (chat list, messages)
- ✅ Integration тесты для критичных flow (send, edit, navigation)
**Зачем**: Тесты гарантируют, что рефакторинг не сломает функциональность.
---
## Порядок выполнения
Рекомендуется выполнять в следующем порядке:
1. **P1.3** — Константы (быстро, малый риск)
2. **P1.1** — ChatState enum (высокий impact)
3. **P2.5** — Error enum (улучшает весь код)
4. **P4.11** — Тесты для utils (базовая проверка)
5. **P1.2** — Разделить TdClient (большой рефакторинг)
6. **P2.4** — Newtype для ID (широкие изменения)
7. **P3.7** — UI компоненты (постепенно)
8. **P3.8** — Форматирование (изоляция логики)
9. **P3.9** — Группировка сообщений (изоляция логики)
10. Остальные по необходимости
---
## Принципы рефакторинга
1. **Один PR = одна задача** — не смешивать рефакторинг разных областей
2. **Тесты прежде всего** — добавить тесты перед рефакторингом
3. **Обратная совместимость** — сохранять работоспособность на каждом шаге
4. **Маленькие шаги** — лучше 10 маленьких PR, чем 1 огромный
5. **Документация** — обновлять документацию после изменений
---
## Примечания
- Этот документ живой и будет обновляться
- Новые пункты добавляются по мере обнаружения
- После завершения задачи отмечать в метриках
- При появлении блокеров — документировать в соответствующей секции

291
TESTING_PROGRESS.md Normal file
View File

@@ -0,0 +1,291 @@
# Testing Progress Report
## Текущий статус: Фаза 1.3 завершена! 🎉
Дата: 2026-01-28 (обновлено #2)
---
## ✅ Что сделано
### Фаза 1.3: Modals Snapshot Tests (100%) ✅
**Файл**: `tests/modals.rs` (8 тестов)
#### Snapshot тесты для модальных окон:
-`snapshot_delete_confirmation_modal` — модалка подтверждения удаления
-`snapshot_emoji_picker_default` — emoji picker с дефолтным выбором
-`snapshot_emoji_picker_with_selection` — emoji picker с выбранной реакцией (курсор)
-`snapshot_profile_personal_chat` — профиль личного чата
-`snapshot_profile_group_chat` — профиль группы (с участниками)
-`snapshot_pinned_message` — закреплённое сообщение вверху чата
-`snapshot_search_in_chat` — поиск в чате с результатами
-`snapshot_forward_mode` — режим пересылки (выбор чата)
#### Обновления TestAppBuilder:
- ✅ Добавлен метод `with_chats(chats)` — добавить несколько чатов сразу
- ✅ Добавлен метод `message_search(query)` — режим поиска по сообщениям
- ✅ Добавлен метод `forward_mode(message_id)` — режим пересылки
- ✅ Добавлены поля: `message_search_mode`, `message_search_query`, `forwarding_message_id`, `is_selecting_forward_chat`
#### Исправления:
- ✅ Переименованы тесты с динамическими датами (today/yesterday) на фиксированный old_date
- ✅ Удалены нестабильные snapshots зависящие от текущей даты
-Все модальные режимы теперь тестируются через snapshots
#### Результаты:
- **8 новых snapshot тестов** — все проходят ✅
- **8 snapshots приняты** через `cargo insta accept`
- **Все тесты проходят**: 71 тест (21 chat_list + 30 messages + 20 modals)
---
### Фаза 1.2: Messages Snapshot Tests (95%) ✅
**Файл**: `tests/messages.rs` (19 тестов)
#### Snapshot тесты для области сообщений:
-`snapshot_empty_chat` — пустой чат без сообщений
-`snapshot_single_incoming_message` — одно входящее сообщение
-`snapshot_single_outgoing_message` — одно исходящее сообщение
-`snapshot_date_separator_today` — разделитель "Сегодня"
-`snapshot_date_separator_yesterday` — разделитель "Вчера"
-`snapshot_sender_grouping` — группировка по отправителю (Alice → Alice → Bob)
-`snapshot_outgoing_sent` — исходящее с ✓ (отправлено)
-`snapshot_outgoing_read` — исходящее с ✓✓ (прочитано)
-`snapshot_edited_message` — сообщение с индикатором ✎
-`snapshot_long_message_wrap` — длинное сообщение с переносом
-`snapshot_markdown_bold_italic_code`**bold** *italic* `code`
-`snapshot_markdown_link_mention` — [links](url) и @mentions
-`snapshot_markdown_spoiler` — ||спойлер||
-`snapshot_media_placeholder` — [Фото], [Видео] и т.д.
-`snapshot_reply_message` — reply с превью оригинала
-`snapshot_forwarded_message` — ↪ Переслано от Alice
-`snapshot_single_reaction` — сообщение с одной реакцией [👍]
-`snapshot_multiple_reactions` — [👍] 5 👎 3
-`snapshot_selected_message` — выбранное сообщение (подсветка)
#### Обновления TestAppBuilder:
- ✅ Добавлен метод `with_message(chat_id, message)` — добавить одно сообщение
- ✅ Добавлен метод `with_messages(chat_id, messages)` — добавить несколько сообщений
- ✅ Добавлен метод `selecting_message(index)` — установить выбранное сообщение
- ✅ Обновлен `build()` — применяет сообщения к `app.td_client.current_chat_messages`
#### Результаты:
- **19 новых snapshot тестов** — все проходят ✅
- **19 snapshots приняты** через `cargo insta accept`
- **Все тесты проходят**: 52 теста (21 chat_list + 31 messages)
---
### Фаза 0: Инфраструктура (100%)
#### 1. Зависимости
- ✅ Добавлено `insta = "1.34"` для snapshot тестов
- ✅ Добавлено `tokio-test = "0.4"` для async тестов
- ✅ Настроен `.gitignore` для `.snap.new` файлов
#### 2. Test Helpers (5 модулей)
**`tests/helpers/mod.rs`**
- Экспортирует все вспомогательные модули
- Удобный доступ к TestAppBuilder, FakeTdClient и утилитам
**`tests/helpers/test_data.rs`**
-`TestChatBuilder` — fluent API для создания тестовых чатов
-`TestMessageBuilder` — fluent API для создания тестовых сообщений
- ✅ Хелперы: `create_test_chat()`, `create_test_message()`, `create_test_user()`
- ✅ Поддержка всех полей: unread, pinned, muted, mentions, reactions, reply, forward
**`tests/helpers/fake_tdclient.rs`**
-`FakeTdClient` — in-memory мок для интеграционных тестов
- ✅ Методы: `send_message()`, `edit_message()`, `delete_message()`, `add_reaction()`
- ✅ Tracking отправленных/отредактированных/удалённых сообщений
- ✅ Fluent API для построения клиента с данными
- ✅ Встроенные юнит-тесты для проверки мока
**`tests/helpers/snapshot_utils.rs`**
-`buffer_to_string()` — конвертация ratatui Buffer в строку для snapshots
-`render_to_buffer()` — рендеринг UI в виртуальный терминал
-`assert_ui_snapshot!` макрос для упрощения snapshot тестов
- ✅ Удаление trailing spaces для чистых snapshots
- ✅ Встроенные тесты
**`tests/helpers/app_builder.rs`**
-`TestAppBuilder` — fluent API для создания тестового App
- ✅ Методы: `with_chat()`, `selected_chat()`, `message_input()`, `searching()`, etc.
- ✅ Поддержка всех режимов: edit, reply, search, reaction_picker, profile
- ✅ Встроенные тесты для билдера
#### 3. Первые UI тесты
**`tests/ui/chat_list_test.rs`** (9 тестов)
- ✅ snapshot_empty_chat_list
- ✅ snapshot_chat_list_with_three_chats
- ✅ snapshot_chat_with_unread_count
- ✅ snapshot_chat_with_pinned
- ✅ snapshot_chat_with_muted
- ✅ snapshot_chat_with_mentions
- ✅ snapshot_selected_chat
- ✅ snapshot_chat_long_title
- ✅ snapshot_chat_search_mode
---
## 📊 Метрики
**Создано файлов**: 10
- 5 helpers
- 4 test files (chat_list.rs, messages.rs, modals.rs)
- 1 mod.rs
**Строк кода**: ~2200+
- test_data.rs: ~250 строк
- fake_tdclient.rs: ~300 строк
- snapshot_utils.rs: ~100 строк
- app_builder.rs: ~280 строк (обновлён)
- chat_list.rs: ~150 строк
- messages.rs: ~430 строк (обновлён)
- modals.rs: ~220 строк
**Тестов написано**: 35 snapshot + 12 helper = 47 тестов
- All tests: 71 (включая helper tests internal)
**Покрытие**:
- Фаза 0: 8/8 ✅ (100%)
- Фаза 1.1: 9/10 (90%)
- Фаза 1.2: 18/19 (95%) ✅
- Фаза 1.3: 8/8 ✅ (100%)
- **Общий прогресс: 35/151 (23%)**
---
## 🏗️ Структура
```
tests/
├── helpers/
│ ├── mod.rs ✅ Создан
│ ├── app_builder.rs ✅ Создан + 5 тестов
│ ├── fake_tdclient.rs ✅ Создан + 4 теста
│ ├── snapshot_utils.rs ✅ Создан + 2 теста
│ └── test_data.rs ✅ Создан
└── ui/
├── mod.rs ✅ Создан
└── chat_list_test.rs ✅ Создан (9 snapshot тестов)
```
---
## 🎯 Примеры использования
### Создание тестового чата
```rust
let chat = TestChatBuilder::new("Mom", 123)
.unread_count(5)
.pinned()
.muted()
.draft("Hello...")
.build();
```
### Создание тестового App
```rust
let app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(123)
.message_input("Hello!")
.build();
```
### Snapshot тест
```rust
#[test]
fn snapshot_my_ui() {
let app = TestAppBuilder::new()
.with_chat(create_test_chat("Mom", 123))
.build();
let buffer = render_to_buffer(80, 24, |f| {
render_chat_list(f, f.size(), &app);
});
assert_snapshot!("my_ui", buffer_to_string(&buffer));
}
```
### Мок клиент для интеграционных тестов
```rust
let mut client = FakeTdClient::new()
.with_chat(create_test_chat("Mom", 123));
let msg_id = client.send_message(123, "Hello".to_string(), None);
assert_eq!(client.sent_messages().len(), 1);
```
---
## 🚀 Следующие шаги
### Фаза 1.4: Input Field snapshots (7 тестов)
- [ ] Пустое поле ввода
- [ ] Поле ввода с текстом и курсором █
- [ ] Поле ввода с длинным текстом (2 строки)
- [ ] Поле ввода с длинным текстом (10 строк)
- [ ] Режим редактирования (с превью)
- [ ] Режим reply (с превью сообщения)
- [ ] Режим поиска (с query)
### Фаза 2: Integration тесты
После завершения всех snapshot тестов начать писать интеграционные тесты для логики.
---
## 💡 Технические заметки
### Текущие ограничения
1. **TestAppBuilder создаёт реальный TdClient** — подходит только для UI/snapshot тестов
2. **Для интеграционных тестов** понадобится рефакторинг: либо trait для TdClient, либо dependency injection
### Решения
- Snapshot тесты используют TestAppBuilder (UI рендеринг без вызова TdClient методов)
- Интеграционные тесты будут использовать FakeTdClient напрямую
- Возможно потребуется создать `IntegrationTestSession` для комплексных сценариев
---
## ✨ Качество кода
**Все helpers покрыты тестами**:
- `app_builder.rs`: 5 тестов
- `fake_tdclient.rs`: 4 теста
- `snapshot_utils.rs`: 2 теста
**Документация**:
- Все публичные функции имеют doc-комментарии
- Примеры использования в комментариях
- README-секция в TESTING_ROADMAP.md
---
## 🎓 Что изучили
1. **Snapshot testing** с insta — мощный инструмент для TUI
2. **ratatui::backend::TestBackend** — виртуальный терминал для тестов
3. **Fluent builder pattern** — удобно для построения тестовых данных
4. **Test helpers organization** — разделение на модули для переиспользования
---
## 📝 Обновлённые файлы
- `Cargo.toml` — добавлены dev-dependencies
- `.gitignore` — добавлены правила для snapshots
- `TESTING_ROADMAP.md` — обновлён прогресс
- `README.md` — добавлена ссылка на TESTING_ROADMAP
- `REFACTORING_ROADMAP.md` — добавлено предусловие о тестах
---
**Статус**: Готов к продолжению! 🚀
**Следующий шаг**: Запустить тесты и убедиться что всё компилируется, затем продолжить с Фазы 1.2

569
TESTING_ROADMAP.md Normal file
View File

@@ -0,0 +1,569 @@
# Testing Roadmap
План покрытия tele-tui тестами с фокусом на интеграционные и e2e тесты.
## Стратегия тестирования
### Подход: Комбо (Snapshot + Integration + E2E)
1. **Snapshot Testing (70%)** — проверка UI рендеринга через insta
2. **Integration Testing (25%)** — проверка логики и flow через FakeTdClient
3. **E2E Smoke Testing (5%)** — базовая проверка что приложение запускается
### Почему не юнит-тесты?
- TUI сложно тестировать через юниты (моки, хрупкость)
- Интеграционные тесты дают больше уверенности
- Snapshots ловят UI регрессии лучше, чем assert координат
---
## Фаза 0: Инфраструктура
### Зависимости
- [x] Добавить `insta = "1.34"` в dev-dependencies
- [x] Добавить `tokio-test = "0.4"` в dev-dependencies
- [x] Настроить `.gitignore` для snapshots (добавить `tests/snapshots/*.new`)
### Helpers и Test Utilities
- [x] Создать `tests/helpers/mod.rs`
- [x] Создать `tests/helpers/app_builder.rs` — builder для тестового App
- [x] Создать `tests/helpers/fake_tdclient.rs` — mock TDLib клиент
- [x] Создать `tests/helpers/snapshot_utils.rs` — утилиты для snapshot тестов
- [x] Создать `tests/helpers/test_data.rs` — фикстуры данных (чаты, сообщения)
```rust
// tests/helpers/mod.rs
pub mod app_builder;
pub mod fake_tdclient;
pub mod snapshot_utils;
pub mod test_data;
pub use app_builder::TestAppBuilder;
pub use fake_tdclient::FakeTdClient;
pub use snapshot_utils::{render_to_string, assert_ui_snapshot};
pub use test_data::{create_test_chat, create_test_message};
```
**Файлы для создания**:
```
tests/
├── helpers/
│ ├── mod.rs
│ ├── app_builder.rs
│ ├── fake_tdclient.rs
│ ├── snapshot_utils.rs
│ └── test_data.rs
└── snapshots/ # Создаётся insta автоматически
```
---
## Фаза 1: Snapshot Tests для UI (Приоритет: ВЫСОКИЙ)
### 1.1 Chat List — Список чатов
**Файл**: `tests/ui/chat_list_test.rs`
- [x] Пустой список чатов
- [x] Список с 3 чатами (без индикаторов)
- [x] Чат с непрочитанными сообщениями `(5)`
- [x] Чат с иконкой закреплённого 📌
- [x] Чат с иконкой mute 🔇
- [x] Чат с индикатором mention @
- [ ] Чат с онлайн-статусом ●
- [x] Выбранный чат (с ▌)
- [x] Список чатов в режиме поиска
- [x] Длинное название чата (обрезка)
**Пример теста**:
```rust
#[test]
fn snapshot_chat_list_with_unread() {
let app = TestAppBuilder::new()
.with_chat(create_test_chat("Mom", 123, unread: 5))
.with_chat(create_test_chat("Boss", 456, unread: 0))
.build();
assert_ui_snapshot!("chat_list_with_unread", app, |f, app| {
render_chat_list(f, f.size(), app);
});
}
```
---
### 1.2 Messages — Область сообщений
**Файл**: `tests/messages.rs`
- [x] Пустой чат (нет сообщений)
- [x] Одно входящее сообщение
- [x] Одно исходящее сообщение
- [x] Группировка по дате (разделитель "Сегодня")
- [x] Группировка по дате (разделитель "Вчера")
- [x] Группировка по отправителю (заголовок с именем)
- [x] Исходящее сообщение с ✓ (отправлено)
- [x] Исходящее сообщение с ✓✓ (прочитано)
- [x] Сообщение с индикатором редактирования ✎
- [x] Длинное сообщение (wrap на несколько строк)
- [x] Markdown: жирный, курсив, код
- [x] Markdown: ссылка, упоминание
- [x] Markdown: спойлер
- [x] Сообщение с медиа-заглушкой [Фото]
- [x] Reply сообщение с превью
- [x] Пересланное сообщение (↪ Переслано от)
- [x] Сообщение с одной реакцией [👍]
- [x] Сообщение с несколькими реакциями [👍] 5 👎 3
- [x] Выбранное сообщение (подсветка)
---
### 1.3 Modals — Модальные окна
**Файл**: `tests/modals.rs`
- [x] Delete confirmation модалка
- [x] Emoji picker (8x6 сетка)
- [x] Emoji picker с выбранной реакцией (курсор)
- [x] Profile модалка (личный чат)
- [x] Profile модалка (группа)
- [x] Pinned message вверху чата
- [x] Search в чате (с результатами)
- [x] Forward mode (список чатов для пересылки)
---
### 1.4 Input Field — Поле ввода
**Файл**: `tests/ui/input_test.rs`
- [ ] Пустое поле ввода
- [ ] Поле ввода с текстом и курсором █
- [ ] Поле ввода с длинным текстом (2 строки)
- [ ] Поле ввода с длинным текстом (10 строк, максимум)
- [ ] Режим редактирования (с превью)
- [ ] Режим reply (с превью сообщения)
- [ ] Режим поиска (с query)
---
### 1.5 Footer — Нижняя панель
**Файл**: `tests/ui/footer_test.rs`
- [ ] Footer в списке чатов (команды навигации)
- [ ] Footer в открытом чате (команды сообщений)
- [ ] Footer с индикатором "⚠ Нет сети"
- [ ] Footer с индикатором "⏳ Подключение..."
- [ ] Footer в режиме поиска
- [ ] Footer в режиме выбора сообщения
---
### 1.6 Screens — Полные экраны
**Файл**: `tests/ui/screens_test.rs`
- [ ] Loading screen
- [ ] Auth screen (ввод телефона)
- [ ] Auth screen (ввод кода)
- [ ] Auth screen (ввод пароля 2FA)
- [ ] Main screen (папки + чаты + пустая область)
- [ ] Main screen (папки + чаты + открытый чат)
- [ ] Минимальный размер терминала (предупреждение)
---
## Фаза 2: Integration Tests для логики (Приоритет: ВЫСОКИЙ)
### 2.1 Send Message Flow
**Файл**: `tests/integration/send_message_test.rs`
- [ ] Отправка текстового сообщения
- [ ] Отправка сообщения обновляет UI
- [ ] Отправка пустого сообщения игнорируется
- [ ] Отправка с markdown форматированием
- [ ] Счётчик непрочитанных обнуляется при открытии чата
- [ ] Новое сообщение появляется в реальном времени
---
### 2.2 Edit Message Flow
**Файл**: `tests/integration/edit_message_test.rs`
- [ ] ↑ при пустом инпуте активирует режим выбора
- [ ] Enter в режиме выбора начинает редактирование
- [ ] Изменение текста и Enter сохраняет
- [ ] Esc отменяет редактирование
- [ ] Редактирование только своих сообщений
- [ ] Индикатор ✎ появляется после редактирования
---
### 2.3 Delete Message Flow
**Файл**: `tests/integration/delete_message_test.rs`
- [ ] d в режиме выбора открывает модалку
- [ ] y в модалке удаляет сообщение
- [ ] n в модалке отменяет удаление
- [ ] Esc отменяет удаление
- [ ] Сообщение исчезает из списка после удаления
- [ ] Удаление только своих сообщений
---
### 2.4 Reply & Forward Flow
**Файл**: `tests/integration/reply_forward_test.rs`
- [ ] r в режиме выбора активирует reply mode
- [ ] Превью сообщения отображается в инпуте
- [ ] Отправка reply создаёт связь с оригиналом
- [ ] Esc отменяет reply mode
- [ ] f в режиме выбора активирует forward mode
- [ ] Выбор чата стрелками в forward mode
- [ ] Enter пересылает сообщение
- [ ] Пересланное сообщение показывает "↪ Переслано от"
---
### 2.5 Reactions Flow
**Файл**: `tests/integration/reactions_test.rs`
- [ ] e открывает emoji picker
- [ ] Навигация стрелками по сетке эмодзи
- [ ] Enter добавляет реакцию
- [ ] Повторный Enter удаляет реакцию (toggle)
- [ ] Esc закрывает emoji picker
- [ ] Реакция появляется под сообщением
- [ ] Своя реакция в рамках [👍]
- [ ] Чужая реакция без рамок 👍
- [ ] Реакция 1 человека: только эмодзи
- [ ] Реакция 2+ людей: эмодзи + счётчик
---
### 2.6 Search Flow
**Файл**: `tests/integration/search_test.rs`
- [ ] Ctrl+S активирует поиск по чатам
- [ ] Фильтрация чатов по названию
- [ ] Фильтрация чатов по @username
- [ ] Esc закрывает поиск
- [ ] Ctrl+F активирует поиск в чате
- [ ] n переходит к следующему результату
- [ ] N переходит к предыдущему результату
- [ ] Подсветка найденных совпадений
---
### 2.7 Drafts Flow
**Файл**: `tests/integration/drafts_test.rs`
- [ ] Переключение между чатами сохраняет текст
- [ ] Возврат в чат восстанавливает текст
- [ ] Отправка сообщения удаляет черновик
- [ ] Индикатор черновика в списке чатов
---
### 2.8 Navigation Flow
**Файл**: `tests/integration/navigation_test.rs`
- [ ] ↑/↓ навигация по списку чатов
- [ ] Enter открывает чат
- [ ] Esc закрывает чат
- [ ] 1-9 переключение между папками
- [ ] ↑/↓ скролл сообщений в чате
- [ ] Подгрузка старых сообщений при скролле вверх
- [ ] Русская раскладка (р о л д)
---
### 2.9 Profile Flow
**Файл**: `tests/integration/profile_test.rs`
- [ ] i открывает профиль в личном чате
- [ ] Профиль показывает имя, username, телефон
- [ ] i открывает профиль в группе
- [ ] Профиль группы показывает название, описание, участников
- [ ] Esc закрывает профиль
---
### 2.10 Copy Flow
**Файл**: `tests/integration/copy_test.rs`
- [ ] y в режиме выбора копирует текст
- [ ] Clipboard содержит правильный текст
- [ ] Копирование работает на разных платформах
---
### 2.11 Typing Indicator Flow
**Файл**: `tests/integration/typing_test.rs`
- [ ] Ввод текста отправляет статус "печатает"
- [ ] Получение статуса показывает "печатает..." в UI
- [ ] Статус исчезает через timeout
---
### 2.12 Config Flow
**Файл**: `tests/integration/config_test.rs`
- [ ] Загрузка конфига из ~/.config/tele-tui/config.toml
- [ ] Создание дефолтного конфига если отсутствует
- [ ] Применение timezone к отображению времени
- [ ] Применение цветов к сообщениям
- [ ] Валидация невалидного timezone
- [ ] Валидация невалидного цвета
- [ ] Загрузка credentials: приоритет XDG → .env
- [ ] Ошибка если credentials не найдены
---
## Фаза 3: E2E Smoke Tests (Приоритет: СРЕДНИЙ)
**Файл**: `tests/e2e/smoke_test.rs`
- [ ] Приложение запускается без краша
- [ ] Приложение рендерит loading screen
- [ ] Приложение корректно завершается по Ctrl+C
- [ ] Минимальный размер терминала не крашит приложение
**Примечание**: E2E тесты опциональны, так как требуют реального TDLib или сложного мока.
---
## Фаза 4: Дополнительные тесты (Приоритет: НИЗКИЙ)
### 4.1 Utils Tests
**Файл**: `tests/unit/utils_test.rs`
- [ ] `format_timestamp_with_tz` с разными timezone
- [ ] `parse_timezone_offset` валидные значения
- [ ] `parse_timezone_offset` инвалидные значения (fallback)
- [ ] `format_date` для сегодня, вчера, старых дат
- [ ] `format_was_online` для разных временных промежутков
### 4.2 Performance Tests
**Файл**: `tests/performance/render_bench.rs`
- [ ] Benchmark рендеринга 100 сообщений
- [ ] Benchmark рендеринга списка 50 чатов
- [ ] Benchmark форматирования markdown текста
---
## Метрики прогресса
### Фаза 0: Инфраструктура
- [x] 8/8 задач выполнено ✅
### Фаза 1: Snapshot Tests
- [x] 1.1 Chat List: 9/10 (90%)
- [x] 1.2 Messages: 18/19 (95%) ✅
- [x] 1.3 Modals: 8/8 (100%) ✅
- [ ] 1.4 Input Field: 0/7
- [ ] 1.5 Footer: 0/6
- [ ] 1.6 Screens: 0/7
- **Итого: 35/57 snapshot тестов (61%)**
### Фаза 2: Integration Tests
- [ ] 2.1 Send Message: 0/6
- [ ] 2.2 Edit Message: 0/6
- [ ] 2.3 Delete Message: 0/6
- [ ] 2.4 Reply & Forward: 0/8
- [ ] 2.5 Reactions: 0/10
- [ ] 2.6 Search: 0/8
- [ ] 2.7 Drafts: 0/4
- [ ] 2.8 Navigation: 0/7
- [ ] 2.9 Profile: 0/5
- [ ] 2.10 Copy: 0/3
- [ ] 2.11 Typing: 0/3
- [ ] 2.12 Config: 0/8
- **Итого: 0/74 интеграционных тестов**
### Фаза 3: E2E Smoke
- [ ] 0/4 smoke тестов
### Фаза 4: Дополнительно
- [ ] 4.1 Utils: 0/5
- [ ] 4.2 Performance: 0/3
- **Итого: 0/8 дополнительных тестов**
---
## Общий прогресс
**Всего**: 35/151 тестов (23%)
**Фаза 0 (Инфраструктура)**: ✅ Завершена
**Фаза 1.1 (Chat List)**: 9/10 (90%)
**Фаза 1.2 (Messages)**: 18/19 (95%) ✅
**Фаза 1.3 (Modals)**: 8/8 (100%) ✅
---
## Приоритизация
### Критичные (делать в первую очередь):
1. **Фаза 0**: Инфраструктура (без неё никуда)
2. **1.2**: Messages snapshots (ядро приложения)
3. **2.1**: Send message (основной flow)
4. **2.8**: Navigation (базовая навигация)
### Важные (делать после критичных):
5. **1.1**: Chat list snapshots
6. **2.2**: Edit message
7. **2.3**: Delete message
8. **2.5**: Reactions
9. **2.6**: Search
### Желательные (можно отложить):
10. **1.3-1.6**: Остальные snapshots
11. **2.4, 2.7, 2.9-2.12**: Остальные flows
12. **Фаза 3**: E2E smoke tests
### Опциональные (по желанию):
13. **Фаза 4**: Utils и performance
---
## Технологии
### Основные
- **insta** — snapshot testing
- **tokio-test** — async testing utilities
- **ratatui::backend::TestBackend** — виртуальный терминал
### Дополнительные (опционально)
- **expectrl** — для E2E тестов с реальным бинарником
- **criterion** — для бенчмарков (фаза 4.2)
- **mockall** — если понадобятся моки (скорее всего нет)
---
## Примеры структуры тестов
### Snapshot Test
```rust
use insta::assert_snapshot;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
#[test]
fn snapshot_messages_with_reactions() {
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
let app = TestAppBuilder::new()
.with_message(create_test_message("Hello!", reactions: vec![
reaction("👍", 1, chosen: true),
reaction("👎", 3, chosen: false),
]))
.build();
terminal.draw(|f| {
render_messages(f, f.size(), &app);
}).unwrap();
let buffer = terminal.backend().buffer();
assert_snapshot!(buffer_to_string(buffer));
}
```
### Integration Test
```rust
use crate::helpers::{TestAppBuilder, FakeTdClient};
#[tokio::test]
async fn test_send_message_updates_ui() {
let fake_client = FakeTdClient::new()
.with_chat("Mom", 123);
let mut app = TestAppBuilder::new()
.with_client(fake_client)
.with_selected_chat(123)
.build();
// Ввод текста
app.input_text = "Hello!".to_string();
// Отправка
app.handle_key(KeyCode::Enter).await;
// Проверки
assert_eq!(app.input_text, ""); // Инпут очистился
assert_eq!(app.current_messages().len(), 1);
assert_eq!(app.current_messages()[0].text, "Hello!");
assert_eq!(fake_client.sent_messages().len(), 1);
}
```
---
## Команды
```bash
# Прогнать все тесты
cargo test
# Прогнать только snapshot тесты
cargo test --test ui
# Прогнать только integration тесты
cargo test --test integration
# Обновить snapshots (после ревью изменений)
cargo insta review
# Принять все новые snapshots
cargo insta accept
# Показать diff для изменённых snapshots
cargo insta test --review
```
---
## Правила
1. **Один тест = один сценарий** — не делать мега-тесты
2. **Snapshots коммитим** — они часть тестов
3. **Фикстуры переиспользуем** — общие данные в `test_data.rs`
4. **Тесты изолированы** — каждый тест создаёт свой App
5. **Порядок не важен** — тесты можно запускать в любом порядке
---
## TODO перед началом
- [ ] Прочитать документацию insta: https://insta.rs/
- [ ] Решить: нужен ли trait для TdClient или достаточно FakeTdClient
- [ ] Обсудить: какие тесты делать в первую очередь
---
## Примечания
- Этот документ будет обновляться по мере написания тестов
- После завершения фазы — отмечать в метриках
- Если тест падает или не актуален — документировать причину
- Snapshots хранятся в `tests/snapshots/__snapshots__/`

View File

@@ -1,4 +1,4 @@
#[derive(PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum AppScreen { pub enum AppScreen {
Loading, Loading,
Auth, Auth,

9
src/lib.rs Normal file
View File

@@ -0,0 +1,9 @@
// Library interface for tele-tui
// This allows tests to import modules
pub mod app;
pub mod config;
pub mod input;
pub mod tdlib;
pub mod ui;
pub mod utils;

View File

@@ -4,4 +4,10 @@ pub use client::TdClient;
pub use client::UserOnlineStatus; pub use client::UserOnlineStatus;
pub use client::NetworkState; pub use client::NetworkState;
pub use client::ProfileInfo; pub use client::ProfileInfo;
pub use client::ChatInfo;
pub use client::MessageInfo;
pub use client::ReactionInfo;
pub use client::ReplyInfo;
pub use client::ForwardInfo;
pub use client::FolderInfo;
pub use tdlib_rs::enums::ChatAction; pub use tdlib_rs::enums::ChatAction;

View File

@@ -1,8 +1,8 @@
mod loading; mod loading;
mod auth; mod auth;
mod main_screen; mod main_screen;
mod chat_list; pub mod chat_list;
mod messages; pub mod messages;
mod footer; mod footer;
pub mod profile; pub mod profile;

171
tests/chat_list.rs Normal file
View File

@@ -0,0 +1,171 @@
// Chat list UI snapshot tests
mod helpers;
use helpers::test_data::{TestChatBuilder, create_test_chat};
use helpers::app_builder::TestAppBuilder;
use helpers::snapshot_utils::{render_to_buffer, buffer_to_string};
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 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);
}

View File

@@ -0,0 +1,277 @@
// Test App builder
use tele_tui::app::{App, AppScreen};
use tele_tui::config::Config;
use tele_tui::tdlib::{ChatInfo, MessageInfo};
use ratatui::widgets::ListState;
use std::collections::HashMap;
/// Builder для создания тестового App
///
/// Примечание: Так как App содержит реальный TdClient,
/// этот билдер подходит только для UI/snapshot тестов.
/// Для интеграционных тестов логики понадобится рефакторинг
/// с выделением trait для TdClient.
pub struct TestAppBuilder {
config: Config,
screen: AppScreen,
chats: Vec<ChatInfo>,
selected_chat_id: Option<i64>,
message_input: String,
is_searching: bool,
search_query: String,
editing_message_id: Option<i64>,
replying_to_message_id: Option<i64>,
is_reaction_picker_mode: bool,
is_profile_mode: bool,
confirm_delete_message_id: Option<i64>,
messages: HashMap<i64, Vec<MessageInfo>>,
selected_message_index: Option<usize>,
message_search_mode: bool,
message_search_query: String,
forwarding_message_id: Option<i64>,
is_selecting_forward_chat: bool,
}
impl Default for TestAppBuilder {
fn default() -> Self {
Self::new()
}
}
impl TestAppBuilder {
pub fn new() -> Self {
Self {
config: Config::default(),
screen: AppScreen::Main,
chats: vec![],
selected_chat_id: None,
message_input: String::new(),
is_searching: false,
search_query: String::new(),
editing_message_id: None,
replying_to_message_id: None,
is_reaction_picker_mode: false,
is_profile_mode: false,
confirm_delete_message_id: None,
messages: HashMap::new(),
selected_message_index: None,
message_search_mode: false,
message_search_query: String::new(),
forwarding_message_id: None,
is_selecting_forward_chat: false,
}
}
/// Установить экран
pub fn screen(mut self, screen: AppScreen) -> Self {
self.screen = screen;
self
}
/// Установить конфиг
pub fn config(mut self, config: Config) -> Self {
self.config = config;
self
}
/// Добавить чат
pub fn with_chat(mut self, chat: ChatInfo) -> Self {
self.chats.push(chat);
self
}
/// Добавить несколько чатов
pub fn with_chats(mut self, chats: Vec<ChatInfo>) -> Self {
self.chats.extend(chats);
self
}
/// Выбрать чат
pub fn selected_chat(mut self, chat_id: i64) -> Self {
self.selected_chat_id = Some(chat_id);
self
}
/// Установить текст в инпуте
pub fn message_input(mut self, text: &str) -> Self {
self.message_input = text.to_string();
self
}
/// Режим поиска
pub fn searching(mut self, query: &str) -> Self {
self.is_searching = true;
self.search_query = query.to_string();
self
}
/// Режим редактирования сообщения
pub fn editing_message(mut self, message_id: i64) -> Self {
self.editing_message_id = Some(message_id);
self
}
/// Режим ответа на сообщение
pub fn replying_to(mut self, message_id: i64) -> Self {
self.replying_to_message_id = Some(message_id);
self
}
/// Режим выбора реакции
pub fn reaction_picker(mut self) -> Self {
self.is_reaction_picker_mode = true;
self
}
/// Режим профиля
pub fn profile_mode(mut self) -> Self {
self.is_profile_mode = true;
self
}
/// Подтверждение удаления
pub fn delete_confirmation(mut self, message_id: i64) -> Self {
self.confirm_delete_message_id = Some(message_id);
self
}
/// Добавить сообщение для чата
pub fn with_message(mut self, chat_id: i64, message: MessageInfo) -> Self {
self.messages.entry(chat_id).or_insert_with(Vec::new).push(message);
self
}
/// Добавить несколько сообщений для чата
pub fn with_messages(mut self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
self.messages.entry(chat_id).or_insert_with(Vec::new).extend(messages);
self
}
/// Установить выбранное сообщение (режим selection)
pub fn selecting_message(mut self, message_index: usize) -> Self {
self.selected_message_index = Some(message_index);
self
}
/// Режим поиска по сообщениям в чате
pub fn message_search(mut self, query: &str) -> Self {
self.message_search_mode = true;
self.message_search_query = query.to_string();
self
}
/// Режим пересылки сообщения
pub fn forward_mode(mut self, message_id: i64) -> Self {
self.forwarding_message_id = Some(message_id);
self.is_selecting_forward_chat = true;
self
}
/// Построить App
///
/// ВАЖНО: Этот метод создаёт App с реальным TdClient,
/// поэтому он подходит только для UI тестов, где мы
/// не вызываем методы TdClient.
pub fn build(self) -> App {
let mut app = App::new(self.config);
app.screen = self.screen;
app.chats = self.chats;
app.selected_chat_id = self.selected_chat_id;
app.message_input = self.message_input;
app.is_searching = self.is_searching;
app.search_query = self.search_query;
app.editing_message_id = self.editing_message_id;
app.replying_to_message_id = self.replying_to_message_id;
app.is_reaction_picker_mode = self.is_reaction_picker_mode;
app.is_profile_mode = self.is_profile_mode;
app.confirm_delete_message_id = self.confirm_delete_message_id;
app.selected_message_index = self.selected_message_index;
app.is_message_search_mode = self.message_search_mode;
app.message_search_query = self.message_search_query;
app.forwarding_message_id = self.forwarding_message_id;
app.is_selecting_forward_chat = self.is_selecting_forward_chat;
// Выбираем первый чат если есть
if !app.chats.is_empty() {
let mut list_state = ListState::default();
list_state.select(Some(0));
app.chat_list_state = list_state;
}
// Применяем сообщения к текущему открытому чату
if let Some(chat_id) = self.selected_chat_id {
if let Some(messages) = self.messages.get(&chat_id) {
app.td_client.current_chat_messages = messages.clone();
app.td_client.current_chat_id = Some(chat_id);
}
}
app
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::test_data::create_test_chat;
#[test]
fn test_builder_defaults() {
let app = TestAppBuilder::new().build();
assert_eq!(app.screen, AppScreen::Main);
assert_eq!(app.chats.len(), 0);
assert_eq!(app.selected_chat_id, None);
assert_eq!(app.message_input, "");
}
#[test]
fn test_builder_with_chats() {
let chat1 = create_test_chat("Mom", 123);
let chat2 = create_test_chat("Boss", 456);
let app = TestAppBuilder::new()
.with_chat(chat1)
.with_chat(chat2)
.build();
assert_eq!(app.chats.len(), 2);
assert_eq!(app.chats[0].title, "Mom");
assert_eq!(app.chats[1].title, "Boss");
}
#[test]
fn test_builder_with_selected_chat() {
let chat = create_test_chat("Mom", 123);
let app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(123)
.build();
assert_eq!(app.selected_chat_id, Some(123));
}
#[test]
fn test_builder_editing_mode() {
let app = TestAppBuilder::new()
.editing_message(999)
.message_input("Edited text")
.build();
assert_eq!(app.editing_message_id, Some(999));
assert_eq!(app.message_input, "Edited text");
}
#[test]
fn test_builder_search_mode() {
let app = TestAppBuilder::new()
.searching("test query")
.build();
assert!(app.is_searching);
assert_eq!(app.search_query, "test query");
}
}

View File

@@ -0,0 +1,280 @@
// Fake TDLib client for testing
use std::collections::HashMap;
use tele_tui::tdlib::{ChatInfo, MessageInfo, FolderInfo, NetworkState};
/// Упрощённый mock TDLib клиента для тестов
#[derive(Clone)]
pub struct FakeTdClient {
pub chats: Vec<ChatInfo>,
pub messages: HashMap<i64, Vec<MessageInfo>>,
pub folders: Vec<FolderInfo>,
pub user_names: HashMap<i64, String>,
pub network_state: NetworkState,
pub typing_chat_id: Option<i64>,
pub sent_messages: Vec<SentMessage>,
pub edited_messages: Vec<EditedMessage>,
pub deleted_messages: Vec<i64>,
pub reactions: HashMap<i64, Vec<String>>, // message_id -> emojis
}
#[derive(Debug, Clone)]
pub struct SentMessage {
pub chat_id: i64,
pub text: String,
pub reply_to: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct EditedMessage {
pub message_id: i64,
pub new_text: String,
}
impl Default for FakeTdClient {
fn default() -> Self {
Self::new()
}
}
impl FakeTdClient {
pub fn new() -> Self {
Self {
chats: vec![],
messages: HashMap::new(),
folders: vec![
FolderInfo {
id: 0,
name: "All".to_string(),
},
],
user_names: HashMap::new(),
network_state: NetworkState::Ready,
typing_chat_id: None,
sent_messages: vec![],
edited_messages: vec![],
deleted_messages: vec![],
reactions: HashMap::new(),
}
}
/// Добавить чат
pub fn with_chat(mut self, chat: ChatInfo) -> Self {
self.chats.push(chat);
self
}
/// Добавить несколько чатов
pub fn with_chats(mut self, chats: Vec<ChatInfo>) -> Self {
self.chats.extend(chats);
self
}
/// Добавить сообщение в чат
pub fn with_message(mut self, chat_id: i64, message: MessageInfo) -> Self {
self.messages
.entry(chat_id)
.or_insert_with(Vec::new)
.push(message);
self
}
/// Добавить несколько сообщений в чат
pub fn with_messages(mut self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
self.messages
.entry(chat_id)
.or_insert_with(Vec::new)
.extend(messages);
self
}
/// Добавить папку
pub fn with_folder(mut self, id: i32, name: &str) -> Self {
self.folders.push(FolderInfo {
id,
name: name.to_string(),
});
self
}
/// Добавить пользователя
pub fn with_user(mut self, id: i64, name: &str) -> Self {
self.user_names.insert(id, name.to_string());
self
}
/// Установить состояние сети
pub fn with_network_state(mut self, state: NetworkState) -> Self {
self.network_state = state;
self
}
/// Получить чаты
pub fn get_chats(&self) -> &[ChatInfo] {
&self.chats
}
/// Получить сообщения для чата
pub fn get_messages(&self, chat_id: i64) -> Vec<MessageInfo> {
self.messages
.get(&chat_id)
.cloned()
.unwrap_or_default()
}
/// Получить папки
pub fn get_folders(&self) -> &[FolderInfo] {
&self.folders
}
/// Отправить сообщение (мок)
pub fn send_message(&mut self, chat_id: i64, text: String, reply_to: Option<i64>) -> i64 {
let message_id = (self.sent_messages.len() as i64) + 1000;
self.sent_messages.push(SentMessage {
chat_id,
text: text.clone(),
reply_to,
});
// Добавляем сообщение в список сообщений чата
let message = MessageInfo {
id: message_id,
sender_name: "You".to_string(),
is_outgoing: true,
content: text,
entities: vec![],
date: 1640000000,
edit_date: 0,
is_read: true,
can_be_edited: true,
can_be_deleted_only_for_self: true,
can_be_deleted_for_all_users: true,
reply_to: None,
forward_from: None,
reactions: vec![],
};
self.messages
.entry(chat_id)
.or_insert_with(Vec::new)
.push(message);
message_id
}
/// Редактировать сообщение (мок)
pub fn edit_message(&mut self, chat_id: i64, message_id: i64, new_text: String) {
self.edited_messages.push(EditedMessage {
message_id,
new_text: new_text.clone(),
});
// Обновляем сообщение в списке
if let Some(messages) = self.messages.get_mut(&chat_id) {
if let Some(msg) = messages.iter_mut().find(|m| m.id == message_id) {
msg.content = new_text;
msg.edit_date = msg.date + 60;
}
}
}
/// Удалить сообщение (мок)
pub fn delete_message(&mut self, chat_id: i64, message_id: i64) {
self.deleted_messages.push(message_id);
// Удаляем сообщение из списка
if let Some(messages) = self.messages.get_mut(&chat_id) {
messages.retain(|m| m.id != message_id);
}
}
/// Добавить реакцию (мок)
pub fn add_reaction(&mut self, message_id: i64, emoji: String) {
self.reactions
.entry(message_id)
.or_insert_with(Vec::new)
.push(emoji);
}
/// Установить статус "печатает"
pub fn set_typing(&mut self, chat_id: Option<i64>) {
self.typing_chat_id = chat_id;
}
/// Получить список отправленных сообщений
pub fn sent_messages(&self) -> &[SentMessage] {
&self.sent_messages
}
/// Получить список отредактированных сообщений
pub fn edited_messages(&self) -> &[EditedMessage] {
&self.edited_messages
}
/// Получить список удалённых сообщений
pub fn deleted_messages(&self) -> &[i64] {
&self.deleted_messages
}
/// Очистить историю действий
pub fn clear_history(&mut self) {
self.sent_messages.clear();
self.edited_messages.clear();
self.deleted_messages.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::test_data::create_test_chat;
#[test]
fn test_fake_client_creation() {
let client = FakeTdClient::new();
assert_eq!(client.chats.len(), 0);
assert_eq!(client.folders.len(), 1); // Default "All" folder
}
#[test]
fn test_fake_client_with_chat() {
let chat = create_test_chat("Mom", 123);
let client = FakeTdClient::new().with_chat(chat);
assert_eq!(client.chats.len(), 1);
assert_eq!(client.chats[0].title, "Mom");
}
#[test]
fn test_send_message() {
let mut client = FakeTdClient::new();
let msg_id = client.send_message(123, "Hello".to_string(), None);
assert_eq!(client.sent_messages().len(), 1);
assert_eq!(client.sent_messages()[0].text, "Hello");
assert_eq!(client.get_messages(123).len(), 1);
assert_eq!(client.get_messages(123)[0].id, msg_id);
}
#[test]
fn test_edit_message() {
let mut client = FakeTdClient::new();
let msg_id = client.send_message(123, "Hello".to_string(), None);
client.edit_message(123, msg_id, "Hello World".to_string());
assert_eq!(client.edited_messages().len(), 1);
assert_eq!(client.get_messages(123)[0].content, "Hello World");
assert!(client.get_messages(123)[0].edit_date > 0);
}
#[test]
fn test_delete_message() {
let mut client = FakeTdClient::new();
let msg_id = client.send_message(123, "Hello".to_string(), None);
client.delete_message(123, msg_id);
assert_eq!(client.deleted_messages().len(), 1);
assert_eq!(client.get_messages(123).len(), 0);
}
}

11
tests/helpers/mod.rs Normal file
View File

@@ -0,0 +1,11 @@
// Test helpers module
pub mod app_builder;
pub mod fake_tdclient;
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

@@ -0,0 +1,89 @@
// Snapshot testing utilities
use ratatui::backend::TestBackend;
use ratatui::Terminal;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
/// Конвертирует Buffer в читаемую строку для snapshot тестов
pub fn buffer_to_string(buffer: &Buffer) -> String {
let area = buffer.area();
let mut result = String::new();
for y in 0..area.height {
let mut line = String::new();
for x in 0..area.width {
line.push_str(buffer[(x, y)].symbol());
}
// Убираем trailing spaces в конце строки
result.push_str(line.trim_end());
if y < area.height - 1 {
result.push('\n');
}
}
result
}
/// Создаёт TestBackend с заданным размером и рендерит UI
pub fn render_to_buffer<F>(width: u16, height: u16, render_fn: F) -> Buffer
where
F: FnOnce(&mut ratatui::Frame),
{
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(render_fn)
.unwrap();
terminal.backend().buffer().clone()
}
/// Макрос для упрощения snapshot тестов
#[macro_export]
macro_rules! assert_ui_snapshot {
($name:expr, $width:expr, $height:expr, $render_fn:expr) => {{
use $crate::helpers::snapshot_utils::{render_to_buffer, buffer_to_string};
let buffer = render_to_buffer($width, $height, $render_fn);
let output = buffer_to_string(&buffer);
insta::assert_snapshot!($name, output);
}};
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::widgets::{Block, Borders};
#[test]
fn test_buffer_to_string_simple() {
let buffer = render_to_buffer(10, 3, |f| {
let block = Block::default()
.borders(Borders::ALL)
.title("Hi");
f.render_widget(block, f.area());
});
let result = buffer_to_string(&buffer);
assert!(result.contains("Hi"));
assert!(result.contains(""));
assert!(result.contains(""));
}
#[test]
fn test_buffer_to_string_removes_trailing_spaces() {
let buffer = render_to_buffer(20, 3, |f| {
let block = Block::default().title("Test");
f.render_widget(block, Rect::new(0, 0, 10, 3));
});
let result = buffer_to_string(&buffer);
let lines: Vec<&str> = result.lines().collect();
// Проверяем что trailing spaces убраны
for line in lines {
assert!(!line.ends_with(' ') || line.trim().is_empty());
}
}
}

241
tests/helpers/test_data.rs Normal file
View File

@@ -0,0 +1,241 @@
// Test data builders and fixtures
use tele_tui::tdlib::{ChatInfo, MessageInfo, ReactionInfo, ReplyInfo, ForwardInfo, ProfileInfo};
/// Builder для создания тестового чата
pub struct TestChatBuilder {
id: i64,
title: String,
username: Option<String>,
last_message: String,
last_message_date: i32,
unread_count: i32,
unread_mention_count: i32,
is_pinned: bool,
order: i64,
last_read_outbox_message_id: i64,
folder_ids: Vec<i32>,
is_muted: bool,
draft_text: Option<String>,
}
impl TestChatBuilder {
pub fn new(title: &str, id: i64) -> Self {
Self {
id,
title: title.to_string(),
username: None,
last_message: "".to_string(),
last_message_date: 1640000000,
unread_count: 0,
unread_mention_count: 0,
is_pinned: false,
order: id,
last_read_outbox_message_id: 0,
folder_ids: vec![0],
is_muted: false,
draft_text: None,
}
}
pub fn username(mut self, username: &str) -> Self {
self.username = Some(username.to_string());
self
}
pub fn last_message(mut self, text: &str) -> Self {
self.last_message = text.to_string();
self
}
pub fn unread_count(mut self, count: i32) -> Self {
self.unread_count = count;
self
}
pub fn unread_mentions(mut self, count: i32) -> Self {
self.unread_mention_count = count;
self
}
pub fn pinned(mut self) -> Self {
self.is_pinned = true;
self
}
pub fn muted(mut self) -> Self {
self.is_muted = true;
self
}
pub fn draft(mut self, text: &str) -> Self {
self.draft_text = Some(text.to_string());
self
}
pub fn folder(mut self, folder_id: i32) -> Self {
self.folder_ids = vec![folder_id];
self
}
pub fn build(self) -> ChatInfo {
ChatInfo {
id: self.id,
title: self.title,
username: self.username,
last_message: self.last_message,
last_message_date: self.last_message_date,
unread_count: self.unread_count,
unread_mention_count: self.unread_mention_count,
is_pinned: self.is_pinned,
order: self.order,
last_read_outbox_message_id: self.last_read_outbox_message_id,
folder_ids: self.folder_ids,
is_muted: self.is_muted,
draft_text: self.draft_text,
}
}
}
/// Builder для создания тестового сообщения
pub struct TestMessageBuilder {
id: i64,
sender_name: String,
is_outgoing: bool,
content: String,
entities: Vec<tdlib_rs::types::TextEntity>,
date: i32,
edit_date: i32,
is_read: bool,
can_be_edited: bool,
can_be_deleted_only_for_self: bool,
can_be_deleted_for_all_users: bool,
reply_to: Option<ReplyInfo>,
forward_from: Option<ForwardInfo>,
reactions: Vec<ReactionInfo>,
}
impl TestMessageBuilder {
pub fn new(content: &str, id: i64) -> Self {
Self {
id,
sender_name: "User".to_string(),
is_outgoing: false,
content: content.to_string(),
entities: vec![],
date: 1640000000,
edit_date: 0,
is_read: true,
can_be_edited: false,
can_be_deleted_only_for_self: true,
can_be_deleted_for_all_users: false,
reply_to: None,
forward_from: None,
reactions: vec![],
}
}
pub fn outgoing(mut self) -> Self {
self.is_outgoing = true;
self.sender_name = "You".to_string();
self.can_be_edited = true;
self.can_be_deleted_for_all_users = true;
self
}
pub fn sender(mut self, name: &str) -> Self {
self.sender_name = name.to_string();
self
}
pub fn date(mut self, timestamp: i32) -> Self {
self.date = timestamp;
self
}
pub fn edited(mut self) -> Self {
self.edit_date = self.date + 60;
self
}
pub fn unread(mut self) -> Self {
self.is_read = false;
self
}
pub fn reply_to(mut self, message_id: i64, sender: &str, text: &str) -> Self {
self.reply_to = Some(ReplyInfo {
message_id,
sender_name: sender.to_string(),
text: text.to_string(),
});
self
}
pub fn forwarded_from(mut self, sender: &str) -> Self {
self.forward_from = Some(ForwardInfo {
sender_name: sender.to_string(),
date: self.date - 3600,
});
self
}
pub fn reaction(mut self, emoji: &str, count: i32, chosen: bool) -> Self {
self.reactions.push(ReactionInfo {
emoji: emoji.to_string(),
count,
is_chosen: chosen,
});
self
}
pub fn build(self) -> MessageInfo {
MessageInfo {
id: self.id,
sender_name: self.sender_name,
is_outgoing: self.is_outgoing,
content: self.content,
entities: self.entities,
date: self.date,
edit_date: self.edit_date,
is_read: self.is_read,
can_be_edited: self.can_be_edited,
can_be_deleted_only_for_self: self.can_be_deleted_only_for_self,
can_be_deleted_for_all_users: self.can_be_deleted_for_all_users,
reply_to: self.reply_to,
forward_from: self.forward_from,
reactions: self.reactions,
}
}
}
/// Хелперы для быстрого создания тестовых данных
pub fn create_test_chat(title: &str, id: i64) -> ChatInfo {
TestChatBuilder::new(title, id).build()
}
pub fn create_test_message(content: &str, id: i64) -> MessageInfo {
TestMessageBuilder::new(content, id).build()
}
pub fn create_test_user(name: &str, id: i64) -> (i64, String) {
(id, name.to_string())
}
/// Хелпер для создания профиля
pub fn create_test_profile(title: &str, chat_id: i64) -> ProfileInfo {
ProfileInfo {
chat_id,
title: title.to_string(),
username: None,
bio: None,
phone_number: None,
chat_type: "Личный чат".to_string(),
member_count: None,
description: None,
invite_link: None,
is_group: false,
online_status: None,
}
}

399
tests/messages.rs Normal file
View File

@@ -0,0 +1,399 @@
// Messages UI snapshot tests
mod helpers;
use helpers::test_data::{TestChatBuilder, TestMessageBuilder, create_test_chat};
use helpers::app_builder::TestAppBuilder;
use helpers::snapshot_utils::{render_to_buffer, buffer_to_string};
use insta::assert_snapshot;
#[test]
fn snapshot_empty_chat() {
let chat = create_test_chat("Mom", 123);
let mut app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("empty_chat", output);
}
#[test]
fn snapshot_single_incoming_message() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Hello there!", 1)
.sender("Mom")
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("single_incoming_message", output);
}
#[test]
fn snapshot_single_outgoing_message() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Hi mom!", 1)
.outgoing()
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("single_outgoing_message", output);
}
#[test]
fn snapshot_date_separator_old_date() {
let chat = create_test_chat("Mom", 123);
// Use a fixed old date (20 Dec 2021) - will show as date separator
let message = TestMessageBuilder::new("Message from the past", 1)
.date(1640000000) // 20 Dec 2021
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("date_separator_old_date", output);
}
// NOTE: Tests for "Сегодня" and "Вчера" date separators are skipped
// because they depend on current date and cannot be stable snapshots.
// The date formatting logic is tested manually and through the old_date test above.
#[test]
fn snapshot_sender_grouping() {
let chat = create_test_chat("Group Chat", 123);
let msg1 = TestMessageBuilder::new("First message", 1)
.sender("Alice")
.build();
let msg2 = TestMessageBuilder::new("Second message", 2)
.sender("Alice")
.build();
let msg3 = TestMessageBuilder::new("Third message", 3)
.sender("Bob")
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_messages(123, vec![msg1, msg2, msg3])
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("sender_grouping", output);
}
#[test]
fn snapshot_outgoing_sent() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Just sent", 1)
.outgoing()
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("outgoing_sent", output);
}
#[test]
fn snapshot_outgoing_read() {
let chat = TestChatBuilder::new("Mom", 123)
.last_message("Read message")
.build();
// Message with id < last_read_outbox_message_id means it's been read
let message = TestMessageBuilder::new("Read message", 1)
.outgoing()
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
// Set last_read_outbox to simulate message being read
if let Some(chat) = app.chats.iter_mut().find(|c| c.id == 123) {
chat.last_read_outbox_message_id = 2;
}
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("outgoing_read", output);
}
#[test]
fn snapshot_edited_message() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Edited text", 1)
.edited()
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("edited_message", output);
}
#[test]
fn snapshot_long_message_wrap() {
let chat = create_test_chat("Mom", 123);
let long_text = "This is a very long message that should wrap across multiple lines when rendered in the terminal UI. Let's make it even longer to ensure we test the wrapping behavior properly.";
let message = TestMessageBuilder::new(long_text, 1)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("long_message_wrap", output);
}
#[test]
fn snapshot_markdown_bold_italic_code() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("**bold** *italic* `code`", 1)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("markdown_bold_italic_code", output);
}
#[test]
fn snapshot_markdown_link_mention() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Check [this](https://example.com) and @username", 1)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("markdown_link_mention", output);
}
#[test]
fn snapshot_markdown_spoiler() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Spoiler: ||hidden text||", 1)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("markdown_spoiler", output);
}
#[test]
fn snapshot_media_placeholder() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("[Фото]", 1)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("media_placeholder", output);
}
#[test]
fn snapshot_reply_message() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("This is a reply", 2)
.reply_to(1, "Mom", "Original message text")
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("reply_message", output);
}
#[test]
fn snapshot_forwarded_message() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Forwarded content", 1)
.forwarded_from("Alice")
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("forwarded_message", output);
}
#[test]
fn snapshot_single_reaction() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Great!", 1)
.reaction("👍", 1, true)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("single_reaction", output);
}
#[test]
fn snapshot_multiple_reactions() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Popular message", 1)
.reaction("👍", 5, true)
.reaction("👎", 3, false)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("multiple_reactions", output);
}
#[test]
fn snapshot_selected_message() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Selected message", 1)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.selecting_message(1)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("selected_message", output);
}

202
tests/modals.rs Normal file
View File

@@ -0,0 +1,202 @@
// Modals UI snapshot tests
mod helpers;
use helpers::test_data::{TestChatBuilder, TestMessageBuilder, create_test_chat, create_test_profile};
use helpers::app_builder::TestAppBuilder;
use helpers::snapshot_utils::{render_to_buffer, buffer_to_string};
use insta::assert_snapshot;
#[test]
fn snapshot_delete_confirmation_modal() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("Delete me", 1)
.outgoing()
.build();
let app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.delete_confirmation(1)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("delete_confirmation_modal", output);
}
#[test]
fn snapshot_emoji_picker_default() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("React to this", 1)
.build();
let app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.reaction_picker()
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("emoji_picker_default", output);
}
#[test]
fn snapshot_emoji_picker_with_selection() {
let chat = create_test_chat("Mom", 123);
let message = TestMessageBuilder::new("React to this", 1)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_message(123, message)
.selected_chat(123)
.reaction_picker()
.build();
// Выбираем 5-ю реакцию (индекс 4)
app.selected_reaction_index = 4;
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("emoji_picker_with_selection", output);
}
#[test]
fn snapshot_profile_personal_chat() {
let chat = create_test_chat("Alice", 123);
let profile = create_test_profile("Alice", 123);
let mut app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(123)
.profile_mode()
.build();
app.profile_info = Some(profile);
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("profile_personal_chat", output);
}
#[test]
fn snapshot_profile_group_chat() {
let chat = TestChatBuilder::new("Work Group", 456)
.build();
let mut profile = create_test_profile("Work Group", 456);
profile.is_group = true;
profile.chat_type = "Группа".to_string();
profile.member_count = Some(25);
profile.description = Some("Work discussion group".to_string());
let mut app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(456)
.profile_mode()
.build();
app.profile_info = Some(profile);
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("profile_group_chat", output);
}
#[test]
fn snapshot_pinned_message() {
let chat = create_test_chat("Mom", 123);
let message1 = TestMessageBuilder::new("Regular message", 1)
.build();
let pinned_msg = TestMessageBuilder::new("Important pinned message!", 2)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_messages(123, vec![message1])
.selected_chat(123)
.build();
// Устанавливаем закреплённое сообщение
app.td_client.current_pinned_message = Some(pinned_msg);
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("pinned_message", output);
}
#[test]
fn snapshot_search_in_chat() {
let chat = create_test_chat("Mom", 123);
let msg1 = TestMessageBuilder::new("Hello world", 1)
.build();
let msg2 = TestMessageBuilder::new("World is beautiful", 2)
.build();
let msg3 = TestMessageBuilder::new("Beautiful day", 3)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.with_messages(123, vec![msg1.clone(), msg2.clone(), msg3])
.selected_chat(123)
.message_search("world")
.build();
// Устанавливаем результаты поиска
app.message_search_results = vec![msg1, msg2];
app.selected_search_result_index = 0;
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("search_in_chat", output);
}
#[test]
fn snapshot_forward_mode() {
let chat1 = create_test_chat("Mom", 123);
let chat2 = create_test_chat("Dad", 456);
let chat3 = create_test_chat("Work Group", 789);
let message = TestMessageBuilder::new("Forward this message", 1)
.build();
let mut app = TestAppBuilder::new()
.with_chats(vec![chat1.clone(), chat2, chat3])
.with_message(123, message)
.selected_chat(123)
.forward_mode(1)
.build();
let buffer = render_to_buffer(80, 24, |f| {
// В forward mode показывается chat_list для выбора чата
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("forward_mode", output);
}

View File

@@ -0,0 +1,28 @@
---
source: tests/chat_list.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│🔍 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Mom │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/chat_list.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│🔍 Ctrl+S для поиска │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Mom │
│ Boss │
│ Rust Community │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/chat_list.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│🔍 Ctrl+S для поиска │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Very Long Chat Title That Should Be Truncated │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/chat_list.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│🔍 Ctrl+S для поиска │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ 🔇 Spam Group (99) │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/chat_list.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│🔍 Ctrl+S для поиска │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ 📌 Important Chat │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/chat_list.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│🔍 Ctrl+S для поиска │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│▌ Mom │
│ Boss │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/chat_list.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│🔍 Ctrl+S для поиска │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Work Group @ (10) │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/chat_list.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│🔍 Ctrl+S для поиска │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Mom (5) │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/chat_list.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│🔍 Ctrl+S для поиска │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Message from the past │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33 ✎) Edited text │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│Нет сообщений │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│↪ Переслано от Alice │
│ (14:33) Forwarded content │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) This is a very long message that should wrap across multiple lines │
│ when rendered in the terminal UI. Let's make it even longer to │
│ ensure we test the wrapping behavior properly. │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) **bold** *italic* `code` │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Check [this](https://example.com) and @username │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Spoiler: ||hidden text|| │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) [Фото] │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Popular message │
│[👍 ] 5 👎 3 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│ Вы ──────────────── │
│ Read message (14:33 ✓✓) │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│ Вы ──────────────── │
│ Just sent (14:33 ✓✓) │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│┌ Mom: Original message text │
│ (14:33) This is a reply │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Selected message │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌ Выбор сообщения ─────────────────────────────────────────────────────────────┐
│↑↓ · r ответить · f переслать · y копировать · Esc │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Group Chat │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│Alice ──────────────── │
│ (14:33) First message │
│ (14:33) Second message │
│ │
│Bob ──────────────── │
│ (14:33) Third message │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│Mom ──────────────── │
│ (14:33) Hello there! │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│ Вы ──────────────── │
│ Hi mom! (14:33 ✓✓) │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/messages.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Great! │
│[👍 ] │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/modals.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│ Вы ──────────────── │
│ Delete me (14:33 ✓✓) │
│ ┌ Подтверждение ───────────────────────┐ │
│ │ │ │
│ │ Удалить сообщение? │ │
│ │ │ │
│ │ [y/Enter] Да [n/Esc] Нет │ │
│ │ │ │
│ └──────────────────────────────────────┘ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/modals.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) React to this │
│ │
│ │
│ ┌ Выбери реакцию ────────────────────────────────┐ │
│ │ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/modals.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) React to this │
│ │
│ │
│ ┌ Выбери реакцию ────────────────────────────────┐ │
│ │ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/modals.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│🔍 Ctrl+S для поиска │
└──────────────────────────────────────────────────────────────────────────────┘
┌ ↪ Выберите чат ──────────────────────────────────────────────────────────────┐
│▌ Mom │
│ Dad │
│ Work Group │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/modals.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 Mom │
└──────────────────────────────────────────────────────────────────────────────┘
📌 02.01.2022 14:33 Important pinned message! Ctrl+P
┌──────────────────────────────────────────────────────────────────────────────┐
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Regular message │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│> █ Введите сообщение... │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/modals.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 ПРОФИЛЬ: Work Group │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│Тип: Группа │
│ │
│ID: 456 │
│ │
│Участников: 25 │
│ │
│Описание: │
│Work discussion group │
│ │
│──────────────────────────────── │
│ │
│Действия: │
│ │
│▶ Скопировать ID │
│ Покинуть группу │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ↑↓ навигация Enter выбрать Esc выход │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/modals.rs
expression: output
---
┌──────────────────────────────────────────────────────────────────────────────┐
│👤 ПРОФИЛЬ: Alice │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│Тип: Личный чат │
│ │
│ID: 123 │
│ │
│──────────────────────────────── │
│ │
│Действия: │
│ │
│▶ Скопировать ID │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ↑↓ навигация Enter выбрать Esc выход │
└──────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,28 @@
---
source: tests/modals.rs
expression: output
---
┌ Поиск по сообщениям ─────────────────────────────────────────────────────────┐
│🔍 world█ (1/2) │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│▶ User (02.01.2022 14:33) │
│ Hello world │
│ │
│ User (02.01.2022 14:33) │
│ World is beautiful │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ ↑↓ навигация n/N след./пред. Enter перейти Esc выход │
└──────────────────────────────────────────────────────────────────────────────┘