Files
effectcraft/crates/ui-egui/tests/it/ui_large_project.rs
T
skylartaylor 9ee853715d M0.37: migrate EffectCraft to egui 0.37
Reconcile main 991904888a and preserve the reviewed lifecycle, IME, rendering and lockfile changes.

Formatting, strict lint, layers, assets and WASM passed. Full CI retained 3,961 passes, 14 baseline-matching GPU failures and 31 ignored executions. The local review commit has explicit approval to retain these documented failures; native/browser acceptance and paired app performance remain open.
2026-10-25 02:01:16 +01:00

121 lines
5.5 KiB
Rust

//! M13.14: the UI with a large project. The Project panel scrolls (only the rows in view are
//! drawn), reveals an item selected elsewhere and renders thumbnails off the UI thread; the
//! Timeline reaches the last of thousands of layers; the Progress panel lists the footage check
//! started by a lazy open.
use effectcraft_engine::Session;
use effectcraft_engine::perf::{LargeSpec, large_project};
use effectcraft_ui_egui::EffectcraftApp;
use effectcraft_ui_egui::dock::PanelKind;
use egui_kittest::Harness;
use serde_json::json;
fn spec() -> LargeSpec {
LargeSpec { comps: 8, main_layers: 1500, comp_layers: 3, footage: 90, sequence_frames: 12, nest_depth: 3, expression_every: 5, ..Default::default() }
}
fn harness(s: Session) -> Harness<'static, EffectcraftApp> {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(s));
h.run_steps(3);
h
}
fn large() -> Session {
let mut s = Session::default();
s.replace_project(large_project(&spec()), None);
s
}
#[test]
fn project_panel_scrolls_and_reveals_the_selection() {
let mut h = harness(large());
let folders: Vec<u64> = h.state().session.project.items.values().filter(|i| i.is_folder()).map(|i| i.id.0).collect();
h.state_mut().ui.project_open_folders.extend(folders);
h.state_mut().show_panel(PanelKind::Project);
h.run_steps(3);
// Far more rows than fit: a scroll bar, and the bottom rows aren't drawn yet.
assert!(h.state().auto.find("project.vscroll").is_some());
let drawn = |h: &Harness<'_, EffectcraftApp>| {
h.state().auto.previous.iter().filter(|e| e.id.starts_with("project.item.") && e.id.matches('.').count() == 2).count()
};
let rows = drawn(&h);
assert!(rows > 5 && rows < 80, "only the rows in view are drawn: {rows}");
// Selecting an item (a command, an agent) scrolls it into view.
let footage =
h.state().session.project.items.values().filter(|i| matches!(i.kind, effectcraft_engine::project::ItemKind::Footage(_))).map(|i| i.id.0).max().unwrap();
h.state_mut().session.execute("project.select", json!({"items": [footage]})).unwrap();
h.run_steps(3);
assert!(h.state().auto.find(&format!("project.item.{footage}")).is_some(), "selected item scrolled into view");
assert!(h.state().ui.project_scroll > 0.0);
// Scrolling to the end shows the last rows.
h.state_mut().ui.project_scroll = 1.0e6;
h.run_steps(2);
let shown: Vec<String> = h.state().auto.previous.iter().filter(|e| e.id.starts_with("project.item.")).map(|e| e.id.clone()).collect();
assert!(!shown.is_empty() && drawn(&h) < 80, "{shown:?}");
}
#[test]
fn comp_thumbnail_renders_off_the_ui_thread() {
let mut h = harness(large());
let main = h.state().session.active_comp_id().unwrap();
h.state_mut().session.execute("project.select", json!({"items": [main.0]})).unwrap();
h.state_mut().show_panel(PanelKind::Project);
// The frame that asks for the thumbnail doesn't wait for it; it lands a few frames later.
let mut landed = false;
for _ in 0..400 {
h.step();
if h.state().auto.find("project.thumbnail").is_some()
&& h.ctx.data(|d| d.get_temp::<(u64, egui::TextureHandle)>(egui::Id::unique(("proj-thumb", main.0)))).is_some()
{
landed = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(landed, "thumbnail texture");
}
#[test]
fn timeline_reaches_the_last_of_many_layers() {
let mut h = harness(large());
h.state_mut().show_panel(PanelKind::Timeline);
h.state_mut().ui.maximized = Some(PanelKind::Timeline);
let last = h.state().session.active_comp().unwrap().layers.last().unwrap().id.0;
let first = h.state().session.active_comp().unwrap().layers[0].id.0;
h.run_steps(2);
assert!(h.state().auto.find(&format!("timeline.layer.{first}.bar")).is_some());
assert!(h.state().auto.find(&format!("timeline.layer.{last}.bar")).is_none(), "off-screen rows aren't drawn");
h.state_mut().ui.timeline.scroll_y = 1.0e7;
h.run_steps(2);
assert!(h.state().auto.find(&format!("timeline.layer.{last}.bar")).is_some());
// Track matte and parent menus list every layer only while open.
let pop = h.state().auto.find(&format!("timeline.layer.{last}.parent")).cloned();
assert!(pop.is_some());
}
#[test]
fn lazy_open_lists_the_footage_check_in_the_progress_panel() {
let path = std::env::temp_dir().join(format!("ec-ui-lazy-open-{}.ecproj", std::process::id()));
let mut s = large();
s.execute("file.saveAs", json!({"path": path.to_string_lossy()})).unwrap();
let mut o = Session { check_footage_on_open: true, ..Default::default() };
o.execute("file.open", json!({"path": path.to_string_lossy()})).unwrap();
assert!(o.jobs().iter().any(|j| j.kind == "footageCheck"));
let mut h = harness(o);
h.state_mut().show_panel(PanelKind::Progress);
// The check finishes in the background; polling applies it: every file is missing.
for _ in 0..500 {
h.step();
if h.state().session.tasks.is_empty() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(h.state().session.tasks.is_empty());
let missing =
h.state().session.project.items.values().filter(|i| matches!(&i.kind, effectcraft_engine::project::ItemKind::Footage(f) if f.missing)).count();
assert_eq!(missing, spec().footage);
assert!(!h.state().session.is_dirty(), "the check doesn't modify the project");
let _ = std::fs::remove_file(path);
}