Files
effectcraft/crates/ui-egui/tests/ui_presets_folder.rs
BFlat 7dc488b79d Merge pull request #1222 from arnav-hiwarkar/perf/preset-panel-cache
perf(presets): reuse cached text and user preset data
2026-10-16 17:03:05 +07:00

167 lines
7.9 KiB
Rust

//! Effects & Presets: copy the configured folder path through the contents menu.
use effectcraft_engine::Session;
use effectcraft_ui_egui::panels::effects_presets::PresetFile;
use effectcraft_ui_egui::{ControlRequest, EffectcraftApp, control, dock::PanelKind};
use egui::{Event, Modifiers, OutputCommand, PointerButton, pos2};
use egui_kittest::Harness;
use serde_json::json;
fn expected_folder() -> String {
let path = std::env::var("EC_PRESETS_DIR").unwrap_or_else(|_| {
let home = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")).unwrap();
std::path::Path::new(&home).join("Documents/EffectCraft/Presets").to_str().unwrap().to_string()
});
if path.is_empty() { path } else { std::path::absolute(path).unwrap().to_str().unwrap().to_string() }
}
fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) -> Vec<String> {
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("missing {id}"));
let pos = pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0);
let mut copied = Vec::new();
h.input_mut().events.push(Event::PointerMoved(pos));
h.step();
for pressed in [true, false] {
h.input_mut().events.push(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
h.step();
for command in &h.output().platform_output.commands {
if let OutputCommand::CopyText(text) = command {
copied.push(text.clone());
}
}
}
copied
}
#[test]
fn presets_menu_copies_the_configured_folder_without_editing_the_project() {
let expected = expected_folder();
for language in ["en", "ja"] {
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| {
let mut app = EffectcraftApp::new(Session::default());
app.session.prefs.general.language = language.into();
app.ui.start_screen = false;
app.show_panel(PanelKind::EffectsPresets);
app
});
h.run_steps(3);
let before = serde_json::to_value(&h.state().session.project).unwrap();
let revision = h.state().session.revision;
assert!(h.state().auto.find("effects.copyPresetsFolderPath").is_none());
click(&mut h, "effects.menu");
h.run_steps(3);
if let Ok(path) = std::env::var("PRESETS_FOLDER_SNAPSHOT") {
h.render().unwrap().save(path).unwrap();
}
assert_eq!(click(&mut h, "effects.copyPresetsFolderPath"), [expected.as_str()]);
h.run_steps(2);
assert!(h.state().auto.find("effects.copyPresetsFolderPath").is_none());
assert_eq!(serde_json::to_value(&h.state().session.project).unwrap(), before);
assert_eq!(h.state().session.revision, revision);
}
}
#[test]
fn presets_folder_control_command_returns_the_path_or_a_configuration_error() {
let mut app = EffectcraftApp::new(Session::default());
let ctx = egui::Context::default();
let expected = expected_folder();
let mut reply = None;
let mut output = ctx.run_ui(Default::default(), |ui| {
let (request, _rx) = ControlRequest::new("ui.menu.invoke", json!({"id": "effects.copyPresetsFolderPath"}));
reply = match control::handle(&mut app, ui.ctx(), &request) {
control::Outcome::Done(value) => Some(value),
_ => panic!("expected an immediate reply"),
};
});
output.textures_delta.clear();
let copied: Vec<_> = output
.platform_output
.commands
.iter()
.filter_map(|command| match command {
OutputCommand::CopyText(text) => Some(text.as_str()),
_ => None,
})
.collect();
if expected.is_empty() {
assert_eq!(reply, Some(json!({"ok": false, "error": "No presets folder is configured"})));
assert!(copied.is_empty());
assert_eq!(app.ui.status, "No presets folder is configured");
} else {
assert_eq!(reply, Some(json!({"ok": true, "result": {"path": expected}})));
assert_eq!(copied, [expected.as_str()]);
}
}
#[test]
fn shared_preset_rows_search_apply_and_undo_the_selected_definition() {
struct Files(std::path::PathBuf);
impl Drop for Files {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
let files = Files(std::env::temp_dir().join(format!(
"ec-preset-rows-{}-{}",
std::process::id(),
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
)));
std::fs::create_dir(&files.0).unwrap();
let mut entries = Vec::new();
for (name, value) in [("Custom Cache A", 35.0), ("Custom Cache B", 70.0)] {
let path = files.0.join(format!("{name}.ecpreset"));
let preset = json!({"effectcraftAnimationPreset": 1, "name": name, "properties": [{"path": "transform/opacity", "value": {"t": "Scalar", "v": value}}], "effects": []});
std::fs::write(&path, serde_json::to_vec(&preset).unwrap()).unwrap();
entries.push(PresetFile { name: name.into(), path: path.to_string_lossy().into_owned() });
}
let mut session = Session::default();
session.prefs.general.language = "en".into();
session.execute("comp.new", json!({"width": 64, "height": 36, "duration": 2})).unwrap();
session.execute("layer.newText", json!({"text": "Cache"})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_eframe(|_| {
let mut app = EffectcraftApp::new(session);
app.ui.start_screen = false;
app.show_panel(PanelKind::EffectsPresets);
app
});
h.run_steps(3);
let shared = std::sync::Arc::new(entries);
let now = h.ctx.input(|i| i.time);
h.ctx.data_mut(|d| d.insert_temp(egui::Id::new("fx-user-presets"), (now, shared.clone())));
let search = |h: &mut Harness<'_, EffectcraftApp>, text: &str| {
click(h, "effects.search");
h.input_mut().events.push(Event::Key { key: egui::Key::A, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND });
h.input_mut().events.push(Event::Text(text.into()));
h.run_steps(40);
};
search(&mut h, "Typewriter");
assert!(h.state().auto.find("effects.preset.text.typewriter").is_some());
assert!(h.state().auto.find("effects.preset.file.Custom Cache A").is_none());
click(&mut h, "effects.preset.text.typewriter");
click(&mut h, "effects.preset.text.typewriter");
h.run_steps(3);
assert_eq!(h.state().session.active_comp().unwrap().layers[0].props.group("text/animators").unwrap().groups().count(), 1);
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
for (name, expected) in [("Custom Cache A", 35.0), ("Custom Cache B", 70.0)] {
search(&mut h, name);
let id = format!("effects.preset.file.{name}");
let history = h.state().session.history.undo.len();
assert_eq!(h.state().session.active_comp().unwrap().layers[0].props.prop("transform/opacity").unwrap().value.as_f64(), 100.0);
click(&mut h, &id);
click(&mut h, &id);
h.run_steps(3);
assert_eq!(h.state().session.active_comp().unwrap().layers[0].props.prop("transform/opacity").unwrap().value.as_f64(), expected);
assert_eq!(h.state().session.history.undo.len(), history + 1);
if let Ok(dir) = std::env::var("PRESET_CACHE_SNAPSHOTS") {
h.render().unwrap().save(format!("{dir}/{name}.png")).unwrap();
}
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
h.run_steps(2);
assert_eq!(h.state().session.active_comp().unwrap().layers[0].props.prop("transform/opacity").unwrap().value.as_f64(), 100.0);
}
let (_, cached) = h.ctx.data(|d| d.get_temp::<(f64, std::sync::Arc<Vec<PresetFile>>)>(egui::Id::new("fx-user-presets"))).unwrap();
assert!(std::sync::Arc::ptr_eq(&shared, &cached));
assert_eq!(shared.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["Custom Cache A", "Custom Cache B"]);
}