274 lines
12 KiB
Rust
274 lines
12 KiB
Rust
//! Headless checks for M13.8: the Preview panel's per-shortcut options drive playback (range,
|
|
//! play from, resolution, overlays, full screen, move time), the preview shortcuts pick their
|
|
//! options, and the Align panel runs `layer.align` / `layer.distribute`.
|
|
|
|
use effectcraft_engine::Session;
|
|
use effectcraft_engine::preview::PreviewShortcut;
|
|
use effectcraft_engine::time::Tick;
|
|
use effectcraft_ui_egui::EffectcraftApp;
|
|
use effectcraft_ui_egui::dock::PanelKind;
|
|
use egui::{Event, Pos2, pos2};
|
|
use egui_kittest::Harness;
|
|
use serde_json::json;
|
|
|
|
fn harness() -> Harness<'static, EffectcraftApp> {
|
|
let mut s = Session::default();
|
|
s.execute("comp.new", json!({"name": "Preview", "width": 320, "height": 180, "frameRate": 24, "duration": 10})).unwrap();
|
|
s.execute("layer.newSolid", json!({"name": "A", "color": "#406080", "width": 40, "height": 30})).unwrap();
|
|
s.execute("layer.newSolid", json!({"name": "B", "color": "#806040", "width": 60, "height": 20})).unwrap();
|
|
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
|
h.run_steps(3);
|
|
h
|
|
}
|
|
|
|
fn invoke(h: &mut Harness<'_, EffectcraftApp>, id: &str, p: serde_json::Value) -> serde_json::Value {
|
|
let ctx = h.ctx.clone();
|
|
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, id, p).unwrap()
|
|
}
|
|
|
|
fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
|
|
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}")).clone();
|
|
let p: Pos2 = pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0);
|
|
h.input_mut().events.push(Event::PointerMoved(p));
|
|
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
|
h.step();
|
|
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
|
h.run_steps(2);
|
|
}
|
|
|
|
fn set(h: &mut Harness<'_, EffectcraftApp>, p: serde_json::Value) {
|
|
h.state_mut().session.execute("playback.settings.set", p).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn preview_panel_edits_the_shown_shortcut() {
|
|
let mut h = harness();
|
|
invoke(&mut h, "window.panel", json!({"panel": "preview"}));
|
|
// The default workspace keeps the Preview panel short: maximize it.
|
|
h.state_mut().toggle_maximize(PanelKind::Preview);
|
|
h.run_steps(3);
|
|
for id in [
|
|
"preview.shortcut",
|
|
"preview.includeVideo",
|
|
"preview.includeAudio",
|
|
"preview.includeOverlays",
|
|
"preview.includeLayerControls",
|
|
"preview.loop",
|
|
"preview.cacheBeforePlayback",
|
|
"preview.range",
|
|
"preview.playFrom",
|
|
"preview.frameRate",
|
|
"preview.skip",
|
|
"preview.resolution",
|
|
"preview.fullScreen",
|
|
"preview.playCachedFrames",
|
|
"preview.moveTimeToPreviewTime",
|
|
] {
|
|
assert!(h.state().auto.find(id).is_some(), "{id}");
|
|
}
|
|
assert!(h.state().session.prefs.preview.spacebar.loop_);
|
|
click(&mut h, "preview.loop");
|
|
assert!(!h.state().session.prefs.preview.spacebar.loop_);
|
|
// Shortcut popup: Shift+Numpad 0; its options are shown and edited from now on.
|
|
click(&mut h, "preview.shortcut");
|
|
click(&mut h, "preview.shortcut.3");
|
|
assert_eq!(h.state().session.prefs.preview.current, PreviewShortcut::ShiftNumpad0);
|
|
click(&mut h, "preview.fullScreen");
|
|
assert!(h.state().session.prefs.preview.shift_numpad0.full_screen);
|
|
assert!(!h.state().session.prefs.preview.spacebar.full_screen);
|
|
// Range popup → Play Around Current Time shows the pre/post-roll.
|
|
click(&mut h, "preview.range");
|
|
click(&mut h, "preview.range.3");
|
|
h.run_steps(2);
|
|
assert!(h.state().auto.find("preview.preRoll").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn playback_follows_the_shortcut_options() {
|
|
let mut h = harness();
|
|
// Numpad 0: Work Area from its start; the time returns when the preview stops.
|
|
h.state_mut().session.execute("comp.workArea", json!({"start": 2.0, "end": 4.0})).unwrap();
|
|
set(&mut h, json!({"shortcut": "numpad0", "moveTimeToPreviewTime": false, "resolution": "quarter", "includeOverlays": false}));
|
|
h.state_mut().session.set_time(Tick::from_seconds_f64(7.0));
|
|
invoke(&mut h, "playback.ramPreview", json!({}));
|
|
let app = h.state();
|
|
assert!(app.playback.playing);
|
|
assert_eq!(app.playback.shortcut, PreviewShortcut::Numpad0);
|
|
let pl = app.playback.plan.unwrap();
|
|
assert_eq!((pl.start, pl.end, pl.first), (48, 95, 48));
|
|
assert_eq!(app.session.time(), Tick::from_seconds_f64(2.0));
|
|
assert_eq!(app.viewer_scale(1.0, 1.0), 0.25);
|
|
assert!(!app.overlays_visible());
|
|
invoke(&mut h, "playback.stop", json!({}));
|
|
assert!(!h.state().playback.playing);
|
|
assert_eq!(h.state().session.time(), Tick::from_seconds_f64(7.0));
|
|
assert!(h.state().overlays_visible());
|
|
|
|
// Spacebar with Full Screen: the viewer is maximized while it plays.
|
|
set(&mut h, json!({"shortcut": "spacebar", "fullScreen": true, "skip": 2}));
|
|
invoke(&mut h, "playback.toggle", json!({}));
|
|
assert_eq!(h.state().ui.maximized, Some(PanelKind::Composition));
|
|
assert_eq!(h.state().playback.plan.unwrap().step, 3);
|
|
invoke(&mut h, "playback.toggle", json!({}));
|
|
assert_eq!(h.state().ui.maximized, None);
|
|
|
|
// Shift+Spacebar from the keyboard plays with its own options.
|
|
let ev = Event::Key { key: egui::Key::Space, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::SHIFT };
|
|
h.input_mut().events.push(ev);
|
|
h.step();
|
|
assert!(h.state().playback.playing);
|
|
assert_eq!(h.state().playback.shortcut, PreviewShortcut::ShiftSpacebar);
|
|
assert_eq!(h.state().playback.plan.unwrap().step, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn cache_before_playback_waits_then_plays_cached() {
|
|
let mut h = harness();
|
|
set(&mut h, json!({"shortcut": "spacebar", "cacheBeforePlayback": true, "range": "entireDuration", "playFrom": "rangeStart"}));
|
|
invoke(&mut h, "playback.toggle", json!({}));
|
|
assert!(h.state().playback.caching);
|
|
// Pressing again while caching ("If caching, play cached frames") stops only when nothing
|
|
// is cached yet, otherwise plays the cached frames.
|
|
h.run_steps(4);
|
|
invoke(&mut h, "playback.toggle", json!({}));
|
|
let st = &h.state().playback;
|
|
assert!(!st.caching);
|
|
if st.playing {
|
|
let pl = st.plan.unwrap();
|
|
assert!(pl.end < 239);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn align_panel_runs_align_and_distribute() {
|
|
let mut h = harness();
|
|
invoke(&mut h, "window.panel", json!({"panel": "align"}));
|
|
h.state_mut().toggle_maximize(PanelKind::Align);
|
|
h.run_steps(3);
|
|
for id in ["align.to", "align.left", "align.vcenter", "distribute.hcenter", "distribute.bottom"] {
|
|
assert!(h.state().auto.find(id).is_some(), "{id}");
|
|
}
|
|
let ids: Vec<u64> = h.state().session.active_comp().unwrap().layers.iter().map(|l| l.id.0).collect();
|
|
h.state_mut().session.execute("layer.select", json!({"layers": ids})).unwrap();
|
|
h.run_steps(2);
|
|
// (x position, width) of A and B (anchors in the middle of the solids).
|
|
let pos = |h: &Harness<'_, EffectcraftApp>| -> Vec<(f64, f64)> {
|
|
let c = h.state().session.active_comp().unwrap();
|
|
["A", "B"]
|
|
.iter()
|
|
.map(|n| {
|
|
let l = c.layers.iter().find(|l| l.name == *n).unwrap();
|
|
let x = l.transform().unwrap().get("position").unwrap().value_at(Tick::ZERO).to_json()[0].as_f64().unwrap();
|
|
(x, if *n == "A" { 40.0 } else { 60.0 })
|
|
})
|
|
.collect()
|
|
};
|
|
let orig = pos(&h);
|
|
click(&mut h, "align.left");
|
|
assert_eq!(pos(&h), vec![(20.0, 40.0), (30.0, 60.0)]);
|
|
// One undo step for the whole alignment.
|
|
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
|
assert_eq!(pos(&h), orig);
|
|
// Align Layers to: Selection — right edges meet at the rightmost.
|
|
click(&mut h, "align.to");
|
|
click(&mut h, "align.to");
|
|
h.state_mut().ui.align_to_selection = true;
|
|
h.run_steps(2);
|
|
assert_eq!(h.state().auto.find("align.to").unwrap().label, "Selection");
|
|
click(&mut h, "align.right");
|
|
let p = pos(&h);
|
|
let right = |(x, w): (f64, f64)| x + w / 2.0;
|
|
let goal = orig.iter().map(|v| right(*v)).fold(f64::NEG_INFINITY, f64::max);
|
|
assert!((right(p[0]) - goal).abs() < 1e-9 && (right(p[1]) - goal).abs() < 1e-9, "{p:?}");
|
|
// Distribute needs three layers: nothing moves.
|
|
click(&mut h, "distribute.hcenter");
|
|
assert_eq!(pos(&h), p);
|
|
}
|
|
|
|
#[test]
|
|
fn cache_before_playback_plays_what_fits_in_ram() {
|
|
let mut h = harness();
|
|
set(&mut h, json!({"shortcut": "spacebar", "cacheBeforePlayback": true, "range": "entireDuration", "playFrom": "rangeStart"}));
|
|
h.run_steps(2);
|
|
// Room for about 12 full-size frames: the 240-frame range can't all be cached.
|
|
h.state().frames.set_budget(320 * 180 * 4 * 12);
|
|
invoke(&mut h, "playback.toggle", json!({}));
|
|
assert!(h.state().playback.caching);
|
|
for _ in 0..600 {
|
|
h.step();
|
|
if !h.state().playback.caching {
|
|
break;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(5));
|
|
}
|
|
let st = &h.state().playback;
|
|
assert!(st.playing && !st.caching, "caching finished and the preview plays");
|
|
let pl = st.plan.unwrap();
|
|
assert!(pl.start == 0 && pl.end < 239 && pl.end >= 9, "cut to what fits: {}..{}", pl.start, pl.end);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_resolution_is_the_same_playing_and_paused() {
|
|
let mut h = harness();
|
|
let paused = h.state().viewer_scale(0.5, 2.0);
|
|
h.state_mut().playback.playing = true;
|
|
assert_eq!(h.state().viewer_scale(0.5, 2.0), paused);
|
|
}
|
|
|
|
#[test]
|
|
fn achieved_frame_rate_and_real_time() {
|
|
let mut p = effectcraft_ui_egui::Playback { playing: true, fps: 24.0, ..Default::default() };
|
|
assert_eq!(p.achieved_fps(), None);
|
|
for i in 0..25 {
|
|
p.frame_shown(i as f64 / 24.0);
|
|
}
|
|
assert!((p.achieved_fps().unwrap() - 24.0).abs() < 1e-9);
|
|
assert_eq!(p.real_time(), Some(true));
|
|
let mut slow = effectcraft_ui_egui::Playback { playing: true, fps: 24.0, ..Default::default() };
|
|
for i in 0..13 {
|
|
slow.frame_shown(i as f64 / 12.0);
|
|
}
|
|
assert_eq!(slow.real_time(), Some(false));
|
|
assert_eq!(slow.shown_at.len(), 13, "only the last second counts");
|
|
}
|
|
|
|
#[test]
|
|
fn cache_frames_when_idle_fills_the_work_area() {
|
|
let mut h = harness();
|
|
let cid = h.state().session.active_comp_id().unwrap();
|
|
let cached = |h: &Harness<'_, EffectcraftApp>| h.state().frames.cached_frames(&h.state().shown_series(cid)).len();
|
|
// Off: only a few frames ahead of the current time are prefetched (the work area is the
|
|
// whole 240 frames).
|
|
for _ in 0..120 {
|
|
h.step();
|
|
std::thread::sleep(std::time::Duration::from_millis(2));
|
|
}
|
|
let before = cached(&h);
|
|
assert!(before < 240, "{before}");
|
|
invoke(&mut h, "playback.cacheWhenIdle", json!({"value": true}));
|
|
let mut n = 0;
|
|
for _ in 0..4000 {
|
|
h.step();
|
|
n = cached(&h);
|
|
if n >= 240 {
|
|
break;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(2));
|
|
}
|
|
assert_eq!(n, 240, "the work area is cached while idle");
|
|
}
|
|
|
|
#[test]
|
|
fn cache_frames_when_idle_survives_an_inverted_work_area() {
|
|
let mut h = harness();
|
|
// Start set past the end: an empty work area (it panicked in `clamp` / `rem_euclid`).
|
|
invoke(&mut h, "comp.workArea", json!({"end": 2.0}));
|
|
invoke(&mut h, "comp.workArea", json!({"start": 5.0}));
|
|
invoke(&mut h, "playback.cacheWhenIdle", json!({"value": true}));
|
|
for _ in 0..150 {
|
|
h.step();
|
|
std::thread::sleep(std::time::Duration::from_millis(2));
|
|
}
|
|
assert!(!h.state().playback.playing);
|
|
}
|