feat: FTPS, AppError, Site Manager, tests, multi-window, auto-clear, CI/CD, virtual scroll

- FTPS: реализован FtpsClient через suppaftp + async-native-tls
- AppError: кастомный enum с Serialize вместо Result<T, String>
- Site Manager UI: создание/редактирование/удаление сайтов
- Тесты: 53 тестов (domain, storage, transfer, fs, remote)
- Multi-window: новое окно + сохранение геометрии
- Auto-clear: воркер чистит завершённые задачи по таймеру
- CI/CD: 3-ступенчатый пайплайн (lint→build→test) с кэшированием
- Virtual scroll: fileTable с рендером только видимых строк
- UI: исправлены close-кнопки, color-scheme для select, скриншоты в README
This commit is contained in:
loki5512344 2026-07-11 00:04:13 +02:00
parent 545dd4ed0b
commit e188fbef36
Signed by: boba
GPG key ID: 253067914055423B
52 changed files with 5188 additions and 523 deletions

View file

@ -51,3 +51,115 @@ impl TransferQueue {
q.retain(|t| t.id != id);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::transfer::{TaskState, TransferKind, TransferTask};
fn make_task(id: &str, total: u64) -> TransferTask {
TransferTask {
id: id.into(),
kind: TransferKind::Download,
connection_id: "conn-1".into(),
local_path: "/local".into(),
remote_path: "/remote".into(),
file_name: "file.txt".into(),
total_bytes: total,
transferred_bytes: 0,
state: TaskState::Queued,
speed: None,
eta_secs: None,
}
}
#[test]
fn test_push_pop_all() {
let q = TransferQueue::default();
assert!(q.all().is_empty());
q.push(make_task("t1", 100));
q.push(make_task("t2", 200));
assert_eq!(q.all().len(), 2);
let t1 = q.pop().unwrap();
assert_eq!(t1.id, "t1");
assert_eq!(q.all().len(), 1);
let t2 = q.pop().unwrap();
assert_eq!(t2.id, "t2");
assert!(q.pop().is_none());
}
#[test]
fn test_get() {
let q = TransferQueue::default();
q.push(make_task("t1", 100));
q.push(make_task("t2", 200));
assert_eq!(q.get("t2").unwrap().total_bytes, 200);
assert!(q.get("nonexistent").is_none());
}
#[test]
fn test_update_state() {
let q = TransferQueue::default();
q.push(make_task("t1", 100));
for state in [
TaskState::Running,
TaskState::Paused,
TaskState::Completed,
TaskState::Failed("err".into()),
TaskState::Cancelled,
TaskState::Retrying(5),
] {
q.update_state("t1", state.clone());
assert_eq!(q.get("t1").unwrap().state, state);
}
}
#[test]
fn test_update_progress() {
let q = TransferQueue::default();
q.push(make_task("t1", 1000));
q.update_progress("t1", 250, 50);
let t = q.get("t1").unwrap();
assert_eq!(t.transferred_bytes, 250);
assert_eq!(t.speed, Some(50));
assert_eq!(t.eta_secs, Some(15));
}
#[test]
fn test_update_progress_zero_speed() {
let q = TransferQueue::default();
q.push(make_task("t1", 1000));
q.update_progress("t1", 100, 0);
let t = q.get("t1").unwrap();
assert_eq!(t.transferred_bytes, 100);
assert_eq!(t.speed, Some(0));
assert_eq!(t.eta_secs, None);
}
#[test]
fn test_remove() {
let q = TransferQueue::default();
q.push(make_task("t1", 100));
q.push(make_task("t2", 200));
q.push(make_task("t3", 300));
q.remove("t2");
let ids: Vec<String> = q.all().into_iter().map(|t| t.id).collect();
assert_eq!(ids, vec!["t1", "t3"]);
}
#[test]
fn test_remove_nonexistent() {
let q = TransferQueue::default();
q.push(make_task("t1", 100));
q.remove("does-not-exist");
assert_eq!(q.all().len(), 1);
}
}