243 lines
10 KiB
Rust
243 lines
10 KiB
Rust
//! Headless UI tests of the M3.11 menu long tail: Find, Create Search Bin (and its Project panel
|
|
//! rows), Project Settings, Scene Edit Detection, Simplify Sequence, Automate to Sequence,
|
|
//! Normalize Mix Track, Dynamic Audio Waveforms, Reveal Log Files and the System Compatibility
|
|
//! Report, all driven by automation id through the control channel.
|
|
|
|
use std::sync::mpsc::{Sender, channel};
|
|
|
|
use egui_kittest::Harness;
|
|
use filmcraft_engine::Session;
|
|
use filmcraft_engine::project::ItemId;
|
|
use filmcraft_ui_egui::FilmcraftApp;
|
|
use filmcraft_ui_egui::control::ControlRequest;
|
|
use serde_json::{Value, json};
|
|
|
|
struct Driver {
|
|
harness: Harness<'static, FilmcraftApp>,
|
|
tx: Sender<ControlRequest>,
|
|
}
|
|
|
|
impl Driver {
|
|
fn new() -> Self {
|
|
let mut s = Session::default();
|
|
s.execute("file.openDemoProject", json!({})).unwrap();
|
|
let (tx, rx) = channel();
|
|
let mut app = FilmcraftApp::new(s).with_control(rx);
|
|
// never hand paths to the real OS opener from a test
|
|
app.hooks.open_path = Some(Box::new(|_, _| Ok(())));
|
|
let harness = Harness::builder().with_size(egui::vec2(1600.0, 980.0)).with_max_steps(10_000).build_eframe(move |_cc| app);
|
|
let mut d = Driver { harness, tx };
|
|
d.frames(4);
|
|
d
|
|
}
|
|
|
|
fn frames(&mut self, n: usize) {
|
|
for _ in 0..n {
|
|
let ctx = self.harness.ctx.clone();
|
|
let mut raw = std::mem::take(self.harness.input_mut());
|
|
eframe::App::raw_input_hook(self.harness.state_mut(), &ctx, &mut raw);
|
|
*self.harness.input_mut() = raw;
|
|
self.harness.step();
|
|
}
|
|
}
|
|
|
|
fn call(&mut self, method: &str, params: Value) -> Value {
|
|
let (req, reply) = ControlRequest::new(method, params.clone());
|
|
self.tx.send(req).unwrap();
|
|
for _ in 0..600 {
|
|
self.frames(1);
|
|
if let Ok(v) = reply.try_recv() {
|
|
return v;
|
|
}
|
|
}
|
|
panic!("no reply to {method} {params}");
|
|
}
|
|
|
|
fn ok(&mut self, method: &str, params: Value) -> Value {
|
|
let v = self.call(method, params.clone());
|
|
assert_eq!(v["ok"], json!(true), "{method} {params} failed: {v}");
|
|
v["result"].clone()
|
|
}
|
|
|
|
fn exec(&mut self, command: &str, params: Value) -> Value {
|
|
self.ok("engine.execute", json!({"command": command, "params": params}))
|
|
}
|
|
|
|
fn menu(&mut self, id: &str) -> Value {
|
|
let r = self.ok("ui.menu.invoke", json!({"id": id}));
|
|
self.frames(3);
|
|
r
|
|
}
|
|
|
|
fn click(&mut self, id: &str) {
|
|
self.ok("ui.click", json!({"id": id}));
|
|
self.frames(3);
|
|
}
|
|
|
|
fn has(&mut self, id: &str) -> bool {
|
|
let v = self.ok("ui.elements", json!({"prefix": id}));
|
|
v.as_array().unwrap().iter().any(|e| e["id"] == json!(id))
|
|
}
|
|
|
|
fn app(&mut self) -> &mut FilmcraftApp {
|
|
self.harness.state_mut()
|
|
}
|
|
|
|
fn item_named(&mut self, name: &str) -> u64 {
|
|
self.app().session.project.items.values().find(|i| i.name == name).unwrap().id.0
|
|
}
|
|
|
|
fn dialog_open(&mut self) -> bool {
|
|
self.app().ui.extras.dialog.is_some()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn menu_items_toggles_and_help() {
|
|
let mut d = Driver::new();
|
|
let menus = d.ok("ui.menu.list", json!({}));
|
|
let find = |id: &str| menus.as_array().unwrap().iter().find(|m| m["id"] == id).cloned().unwrap_or_else(|| panic!("{id} not in menus"));
|
|
assert_eq!(find("view.dynamicAudioWaveforms")["checked"], true);
|
|
assert_eq!(find("help.systemCompatibilityReport")["path"], json!(["Help"]));
|
|
assert_eq!(find("file.mediaProperties")["path"], json!(["File", "Get Media File Properties for"]));
|
|
assert_eq!(find("clip.sceneEditDetection")["label"], "Scene Edit Detection…");
|
|
// Dynamic Audio Waveforms toggles
|
|
d.menu("view.dynamicAudioWaveforms");
|
|
assert!(!d.app().ui.extras.dynamic_waveforms);
|
|
let menus = d.ok("ui.menu.list", json!({}));
|
|
assert_eq!(menus.as_array().unwrap().iter().find(|m| m["id"] == "view.dynamicAudioWaveforms").unwrap()["checked"], false);
|
|
// System Compatibility Report
|
|
assert_eq!(d.menu("help.systemCompatibilityReport")["dialog"], "systemReport");
|
|
assert!(d.has("systemReport.ok"));
|
|
d.click("systemReport.ok");
|
|
assert!(!d.dialog_open());
|
|
// Reveal Log Files writes this session's log and reveals it
|
|
let r = d.menu("help.revealLogFiles");
|
|
let path = r["path"].as_str().unwrap().to_string();
|
|
assert!(std::fs::read_to_string(&path).unwrap().contains("\"os\""));
|
|
assert_eq!(d.app().ui.extras.opened.last(), Some(&path));
|
|
let _ = std::fs::remove_file(&path);
|
|
// Import from Media Browser needs a Media Browser selection
|
|
assert_eq!(d.call("ui.menu.invoke", json!({"id": "file.importFromMediaBrowser"}))["ok"], false);
|
|
}
|
|
|
|
#[test]
|
|
fn find_dialog_and_search_bins() {
|
|
let mut d = Driver::new();
|
|
d.ok("ui.set", json!({"focused": "Project"}));
|
|
assert_eq!(d.menu("edit.find")["dialog"], "find");
|
|
for id in
|
|
["find.scope.project", "find.row.0.column", "find.row.0.operator", "find.row.0.text", "find.row.1.text", "find.match.any", "find.ok", "find.cancel"]
|
|
{
|
|
assert!(d.has(id), "{id}");
|
|
}
|
|
d.ok(
|
|
"ui.set",
|
|
json!({"menuDialog": {"rows": [{"column": "Name", "operator": "beginsWith", "text": "Neon"}, {"column": "Name", "operator": "contains", "text": ""}]}}),
|
|
);
|
|
d.click("find.ok");
|
|
let neon = d.item_named("Neon_Loop.mov");
|
|
assert_eq!(d.app().session.state.project_selection, vec![ItemId(neon)]);
|
|
assert!(d.dialog_open(), "Find stays open until Done");
|
|
d.click("find.cancel");
|
|
assert!(!d.dialog_open());
|
|
// timeline Find from the Timeline panel
|
|
d.ok("ui.set", json!({"focused": "Timeline"}));
|
|
d.menu("edit.find");
|
|
assert_eq!(d.app().ui.extras.dialog.as_ref().unwrap().params["scope"], "timeline");
|
|
d.ok("ui.set", json!({"menuDialog": {"rows": [{"column": "Name", "operator": "contains", "text": "forest"}]}}));
|
|
d.click("find.ok");
|
|
assert_eq!(d.app().session.state.selection.len(), 1);
|
|
d.click("find.cancel");
|
|
|
|
// Create Search Bin, shown in the Project panel's list view with its live contents
|
|
d.app().ui.project_view = filmcraft_ui_egui::state::ProjectView::List;
|
|
d.ok("ui.panel.show", json!({"panel": "Project"}));
|
|
d.menu("file.newSearchBin");
|
|
assert!(d.has("searchBin.text") && d.has("searchBin.column"));
|
|
d.ok("ui.set", json!({"menuDialog": {"text": "ocean", "name": "Ocean shots"}}));
|
|
d.click("searchBin.ok");
|
|
let bin = d.app().session.project.search_bins[0].id.0;
|
|
d.frames(3);
|
|
assert!(d.has(&format!("project.searchBin.{bin}")));
|
|
d.click(&format!("project.searchBin.{bin}"));
|
|
let ocean = d.item_named("Ocean_Sunset.mp4");
|
|
assert!(d.app().ui.expanded_bins.contains(&bin));
|
|
let items = d.ok("ui.elements", json!({"prefix": format!("project.item.{ocean}")}));
|
|
assert!(items.as_array().unwrap().len() >= 2, "listed in its bin and in the search bin: {items}");
|
|
}
|
|
|
|
#[test]
|
|
fn project_settings_dialog() {
|
|
let mut d = Driver::new();
|
|
assert_eq!(d.menu("file.projectSettings.scratchDisks")["dialog"], "projectSettings");
|
|
assert_eq!(d.app().ui.extras.dialog.as_ref().unwrap().params["tab"], "scratchDisks");
|
|
for id in ["projectSettings.scratch.captured", "projectSettings.scratch.videoPreviews.browse", "projectSettings.scratch.autoSave.same"] {
|
|
assert!(d.has(id), "{id}");
|
|
}
|
|
d.click("projectSettings.tab.general");
|
|
for id in ["projectSettings.renderer", "projectSettings.videoDisplay", "projectSettings.titleSafe.horizontal", "projectSettings.captureFormat"] {
|
|
assert!(d.has(id), "{id}");
|
|
}
|
|
d.ok(
|
|
"ui.set",
|
|
json!({"menuDialog": {"titleSafe": [15.0, 12.0], "renderer": filmcraft_engine::project_tools::RENDERER_SOFTWARE, "captured": "/tmp/fc-captured"}}),
|
|
);
|
|
d.click("projectSettings.tab.ingest");
|
|
assert!(d.has("projectSettings.ingest.enabled"));
|
|
d.click("projectSettings.ok");
|
|
assert!(!d.dialog_open());
|
|
let st = d.app().session.project.settings.clone();
|
|
assert_eq!(st.title_safe, (15.0, 12.0));
|
|
assert_eq!(st.renderer, filmcraft_engine::project_tools::RENDERER_SOFTWARE);
|
|
assert_eq!(st.scratch.captured.as_deref(), Some("/tmp/fc-captured"));
|
|
assert!(filmcraft_ui_egui::panels::menu_dialogs::software_renderer(d.app()));
|
|
}
|
|
|
|
#[test]
|
|
fn clip_and_sequence_dialogs() {
|
|
let mut d = Driver::new();
|
|
// Scene Edit Detection needs a selected video clip
|
|
assert_eq!(d.call("ui.menu.invoke", json!({"id": "clip.sceneEditDetection"}))["ok"], false);
|
|
let c = d.app().session.active_sequence().unwrap().video_tracks[0].items[0].id.0;
|
|
d.exec("timeline.select", json!({"clips": [c]}));
|
|
d.menu("clip.sceneEditDetection");
|
|
for id in ["sceneDetect.applyCuts", "sceneDetect.createSubclips", "sceneDetect.generateMarkers", "sceneDetect.sensitivity", "sceneDetect.ok"] {
|
|
assert!(d.has(id), "{id}");
|
|
}
|
|
d.click("sceneDetect.cancel");
|
|
assert!(!d.dialog_open());
|
|
// Normalize Mix Track
|
|
d.menu("sequence.normalizeMixTrack");
|
|
d.ok("ui.set", json!({"menuDialog": {"db": -6.0}}));
|
|
d.click("normalizeMix.ok");
|
|
assert!(!d.dialog_open());
|
|
assert!(d.app().session.active_sequence().unwrap().master_volume_db != 0.0);
|
|
// Simplify Sequence
|
|
let n = d.app().session.project.items.len();
|
|
d.menu("sequence.simplify");
|
|
assert!(d.has("simplify.keep.audio") && d.has("simplify.closeGaps"));
|
|
d.click("simplify.closeGaps");
|
|
d.click("simplify.ok");
|
|
assert_eq!(d.app().session.project.items.len(), n + 1);
|
|
let active = d.app().session.state.active_sequence.unwrap();
|
|
assert_eq!(d.app().session.project.item(active).unwrap().name, "Main Edit (Simplified)");
|
|
// Automate to Sequence into a new sequence
|
|
d.exec("file.newSequence", json!({"name": "Auto"}));
|
|
let ids: Vec<u64> = ["Ocean_Sunset.mp4", "Misty_Forest.mp4"].iter().map(|n| d.item_named(n)).collect();
|
|
d.exec("project.select", json!({"items": ids}));
|
|
assert_eq!(d.menu("clip.automateToSequence")["dialog"], "automate");
|
|
d.click("automate.method.overwrite");
|
|
d.click("automate.ordering.selection");
|
|
d.click("automate.ok");
|
|
assert!(!d.dialog_open());
|
|
let q = d.app().session.active_sequence().unwrap().clone();
|
|
assert_eq!(q.video_tracks[0].items.iter().map(|i| i.item.0).collect::<Vec<_>>(), ids);
|
|
// Get Media File Properties for ▸ Selection shows the probe info
|
|
d.exec("project.select", json!({"items": [ids[0]]}));
|
|
assert_eq!(d.menu("file.mediaProperties")["dialog"], "properties");
|
|
assert_eq!(d.app().ui.extras.dialog.as_ref().unwrap().info[0]["video"]["width"], 1920);
|
|
d.click("properties.ok");
|
|
assert!(!d.dialog_open());
|
|
}
|