fixes
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
69
CONTEXT.md
69
CONTEXT.md
@@ -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
60
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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** — текущий статус, архитектурные решения
|
||||||
|
|
||||||
### Спецификации
|
### Спецификации
|
||||||
|
|||||||
94
README.md
94
README.md
@@ -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
664
REFACTORING_ROADMAP.md
Normal 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
291
TESTING_PROGRESS.md
Normal 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
569
TESTING_ROADMAP.md
Normal 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__/`
|
||||||
@@ -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
9
src/lib.rs
Normal 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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
171
tests/chat_list.rs
Normal 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);
|
||||||
|
}
|
||||||
277
tests/helpers/app_builder.rs
Normal file
277
tests/helpers/app_builder.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
280
tests/helpers/fake_tdclient.rs
Normal file
280
tests/helpers/fake_tdclient.rs
Normal 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
11
tests/helpers/mod.rs
Normal 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};
|
||||||
89
tests/helpers/snapshot_utils.rs
Normal file
89
tests/helpers/snapshot_utils.rs
Normal 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
241
tests/helpers/test_data.rs
Normal 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
399
tests/messages.rs
Normal 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
202
tests/modals.rs
Normal 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);
|
||||||
|
}
|
||||||
28
tests/snapshots/chat_list__chat_list_search_mode.snap
Normal file
28
tests/snapshots/chat_list__chat_list_search_mode.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/chat_list.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│🔍 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Mom │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/chat_list__chat_list_three_chats.snap
Normal file
28
tests/snapshots/chat_list__chat_list_three_chats.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/chat_list.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│🔍 Ctrl+S для поиска │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Mom │
|
||||||
|
│ Boss │
|
||||||
|
│ Rust Community │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/chat_list__chat_long_title.snap
Normal file
28
tests/snapshots/chat_list__chat_long_title.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/chat_list.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│🔍 Ctrl+S для поиска │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Very Long Chat Title That Should Be Truncated │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/chat_list__chat_muted.snap
Normal file
28
tests/snapshots/chat_list__chat_muted.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/chat_list.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│🔍 Ctrl+S для поиска │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🔇 Spam Group (99) │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/chat_list__chat_pinned.snap
Normal file
28
tests/snapshots/chat_list__chat_pinned.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/chat_list.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│🔍 Ctrl+S для поиска │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 📌 Important Chat │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/chat_list__chat_selected.snap
Normal file
28
tests/snapshots/chat_list__chat_selected.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/chat_list.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│🔍 Ctrl+S для поиска │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│▌ Mom │
|
||||||
|
│ Boss │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/chat_list__chat_with_mentions.snap
Normal file
28
tests/snapshots/chat_list__chat_with_mentions.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/chat_list.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│🔍 Ctrl+S для поиска │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Work Group @ (10) │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/chat_list__chat_with_unread.snap
Normal file
28
tests/snapshots/chat_list__chat_with_unread.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/chat_list.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│🔍 Ctrl+S для поиска │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Mom (5) │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/chat_list__empty_chat_list.snap
Normal file
28
tests/snapshots/chat_list__empty_chat_list.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/chat_list.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│🔍 Ctrl+S для поиска │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__date_separator_old_date.snap
Normal file
28
tests/snapshots/messages__date_separator_old_date.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│User ──────────────── │
|
||||||
|
│ (14:33) Message from the past │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__edited_message.snap
Normal file
28
tests/snapshots/messages__edited_message.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│User ──────────────── │
|
||||||
|
│ (14:33 ✎) Edited text │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__empty_chat.snap
Normal file
28
tests/snapshots/messages__empty_chat.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│Нет сообщений │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__forwarded_message.snap
Normal file
28
tests/snapshots/messages__forwarded_message.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│User ──────────────── │
|
||||||
|
│↪ Переслано от Alice │
|
||||||
|
│ (14:33) Forwarded content │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__long_message_wrap.snap
Normal file
28
tests/snapshots/messages__long_message_wrap.snap
Normal 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. │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__markdown_bold_italic_code.snap
Normal file
28
tests/snapshots/messages__markdown_bold_italic_code.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│User ──────────────── │
|
||||||
|
│ (14:33) **bold** *italic* `code` │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__markdown_link_mention.snap
Normal file
28
tests/snapshots/messages__markdown_link_mention.snap
Normal 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 │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__markdown_spoiler.snap
Normal file
28
tests/snapshots/messages__markdown_spoiler.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│User ──────────────── │
|
||||||
|
│ (14:33) Spoiler: ||hidden text|| │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__media_placeholder.snap
Normal file
28
tests/snapshots/messages__media_placeholder.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│User ──────────────── │
|
||||||
|
│ (14:33) [Фото] │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__multiple_reactions.snap
Normal file
28
tests/snapshots/messages__multiple_reactions.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│User ──────────────── │
|
||||||
|
│ (14:33) Popular message │
|
||||||
|
│[👍 ] 5 👎 3 │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__outgoing_read.snap
Normal file
28
tests/snapshots/messages__outgoing_read.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│ Вы ──────────────── │
|
||||||
|
│ Read message (14:33 ✓✓) │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__outgoing_sent.snap
Normal file
28
tests/snapshots/messages__outgoing_sent.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│ Вы ──────────────── │
|
||||||
|
│ Just sent (14:33 ✓✓) │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__reply_message.snap
Normal file
28
tests/snapshots/messages__reply_message.snap
Normal 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 │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__selected_message.snap
Normal file
28
tests/snapshots/messages__selected_message.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│User ──────────────── │
|
||||||
|
│ (14:33) Selected message │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌ Выбор сообщения ─────────────────────────────────────────────────────────────┐
|
||||||
|
│↑↓ · r ответить · f переслать · y копировать · Esc │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__sender_grouping.snap
Normal file
28
tests/snapshots/messages__sender_grouping.snap
Normal 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 │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__single_incoming_message.snap
Normal file
28
tests/snapshots/messages__single_incoming_message.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│Mom ──────────────── │
|
||||||
|
│ (14:33) Hello there! │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__single_outgoing_message.snap
Normal file
28
tests/snapshots/messages__single_outgoing_message.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│ Вы ──────────────── │
|
||||||
|
│ Hi mom! (14:33 ✓✓) │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/messages__single_reaction.snap
Normal file
28
tests/snapshots/messages__single_reaction.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/messages.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│User ──────────────── │
|
||||||
|
│ (14:33) Great! │
|
||||||
|
│[👍 ] │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/modals__delete_confirmation_modal.snap
Normal file
28
tests/snapshots/modals__delete_confirmation_modal.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/modals.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│ Вы ──────────────── │
|
||||||
|
│ Delete me (14:33 ✓✓) │
|
||||||
|
│ ┌ Подтверждение ───────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Удалить сообщение? │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [y/Enter] Да [n/Esc] Нет │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └──────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/modals__emoji_picker_default.snap
Normal file
28
tests/snapshots/modals__emoji_picker_default.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/modals.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│User ──────────────── │
|
||||||
|
│ (14:33) React to this │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ ┌ Выбери реакцию ────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/modals__emoji_picker_with_selection.snap
Normal file
28
tests/snapshots/modals__emoji_picker_with_selection.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/modals.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 Mom │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ──────── 02.01.2022 ──────── │
|
||||||
|
│ │
|
||||||
|
│User ──────────────── │
|
||||||
|
│ (14:33) React to this │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ ┌ Выбери реакцию ────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/modals__forward_mode.snap
Normal file
28
tests/snapshots/modals__forward_mode.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/modals.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│🔍 Ctrl+S для поиска │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌ ↪ Выберите чат ──────────────────────────────────────────────────────────────┐
|
||||||
|
│▌ Mom │
|
||||||
|
│ Dad │
|
||||||
|
│ Work Group │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/modals__pinned_message.snap
Normal file
28
tests/snapshots/modals__pinned_message.snap
Normal 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 │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│> █ Введите сообщение... │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/modals__profile_group_chat.snap
Normal file
28
tests/snapshots/modals__profile_group_chat.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/modals.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 ПРОФИЛЬ: Work Group │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│Тип: Группа │
|
||||||
|
│ │
|
||||||
|
│ID: 456 │
|
||||||
|
│ │
|
||||||
|
│Участников: 25 │
|
||||||
|
│ │
|
||||||
|
│Описание: │
|
||||||
|
│Work discussion group │
|
||||||
|
│ │
|
||||||
|
│──────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│Действия: │
|
||||||
|
│ │
|
||||||
|
│▶ Скопировать ID │
|
||||||
|
│ Покинуть группу │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ↑↓ навигация Enter выбрать Esc выход │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/modals__profile_personal_chat.snap
Normal file
28
tests/snapshots/modals__profile_personal_chat.snap
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: tests/modals.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│👤 ПРОФИЛЬ: Alice │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│Тип: Личный чат │
|
||||||
|
│ │
|
||||||
|
│ID: 123 │
|
||||||
|
│ │
|
||||||
|
│──────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│Действия: │
|
||||||
|
│ │
|
||||||
|
│▶ Скопировать ID │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ↑↓ навигация Enter выбрать Esc выход │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
28
tests/snapshots/modals__search_in_chat.snap
Normal file
28
tests/snapshots/modals__search_in_chat.snap
Normal 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 выход │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
Reference in New Issue
Block a user