From 68a2b7a9823b41a755d466bd3ca4f9d0b5a01be5 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Wed, 28 Jan 2026 11:39:21 +0300 Subject: [PATCH] fixes --- .gitignore | 5 + CONTEXT.md | 69 +- Cargo.lock | 60 ++ Cargo.toml | 4 + PROJECT_STRUCTURE.md | 2 + README.md | 94 +++ REFACTORING_ROADMAP.md | 664 ++++++++++++++++++ TESTING_PROGRESS.md | 291 ++++++++ TESTING_ROADMAP.md | 569 +++++++++++++++ src/app/state.rs | 2 +- src/lib.rs | 9 + src/tdlib/mod.rs | 6 + src/ui/mod.rs | 4 +- tests/chat_list.rs | 171 +++++ tests/helpers/app_builder.rs | 277 ++++++++ tests/helpers/fake_tdclient.rs | 280 ++++++++ tests/helpers/mod.rs | 11 + tests/helpers/snapshot_utils.rs | 89 +++ tests/helpers/test_data.rs | 241 +++++++ tests/messages.rs | 399 +++++++++++ tests/modals.rs | 202 ++++++ .../chat_list__chat_list_search_mode.snap | 28 + .../chat_list__chat_list_three_chats.snap | 28 + .../snapshots/chat_list__chat_long_title.snap | 28 + tests/snapshots/chat_list__chat_muted.snap | 28 + tests/snapshots/chat_list__chat_pinned.snap | 28 + tests/snapshots/chat_list__chat_selected.snap | 28 + .../chat_list__chat_with_mentions.snap | 28 + .../chat_list__chat_with_unread.snap | 28 + .../snapshots/chat_list__empty_chat_list.snap | 28 + .../messages__date_separator_old_date.snap | 28 + tests/snapshots/messages__edited_message.snap | 28 + tests/snapshots/messages__empty_chat.snap | 28 + .../messages__forwarded_message.snap | 28 + .../messages__long_message_wrap.snap | 28 + .../messages__markdown_bold_italic_code.snap | 28 + .../messages__markdown_link_mention.snap | 28 + .../snapshots/messages__markdown_spoiler.snap | 28 + .../messages__media_placeholder.snap | 28 + .../messages__multiple_reactions.snap | 28 + tests/snapshots/messages__outgoing_read.snap | 28 + tests/snapshots/messages__outgoing_sent.snap | 28 + tests/snapshots/messages__reply_message.snap | 28 + .../snapshots/messages__selected_message.snap | 28 + .../snapshots/messages__sender_grouping.snap | 28 + .../messages__single_incoming_message.snap | 28 + .../messages__single_outgoing_message.snap | 28 + .../snapshots/messages__single_reaction.snap | 28 + .../modals__delete_confirmation_modal.snap | 28 + .../modals__emoji_picker_default.snap | 28 + .../modals__emoji_picker_with_selection.snap | 28 + tests/snapshots/modals__forward_mode.snap | 28 + tests/snapshots/modals__pinned_message.snap | 28 + .../snapshots/modals__profile_group_chat.snap | 28 + .../modals__profile_personal_chat.snap | 28 + tests/snapshots/modals__search_in_chat.snap | 28 + 56 files changed, 4424 insertions(+), 5 deletions(-) create mode 100644 REFACTORING_ROADMAP.md create mode 100644 TESTING_PROGRESS.md create mode 100644 TESTING_ROADMAP.md create mode 100644 src/lib.rs create mode 100644 tests/chat_list.rs create mode 100644 tests/helpers/app_builder.rs create mode 100644 tests/helpers/fake_tdclient.rs create mode 100644 tests/helpers/mod.rs create mode 100644 tests/helpers/snapshot_utils.rs create mode 100644 tests/helpers/test_data.rs create mode 100644 tests/messages.rs create mode 100644 tests/modals.rs create mode 100644 tests/snapshots/chat_list__chat_list_search_mode.snap create mode 100644 tests/snapshots/chat_list__chat_list_three_chats.snap create mode 100644 tests/snapshots/chat_list__chat_long_title.snap create mode 100644 tests/snapshots/chat_list__chat_muted.snap create mode 100644 tests/snapshots/chat_list__chat_pinned.snap create mode 100644 tests/snapshots/chat_list__chat_selected.snap create mode 100644 tests/snapshots/chat_list__chat_with_mentions.snap create mode 100644 tests/snapshots/chat_list__chat_with_unread.snap create mode 100644 tests/snapshots/chat_list__empty_chat_list.snap create mode 100644 tests/snapshots/messages__date_separator_old_date.snap create mode 100644 tests/snapshots/messages__edited_message.snap create mode 100644 tests/snapshots/messages__empty_chat.snap create mode 100644 tests/snapshots/messages__forwarded_message.snap create mode 100644 tests/snapshots/messages__long_message_wrap.snap create mode 100644 tests/snapshots/messages__markdown_bold_italic_code.snap create mode 100644 tests/snapshots/messages__markdown_link_mention.snap create mode 100644 tests/snapshots/messages__markdown_spoiler.snap create mode 100644 tests/snapshots/messages__media_placeholder.snap create mode 100644 tests/snapshots/messages__multiple_reactions.snap create mode 100644 tests/snapshots/messages__outgoing_read.snap create mode 100644 tests/snapshots/messages__outgoing_sent.snap create mode 100644 tests/snapshots/messages__reply_message.snap create mode 100644 tests/snapshots/messages__selected_message.snap create mode 100644 tests/snapshots/messages__sender_grouping.snap create mode 100644 tests/snapshots/messages__single_incoming_message.snap create mode 100644 tests/snapshots/messages__single_outgoing_message.snap create mode 100644 tests/snapshots/messages__single_reaction.snap create mode 100644 tests/snapshots/modals__delete_confirmation_modal.snap create mode 100644 tests/snapshots/modals__emoji_picker_default.snap create mode 100644 tests/snapshots/modals__emoji_picker_with_selection.snap create mode 100644 tests/snapshots/modals__forward_mode.snap create mode 100644 tests/snapshots/modals__pinned_message.snap create mode 100644 tests/snapshots/modals__profile_group_chat.snap create mode 100644 tests/snapshots/modals__profile_personal_chat.snap create mode 100644 tests/snapshots/modals__search_in_chat.snap diff --git a/.gitignore b/.gitignore index fd2f35f..c4af13b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ # Local config files (if created in project root) config.toml credentials + +# Insta snapshot testing +# Commit snapshots, but not the .new files +tests/**/*.snap.new +*.snap.new diff --git a/CONTEXT.md b/CONTEXT.md index 54c0ceb..faf506e 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,6 +1,6 @@ # Текущий контекст проекта -## Статус: Фаза 9 — ЗАВЕРШЕНО +## Статус: Фаза 9 — ЗАВЕРШЕНО + Тестирование (19%) ### Что сделано @@ -127,6 +127,7 @@ ``` src/ ├── main.rs # Точка входа, event loop, TDLib инициализация, graceful shutdown +├── lib.rs # Библиотечный интерфейс (для тестов) ├── config.rs # Конфигурация (TOML), загрузка credentials ├── app/ │ ├── mod.rs # App структура и состояние (needs_redraw флаг) @@ -147,8 +148,42 @@ src/ └── tdlib/ ├── mod.rs # Модуль экспорта (TdClient, UserOnlineStatus, NetworkState) └── 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 оставаться отзывчивым. @@ -236,9 +271,39 @@ reaction_chosen = "yellow" 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 -Все пункты Фазы 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. **Тестирование** — добавить юнит-тесты для критичных функций ## Известные проблемы diff --git a/Cargo.lock b/Cargo.lock index f420f16..802f19f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,18 @@ dependencies = [ "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]] name = "constant_time_eq" version = "0.3.1" @@ -515,6 +527,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1108,6 +1126,18 @@ dependencies = [ "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]] name = "instability" version = "0.3.11" @@ -2032,6 +2062,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "slab" version = "0.4.11" @@ -2195,12 +2231,14 @@ dependencies = [ "crossterm", "dirs 5.0.1", "dotenvy", + "insta", "open", "ratatui", "serde", "serde_json", "tdlib-rs", "tokio", + "tokio-test", "toml", ] @@ -2360,6 +2398,28 @@ dependencies = [ "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]] name = "tokio-util" version = "0.7.18" diff --git a/Cargo.toml b/Cargo.toml index 809d884..818fe92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,5 +23,9 @@ arboard = "3.4" toml = "0.8" dirs = "5.0" +[dev-dependencies] +insta = "1.34" +tokio-test = "0.4" + [build-dependencies] tdlib-rs = { version = "1.1", features = ["download-tdlib"] } diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 93c94e6..c469bbf 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -223,6 +223,8 @@ XDG config directory: - **DEVELOPMENT.md** — правила разработки - **PROJECT_STRUCTURE.md** — этот файл - **ROADMAP.md** — план развития +- **REFACTORING_ROADMAP.md** — план рефакторинга +- **TESTING_ROADMAP.md** — план покрытия тестами - **CONTEXT.md** — текущий статус, архитектурные решения ### Спецификации diff --git a/README.md b/README.md index 2f7365b..b0a14d4 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,97 @@ src/ - `clipboard` 0.5 — clipboard access - `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) — подробная инструкция по установке @@ -156,6 +247,9 @@ src/ - [REQUIREMENTS.md](REQUIREMENTS.md) — функциональные требования - [DEVELOPMENT.md](DEVELOPMENT.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) — текущий статус разработки ## Лицензия diff --git a/REFACTORING_ROADMAP.md b/REFACTORING_ROADMAP.md new file mode 100644 index 0000000..b92bd8a --- /dev/null +++ b/REFACTORING_ROADMAP.md @@ -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, + selected_index: usize, + }, + Profile { + info: ProfileInfo, + }, + SearchInChat { + query: String, + results: Vec, + 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 for ChatId { + fn from(id: i64) -> Self { + ChatId(id) + } +} + +// Аналогично для MessageId и UserId +``` + +**Преимущества**: +- Невозможно случайно передать message_id вместо chat_id +- Компилятор поймает ошибки +- Улучшенная читаемость + +--- + +### 5. Создать enum для ошибок + +**Проблема**: Везде используется `Result` — теряется контекст ошибок. + +**Решение**: Создать `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 = std::result::Result; +``` + +**Зависимости**: `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, + pub media_type: Option, +} + +pub struct MessageState { + pub is_outgoing: bool, + pub is_edited: bool, + pub is_pinned: bool, +} + +pub struct MessageInteractions { + pub reply_to_message_id: Option, + pub forward_info: Option, + pub reactions: Vec, + 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( + 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 { + // Вся логика форматирования +} +``` + +**Преимущества**: +- Разделение ответственности +- Можно тестировать отдельно +- Переиспользование в других местах + +--- + +### 9. Вынести логику группировки сообщений + +**Проблема**: Логика группировки сообщений смешана с рендерингом в `messages.rs`. + +**Решение**: Создать `src/message_grouping.rs`: +```rust +pub enum MessageGroup { + DateSeparator(String), + SenderHeader(String), + Message(MessageInfo), +} + +pub fn group_messages(messages: &[MessageInfo]) -> Vec { + // Логика группировки по дате и отправителю +} +``` + +**Преимущества**: +- Чистое разделение логики и представления +- Легче тестировать группировку +- Можно переиспользовать + +--- + +### 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, + pub down: Vec, + // ... +} + +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` или использовать готовый крейт `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. **Документация** — обновлять документацию после изменений + +--- + +## Примечания + +- Этот документ живой и будет обновляться +- Новые пункты добавляются по мере обнаружения +- После завершения задачи отмечать в метриках +- При появлении блокеров — документировать в соответствующей секции diff --git a/TESTING_PROGRESS.md b/TESTING_PROGRESS.md new file mode 100644 index 0000000..595d25b --- /dev/null +++ b/TESTING_PROGRESS.md @@ -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 diff --git a/TESTING_ROADMAP.md b/TESTING_ROADMAP.md new file mode 100644 index 0000000..8bbcf78 --- /dev/null +++ b/TESTING_ROADMAP.md @@ -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__/` diff --git a/src/app/state.rs b/src/app/state.rs index 71db9c7..cd69aa7 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1,4 +1,4 @@ -#[derive(PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone)] pub enum AppScreen { Loading, Auth, diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0637a27 --- /dev/null +++ b/src/lib.rs @@ -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; diff --git a/src/tdlib/mod.rs b/src/tdlib/mod.rs index f58cd6a..f2037b0 100644 --- a/src/tdlib/mod.rs +++ b/src/tdlib/mod.rs @@ -4,4 +4,10 @@ pub use client::TdClient; pub use client::UserOnlineStatus; pub use client::NetworkState; 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; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9fd3679..d389073 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,8 +1,8 @@ mod loading; mod auth; mod main_screen; -mod chat_list; -mod messages; +pub mod chat_list; +pub mod messages; mod footer; pub mod profile; diff --git a/tests/chat_list.rs b/tests/chat_list.rs new file mode 100644 index 0000000..de7193f --- /dev/null +++ b/tests/chat_list.rs @@ -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); +} diff --git a/tests/helpers/app_builder.rs b/tests/helpers/app_builder.rs new file mode 100644 index 0000000..c038daa --- /dev/null +++ b/tests/helpers/app_builder.rs @@ -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, + selected_chat_id: Option, + message_input: String, + is_searching: bool, + search_query: String, + editing_message_id: Option, + replying_to_message_id: Option, + is_reaction_picker_mode: bool, + is_profile_mode: bool, + confirm_delete_message_id: Option, + messages: HashMap>, + selected_message_index: Option, + message_search_mode: bool, + message_search_query: String, + forwarding_message_id: Option, + 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) -> 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) -> 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"); + } +} diff --git a/tests/helpers/fake_tdclient.rs b/tests/helpers/fake_tdclient.rs new file mode 100644 index 0000000..0f706ae --- /dev/null +++ b/tests/helpers/fake_tdclient.rs @@ -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, + pub messages: HashMap>, + pub folders: Vec, + pub user_names: HashMap, + pub network_state: NetworkState, + pub typing_chat_id: Option, + pub sent_messages: Vec, + pub edited_messages: Vec, + pub deleted_messages: Vec, + pub reactions: HashMap>, // message_id -> emojis +} + +#[derive(Debug, Clone)] +pub struct SentMessage { + pub chat_id: i64, + pub text: String, + pub reply_to: Option, +} + +#[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) -> 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) -> 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 { + 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 { + 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) { + 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); + } +} diff --git a/tests/helpers/mod.rs b/tests/helpers/mod.rs new file mode 100644 index 0000000..db6e444 --- /dev/null +++ b/tests/helpers/mod.rs @@ -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}; diff --git a/tests/helpers/snapshot_utils.rs b/tests/helpers/snapshot_utils.rs new file mode 100644 index 0000000..b9d38b9 --- /dev/null +++ b/tests/helpers/snapshot_utils.rs @@ -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(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()); + } + } +} diff --git a/tests/helpers/test_data.rs b/tests/helpers/test_data.rs new file mode 100644 index 0000000..29a9963 --- /dev/null +++ b/tests/helpers/test_data.rs @@ -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, + 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, + is_muted: bool, + draft_text: Option, +} + +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, + 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, + forward_from: Option, + reactions: Vec, +} + +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, + } +} diff --git a/tests/messages.rs b/tests/messages.rs new file mode 100644 index 0000000..8e1d1ae --- /dev/null +++ b/tests/messages.rs @@ -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); +} diff --git a/tests/modals.rs b/tests/modals.rs new file mode 100644 index 0000000..a7da16c --- /dev/null +++ b/tests/modals.rs @@ -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); +} diff --git a/tests/snapshots/chat_list__chat_list_search_mode.snap b/tests/snapshots/chat_list__chat_list_search_mode.snap new file mode 100644 index 0000000..54f7741 --- /dev/null +++ b/tests/snapshots/chat_list__chat_list_search_mode.snap @@ -0,0 +1,28 @@ +--- +source: tests/chat_list.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│🔍 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ Mom │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/chat_list__chat_list_three_chats.snap b/tests/snapshots/chat_list__chat_list_three_chats.snap new file mode 100644 index 0000000..164790b --- /dev/null +++ b/tests/snapshots/chat_list__chat_list_three_chats.snap @@ -0,0 +1,28 @@ +--- +source: tests/chat_list.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│🔍 Ctrl+S для поиска │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ Mom │ +│ Boss │ +│ Rust Community │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/chat_list__chat_long_title.snap b/tests/snapshots/chat_list__chat_long_title.snap new file mode 100644 index 0000000..5d1cb66 --- /dev/null +++ b/tests/snapshots/chat_list__chat_long_title.snap @@ -0,0 +1,28 @@ +--- +source: tests/chat_list.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│🔍 Ctrl+S для поиска │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ Very Long Chat Title That Should Be Truncated │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/chat_list__chat_muted.snap b/tests/snapshots/chat_list__chat_muted.snap new file mode 100644 index 0000000..a6242b9 --- /dev/null +++ b/tests/snapshots/chat_list__chat_muted.snap @@ -0,0 +1,28 @@ +--- +source: tests/chat_list.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│🔍 Ctrl+S для поиска │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 🔇 Spam Group (99) │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/chat_list__chat_pinned.snap b/tests/snapshots/chat_list__chat_pinned.snap new file mode 100644 index 0000000..c91eb5f --- /dev/null +++ b/tests/snapshots/chat_list__chat_pinned.snap @@ -0,0 +1,28 @@ +--- +source: tests/chat_list.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│🔍 Ctrl+S для поиска │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 📌 Important Chat │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/chat_list__chat_selected.snap b/tests/snapshots/chat_list__chat_selected.snap new file mode 100644 index 0000000..f601d9a --- /dev/null +++ b/tests/snapshots/chat_list__chat_selected.snap @@ -0,0 +1,28 @@ +--- +source: tests/chat_list.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│🔍 Ctrl+S для поиска │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│▌ Mom │ +│ Boss │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/chat_list__chat_with_mentions.snap b/tests/snapshots/chat_list__chat_with_mentions.snap new file mode 100644 index 0000000..011bc61 --- /dev/null +++ b/tests/snapshots/chat_list__chat_with_mentions.snap @@ -0,0 +1,28 @@ +--- +source: tests/chat_list.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│🔍 Ctrl+S для поиска │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ Work Group @ (10) │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/chat_list__chat_with_unread.snap b/tests/snapshots/chat_list__chat_with_unread.snap new file mode 100644 index 0000000..759ed0d --- /dev/null +++ b/tests/snapshots/chat_list__chat_with_unread.snap @@ -0,0 +1,28 @@ +--- +source: tests/chat_list.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│🔍 Ctrl+S для поиска │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ Mom (5) │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/chat_list__empty_chat_list.snap b/tests/snapshots/chat_list__empty_chat_list.snap new file mode 100644 index 0000000..73634d7 --- /dev/null +++ b/tests/snapshots/chat_list__empty_chat_list.snap @@ -0,0 +1,28 @@ +--- +source: tests/chat_list.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│🔍 Ctrl+S для поиска │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__date_separator_old_date.snap b/tests/snapshots/messages__date_separator_old_date.snap new file mode 100644 index 0000000..c208a55 --- /dev/null +++ b/tests/snapshots/messages__date_separator_old_date.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│ (14:33) Message from the past │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__edited_message.snap b/tests/snapshots/messages__edited_message.snap new file mode 100644 index 0000000..ae43e84 --- /dev/null +++ b/tests/snapshots/messages__edited_message.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│ (14:33 ✎) Edited text │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__empty_chat.snap b/tests/snapshots/messages__empty_chat.snap new file mode 100644 index 0000000..1215be2 --- /dev/null +++ b/tests/snapshots/messages__empty_chat.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│Нет сообщений │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__forwarded_message.snap b/tests/snapshots/messages__forwarded_message.snap new file mode 100644 index 0000000..810dff7 --- /dev/null +++ b/tests/snapshots/messages__forwarded_message.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│↪ Переслано от Alice │ +│ (14:33) Forwarded content │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__long_message_wrap.snap b/tests/snapshots/messages__long_message_wrap.snap new file mode 100644 index 0000000..b03e458 --- /dev/null +++ b/tests/snapshots/messages__long_message_wrap.snap @@ -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. │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__markdown_bold_italic_code.snap b/tests/snapshots/messages__markdown_bold_italic_code.snap new file mode 100644 index 0000000..67b927b --- /dev/null +++ b/tests/snapshots/messages__markdown_bold_italic_code.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│ (14:33) **bold** *italic* `code` │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__markdown_link_mention.snap b/tests/snapshots/messages__markdown_link_mention.snap new file mode 100644 index 0000000..a6211be --- /dev/null +++ b/tests/snapshots/messages__markdown_link_mention.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│ (14:33) Check [this](https://example.com) and @username │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__markdown_spoiler.snap b/tests/snapshots/messages__markdown_spoiler.snap new file mode 100644 index 0000000..8b8bac4 --- /dev/null +++ b/tests/snapshots/messages__markdown_spoiler.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│ (14:33) Spoiler: ||hidden text|| │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__media_placeholder.snap b/tests/snapshots/messages__media_placeholder.snap new file mode 100644 index 0000000..aa6291a --- /dev/null +++ b/tests/snapshots/messages__media_placeholder.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│ (14:33) [Фото] │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__multiple_reactions.snap b/tests/snapshots/messages__multiple_reactions.snap new file mode 100644 index 0000000..c8a2cf5 --- /dev/null +++ b/tests/snapshots/messages__multiple_reactions.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│ (14:33) Popular message │ +│[👍 ] 5 👎 3 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__outgoing_read.snap b/tests/snapshots/messages__outgoing_read.snap new file mode 100644 index 0000000..37da376 --- /dev/null +++ b/tests/snapshots/messages__outgoing_read.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│ Вы ──────────────── │ +│ Read message (14:33 ✓✓) │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__outgoing_sent.snap b/tests/snapshots/messages__outgoing_sent.snap new file mode 100644 index 0000000..c8586c1 --- /dev/null +++ b/tests/snapshots/messages__outgoing_sent.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│ Вы ──────────────── │ +│ Just sent (14:33 ✓✓) │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__reply_message.snap b/tests/snapshots/messages__reply_message.snap new file mode 100644 index 0000000..f4307c4 --- /dev/null +++ b/tests/snapshots/messages__reply_message.snap @@ -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 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__selected_message.snap b/tests/snapshots/messages__selected_message.snap new file mode 100644 index 0000000..581d331 --- /dev/null +++ b/tests/snapshots/messages__selected_message.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│ (14:33) Selected message │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌ Выбор сообщения ─────────────────────────────────────────────────────────────┐ +│↑↓ · r ответить · f переслать · y копировать · Esc │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__sender_grouping.snap b/tests/snapshots/messages__sender_grouping.snap new file mode 100644 index 0000000..345c13d --- /dev/null +++ b/tests/snapshots/messages__sender_grouping.snap @@ -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 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__single_incoming_message.snap b/tests/snapshots/messages__single_incoming_message.snap new file mode 100644 index 0000000..4eb04b1 --- /dev/null +++ b/tests/snapshots/messages__single_incoming_message.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│Mom ──────────────── │ +│ (14:33) Hello there! │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__single_outgoing_message.snap b/tests/snapshots/messages__single_outgoing_message.snap new file mode 100644 index 0000000..1221f7b --- /dev/null +++ b/tests/snapshots/messages__single_outgoing_message.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│ Вы ──────────────── │ +│ Hi mom! (14:33 ✓✓) │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/messages__single_reaction.snap b/tests/snapshots/messages__single_reaction.snap new file mode 100644 index 0000000..b7f88e6 --- /dev/null +++ b/tests/snapshots/messages__single_reaction.snap @@ -0,0 +1,28 @@ +--- +source: tests/messages.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│ (14:33) Great! │ +│[👍 ] │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/modals__delete_confirmation_modal.snap b/tests/snapshots/modals__delete_confirmation_modal.snap new file mode 100644 index 0000000..c2ac787 --- /dev/null +++ b/tests/snapshots/modals__delete_confirmation_modal.snap @@ -0,0 +1,28 @@ +--- +source: tests/modals.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│ Вы ──────────────── │ +│ Delete me (14:33 ✓✓) │ +│ ┌ Подтверждение ───────────────────────┐ │ +│ │ │ │ +│ │ Удалить сообщение? │ │ +│ │ │ │ +│ │ [y/Enter] Да [n/Esc] Нет │ │ +│ │ │ │ +│ └──────────────────────────────────────┘ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/modals__emoji_picker_default.snap b/tests/snapshots/modals__emoji_picker_default.snap new file mode 100644 index 0000000..b3f621a --- /dev/null +++ b/tests/snapshots/modals__emoji_picker_default.snap @@ -0,0 +1,28 @@ +--- +source: tests/modals.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│ (14:33) React to this │ +│ │ +│ │ +│ ┌ Выбери реакцию ────────────────────────────────┐ │ +│ │ │ │ +│ │ │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/modals__emoji_picker_with_selection.snap b/tests/snapshots/modals__emoji_picker_with_selection.snap new file mode 100644 index 0000000..b3f621a --- /dev/null +++ b/tests/snapshots/modals__emoji_picker_with_selection.snap @@ -0,0 +1,28 @@ +--- +source: tests/modals.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 Mom │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ──────── 02.01.2022 ──────── │ +│ │ +│User ──────────────── │ +│ (14:33) React to this │ +│ │ +│ │ +│ ┌ Выбери реакцию ────────────────────────────────┐ │ +│ │ │ │ +│ │ │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/modals__forward_mode.snap b/tests/snapshots/modals__forward_mode.snap new file mode 100644 index 0000000..16e4319 --- /dev/null +++ b/tests/snapshots/modals__forward_mode.snap @@ -0,0 +1,28 @@ +--- +source: tests/modals.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│🔍 Ctrl+S для поиска │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌ ↪ Выберите чат ──────────────────────────────────────────────────────────────┐ +│▌ Mom │ +│ Dad │ +│ Work Group │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/modals__pinned_message.snap b/tests/snapshots/modals__pinned_message.snap new file mode 100644 index 0000000..ee14a2c --- /dev/null +++ b/tests/snapshots/modals__pinned_message.snap @@ -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 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│> █ Введите сообщение... │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/modals__profile_group_chat.snap b/tests/snapshots/modals__profile_group_chat.snap new file mode 100644 index 0000000..75a4568 --- /dev/null +++ b/tests/snapshots/modals__profile_group_chat.snap @@ -0,0 +1,28 @@ +--- +source: tests/modals.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 ПРОФИЛЬ: Work Group │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│Тип: Группа │ +│ │ +│ID: 456 │ +│ │ +│Участников: 25 │ +│ │ +│Описание: │ +│Work discussion group │ +│ │ +│──────────────────────────────── │ +│ │ +│Действия: │ +│ │ +│▶ Скопировать ID │ +│ Покинуть группу │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ↑↓ навигация Enter выбрать Esc выход │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/modals__profile_personal_chat.snap b/tests/snapshots/modals__profile_personal_chat.snap new file mode 100644 index 0000000..0945ff0 --- /dev/null +++ b/tests/snapshots/modals__profile_personal_chat.snap @@ -0,0 +1,28 @@ +--- +source: tests/modals.rs +expression: output +--- +┌──────────────────────────────────────────────────────────────────────────────┐ +│👤 ПРОФИЛЬ: Alice │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│Тип: Личный чат │ +│ │ +│ID: 123 │ +│ │ +│──────────────────────────────── │ +│ │ +│Действия: │ +│ │ +│▶ Скопировать ID │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ↑↓ навигация Enter выбрать Esc выход │ +└──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/modals__search_in_chat.snap b/tests/snapshots/modals__search_in_chat.snap new file mode 100644 index 0000000..73c988f --- /dev/null +++ b/tests/snapshots/modals__search_in_chat.snap @@ -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 выход │ +└──────────────────────────────────────────────────────────────────────────────┘