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

569
TESTING_ROADMAP.md Normal file
View File

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