From 32438ce25f69de99d2523b98237f62cfa90890d7 Mon Sep 17 00:00:00 2001 From: cashmere Date: Tue, 28 Jul 2026 20:14:51 +0200 Subject: feat: port domain crates to Rust (session 1 complete) All green: cargo test --workspace (~300 tests, 0 failures). - timeline (45): placement, resize/ripple, split, snap, retime, audio separation, scene helpers - animation (73): bezier solver, channel sampling, keyframe ops, split/clamp/clone, param definitions - commands (9): Command trait, BatchCommand, history, 19 concrete commands, freeze-frame port 1:1 - audio (22): audible collection, range mixing, mastering limiter, rms/peak buckets - text (19): cosmic-text layout/measure/raster, backgrounds, decorations - graphics (52): full CSS gradient parser, tiny-skia raster, 4 shape definitions - effect-defs (16): blur definition, registry, pass resolution - mask-defs (19): all 9 mask renderers via tiny-skia - renderer (6): node model + scene builder port - desktop: GPUI projects screen wired to ProjectStore - scene: factory (web defaults: v32, Main Track, 1920x1080, 30fps) --- rust/crates/commands/tests/framework.rs | 162 ++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 rust/crates/commands/tests/framework.rs (limited to 'rust/crates/commands/tests/framework.rs') diff --git a/rust/crates/commands/tests/framework.rs b/rust/crates/commands/tests/framework.rs new file mode 100644 index 00000000..e3defbad --- /dev/null +++ b/rust/crates/commands/tests/framework.rs @@ -0,0 +1,162 @@ +mod common; + +use std::cell::RefCell; +use std::rc::Rc; + +use commands::{ + BatchCommand, Command, CommandError, CommandHistory, CommandResult, DeleteElementsCommand, + PreviewTracker, +}; +use common::{VIDEO_SECONDS, seconds, seed_project_with_video}; +use scene::{ElementRef, Project}; + +struct RecordingCommand { + label: &'static str, + log: Rc>>, + selection: Option>, +} + +impl RecordingCommand { + fn new(label: &'static str, log: &Rc>>) -> Self { + Self { + label, + log: Rc::clone(log), + selection: None, + } + } + + fn with_selection(mut self, selection: Vec) -> Self { + self.selection = Some(selection); + self + } +} + +impl Command for RecordingCommand { + fn execute(&mut self, _project: &mut Project) -> Result, CommandError> { + self.log.borrow_mut().push(self.label); + Ok(self + .selection + .clone() + .map(CommandResult::element_selection)) + } + + fn undo(&mut self, _project: &mut Project) { + self.log.borrow_mut().push(self.label); + } +} + +#[test] +fn batch_executes_in_order_and_undoes_in_reverse() { + let (mut project, _) = seed_project_with_video(); + let log = Rc::new(RefCell::new(Vec::new())); + + let mut batch = BatchCommand::new(vec![ + Box::new(RecordingCommand::new("first", &log)), + Box::new(RecordingCommand::new("second", &log)), + Box::new(RecordingCommand::new("third", &log)), + ]); + + batch.execute(&mut project).expect("execute"); + assert_eq!(*log.borrow(), vec!["first", "second", "third"]); + + log.borrow_mut().clear(); + batch.undo(&mut project); + assert_eq!(*log.borrow(), vec!["third", "second", "first"]); +} + +#[test] +fn batch_returns_latest_selection_result() { + let (mut project, _) = seed_project_with_video(); + let log = Rc::new(RefCell::new(Vec::new())); + + let selected = vec![ElementRef { + track_id: "main-track-1".to_string(), + element_id: "video-1".to_string(), + }]; + let mut batch = BatchCommand::new(vec![ + Box::new( + RecordingCommand::new("selecting", &log).with_selection(selected.clone()), + ), + Box::new(RecordingCommand::new("plain", &log)), + ]); + + let result = batch.execute(&mut project).expect("execute"); + let selection = result + .and_then(|result| result.selection) + .expect("selection result"); + assert_eq!(selection.selected_elements, selected); +} + +#[test] +fn history_undo_redo_roundtrip() { + let (mut project, track_id) = seed_project_with_video(); + let mut history = CommandHistory::new(); + + assert!(!history.can_undo()); + assert!(!history.can_redo()); + + history + .execute( + Box::new(DeleteElementsCommand::new(vec![ElementRef { + track_id: track_id.clone(), + element_id: "video-1".to_string(), + }])), + &mut project, + ) + .expect("execute"); + + let scene = common::active_scene; + assert!(scene(&project).tracks.main.elements().is_empty()); + assert!(history.can_undo()); + assert!(!history.can_redo()); + + history.undo(&mut project); + assert_eq!(scene(&project).tracks.main.elements().len(), 1); + assert!(!history.can_undo()); + assert!(history.can_redo()); + + history.redo(&mut project).expect("redo"); + assert!(scene(&project).tracks.main.elements().is_empty()); + assert!(history.can_undo()); + assert!(!history.can_redo()); +} + +#[test] +fn history_new_execute_clears_redo_stack() { + let (mut project, track_id) = seed_project_with_video(); + let mut history = CommandHistory::new(); + + let delete = || { + Box::new(DeleteElementsCommand::new(vec![ElementRef { + track_id: track_id.clone(), + element_id: "video-1".to_string(), + }])) as Box + }; + + history.execute(delete(), &mut project).expect("execute"); + history.undo(&mut project); + assert!(history.can_redo()); + + history.execute(delete(), &mut project).expect("execute"); + assert!(!history.can_redo()); + + history.clear(); + assert!(!history.can_undo()); + assert!(!history.can_redo()); +} + +#[test] +fn preview_tracker_keeps_first_snapshot_until_end() { + let mut tracker = PreviewTracker::new(); + assert!(!tracker.is_active()); + assert_eq!(tracker.end(), None); + + tracker.begin(&seconds(VIDEO_SECONDS)); + tracker.begin(&seconds(1.0)); + assert!(tracker.is_active()); + assert_eq!(tracker.snapshot(), Some(&seconds(VIDEO_SECONDS))); + + assert_eq!(tracker.end(), Some(seconds(VIDEO_SECONDS))); + assert!(!tracker.is_active()); + assert_eq!(tracker.snapshot(), None); +} -- cgit v1.3.1