Rewrite Curator in Rust with an egui GUI

This commit is contained in:
2026-10-17 11:28:17 +02:00
parent 364b515e3c
commit 65b5b74c9f
156 changed files with 24359 additions and 25795 deletions
+236
View File
@@ -0,0 +1,236 @@
//! End-to-end UI tests: drive the egui window headlessly (egui_kittest) the way
//! a user would — open a dialog, click buttons — and check the pool on disk.
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use curator::core::file_manager::FileManager;
use curator::ui::app::CuratorApp;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
struct Env {
dir: tempfile::TempDir,
}
impl Env {
fn new() -> Self {
Self {
dir: tempfile::tempdir().unwrap(),
}
}
fn pool(&self) -> PathBuf {
self.dir.path().join("pool")
}
fn manager(&self) -> FileManager {
let mut fm = FileManager::with_config_path(self.dir.path().join("cfg.json"));
fm.set_pool_dir(&self.pool()).unwrap();
fm.load_pool_movies().unwrap();
fm
}
fn source(&self, name: &str) -> PathBuf {
let p = self.dir.path().join("src").join(name);
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(&p, name).unwrap();
p
}
}
fn harness(fm: FileManager) -> Harness<'static, CuratorApp> {
Harness::builder()
.with_size(egui::vec2(1300.0, 800.0))
.allow_missing_glyphs()
.with_accessibility_check(false)
.build_eframe(|cc| CuratorApp::new(fm, cc.egui_ctx.clone()))
}
/// Step until no background task runs (or time out).
fn wait_idle(h: &mut Harness<'static, CuratorApp>) {
let start = Instant::now();
loop {
h.step();
if !h.state().busy() {
h.run_steps(3);
return;
}
assert!(
start.elapsed() < Duration::from_secs(20),
"task did not finish"
);
std::thread::sleep(Duration::from_millis(20));
}
}
fn exists(p: &Path) -> bool {
p.exists()
}
#[test]
fn test_import_movie_through_the_dialog() {
let env = Env::new();
let src = env.source("Matrix.1999.1080p.mkv");
let mut h = harness(env.manager());
h.state_mut().open_import(vec![src.clone()]);
h.run_steps(3);
h.get_by_label("OK").click();
h.run_steps(2);
wait_idle(&mut h);
let pooled = env.pool().join("Filmy/Matrix.1999.1080p (1999).mkv");
assert!(
exists(&pooled),
"pool: {:?}",
fs::read_dir(env.pool().join("Filmy"))
.unwrap()
.collect::<Vec<_>>()
);
assert!(exists(&src), "copy keeps the source");
// the summary message is shown and can be dismissed
h.get_by_label("OK").click();
h.run_steps(2);
let fm = h.state().file_manager().unwrap();
assert_eq!(fm.filelist.len(), 1);
}
#[test]
fn test_import_requires_a_year() {
let env = Env::new();
let src = env.source("Bez roku.mkv");
let mut h = harness(env.manager());
h.state_mut().open_import(vec![src]);
h.run_steps(3);
h.get_by_label("OK").click();
h.run_steps(3);
// a warning appears and nothing is imported
assert!(h.query_by_label_contains("čtyřmístný rok").is_some());
assert!(
fs::read_dir(env.pool().join("Filmy"))
.unwrap()
.next()
.is_none()
);
}
#[test]
fn test_import_existing_film_as_a_version() {
let env = Env::new();
let mut fm = env.manager();
let opts = curator::core::file_manager::ImportOptions {
year: Some("2009".into()),
tmdb_id: Some("19995".into()),
..Default::default()
};
fm.import_movie(&env.source("a.mkv"), "Avatar", opts)
.unwrap();
// a second copy of the same film, named so title/year/id are prefilled
let src = env.source("Avatar (2009).3D.HSBS.mkv");
let nfo = src.with_extension("nfo");
fs::write(
&nfo,
"<movie><title>Avatar</title><uniqueid type=\"tmdb\">19995</uniqueid></movie>",
)
.unwrap();
let mut h = harness(fm);
h.state_mut().open_import(vec![src]);
h.run_steps(3);
h.get_by_label("OK").click();
h.run_steps(3);
// conflict question → add as a version
h.get_by_label("Přidat jako verzi").click();
h.run_steps(3);
// the popisek prompt is pre-filled from the 3D tag
h.get_by_label("OK").click();
h.run_steps(2);
wait_idle(&mut h);
let folder = env.pool().join("Filmy/Avatar (2009) [tmdbid-19995]");
assert!(folder.join("Avatar (2009) [tmdbid-19995].mkv").exists());
assert!(
folder
.join("Avatar (2009) [tmdbid-19995] - 3D HSBS.mkv")
.exists()
);
}
fn imported(env: &Env, fm: &mut FileManager, name: &str, title: &str) -> PathBuf {
fm.import_movie(
&env.source(name),
title,
curator::core::file_manager::ImportOptions::default(),
)
.unwrap()
.unwrap()
}
#[test]
fn test_edit_movie_renames_with_year_and_id() {
let env = Env::new();
let mut fm = env.manager();
let p = imported(&env, &mut fm, "x.mkv", "Matrix");
let mut h = harness(fm);
h.state_mut().select_movies(&[p]);
h.state_mut().debug_action("edit");
h.run_steps(3);
// the editor's year and TMDb fields (hint texts identify them)
for (label, text) in [("Rok:", "1999"), ("TMDb ID:", "603")] {
h.get_by_label(label).focus();
h.run_steps(1);
h.get_by_label(label).type_text(text);
h.run_steps(1);
}
h.run_steps(2);
h.get_by_label("OK").click();
h.run_steps(3);
assert!(
env.pool()
.join("Filmy/Matrix (1999) [tmdbid-603].mkv")
.exists()
);
}
#[test]
fn test_remove_movie_after_confirmation() {
let env = Env::new();
let mut fm = env.manager();
let p = imported(&env, &mut fm, "x.mkv", "Matrix");
let mut h = harness(fm);
h.state_mut().select_movies(std::slice::from_ref(&p));
h.run_steps(2);
h.key_press(egui::Key::Delete);
h.run_steps(3);
h.get_by_label("Ano").click();
h.run_steps(3);
assert!(!p.exists());
assert!(h.state().file_manager().unwrap().filelist.is_empty());
}
#[test]
fn test_assign_a_new_tag_through_the_dialog() {
let env = Env::new();
let mut fm = env.manager();
let p = imported(&env, &mut fm, "x.mkv", "Matrix");
let mut h = harness(fm);
h.state_mut().select_movies(std::slice::from_ref(&p));
h.state_mut().debug_action("tags");
h.run_steps(3);
h.get_by_label("➕ Nový štítek…").click();
h.run_steps(3);
// pick the "Žánr" category, then type the tag name
h.get_all_by_label("Žánr").last().unwrap().click();
h.run_steps(2);
h.get_all_by_label("OK").last().unwrap().click();
h.run_steps(3);
h.get_all_by_role(egui::accesskit::Role::TextInput)
.last()
.unwrap()
.type_text("Sci-Fi");
h.run_steps(2);
h.get_all_by_label("OK").last().unwrap().click();
h.run_steps(3);
h.get_by_label("OK").click();
h.run_steps(3);
let f = h.state().file_manager().unwrap().file(&p).unwrap().clone();
assert!(f.tag_paths().contains("Žánr/Sci-Fi"), "{:?}", f.tags);
}