570 lines
20 KiB
Markdown
570 lines
20 KiB
Markdown
# 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__/`
|