use float_cmp::assert_approx_eq;
use super::{Hold, Parallel, Sequence, Timeline, TimelineStep, Track};
use crate::{
keyframes::KeyframesBuilder,
property::{OPACITY, PropertySnapshot, PropertySpec, PropertyValue, SCALE, WIDTH},
timing::{Delay, Direction, Duration, Easing, Timing},
};
fn track(
spec: PropertySpec<crate::property::Scalar>,
duration_ms: f64,
from: f32,
to: f32,
) -> Track {
Track::from(spec, from)
.to(to)
.duration(Duration::from_millis(duration_ms))
.finish()
}
fn scalar(snapshot: &PropertySnapshot, spec: PropertySpec<crate::property::Scalar>) -> f32 {
let Some(entry) = snapshot.find_property(&spec.raw()) else {
panic!("expected scalar property {}", spec.raw().key().name());
};
let PropertyValue::Scalar(value) = entry.value() else {
panic!("expected scalar value");
};
*value
}
#[test]
fn timeline_starts_empty_and_samples_nothing() {
let timeline = Timeline::new().with_name("empty");
assert_eq!(timeline.name(), Some("empty"));
assert!(timeline.root().steps().is_empty());
assert_eq!(timeline.total_duration(), Some(Duration::ZERO));
assert_eq!(timeline.sample_at(Duration::ZERO), None);
assert_eq!(timeline.completion_snapshot(), None);
}
#[test]
fn track_preserves_keyframe_timing_options() {
let keyframes = KeyframesBuilder::new()
.with_timing(
Timing::new(100.0)
.with_delay(Delay::from_millis(25.0))
.with_easing(Easing::EaseOut),
)
.opacity(0.0, 0.0)
.opacity(1.0, 1.0)
.finish();
let track = Track::new(keyframes).with_name("fade");
assert_eq!(track.name(), Some("fade"));
assert_eq!(track.total_duration(), Some(Duration::from_millis(125.0)));
let sampled = track
.sample_at(Duration::from_millis(75.0))
.expect("active sample after delay");
assert_approx_eq!(
f32,
scalar(&sampled, OPACITY),
Easing::EaseOut.value(0.5),
epsilon = 1e-5
);
}
#[test]
fn sequence_duration_sums_steps_and_hold_keeps_previous_snapshot() {
let sequence = Sequence::new()
.track(track(OPACITY, 100.0, 0.0, 1.0))
.hold(Duration::from_millis(50.0))
.track(track(SCALE, 100.0, 1.0, 2.0));
assert_eq!(sequence.steps().len(), 3);
assert_eq!(
sequence.total_duration(),
Some(Duration::from_millis(250.0))
);
let held = sequence
.sample_at(Duration::from_millis(125.0))
.expect("hold should expose previous completion");
assert_approx_eq!(f32, scalar(&held, OPACITY), 1.0, epsilon = 1e-5);
assert_eq!(held.find_property(&SCALE.raw()), None);
let second = sequence
.sample_at(Duration::from_millis(200.0))
.expect("second track sample");
assert_approx_eq!(f32, scalar(&second, SCALE), 1.5, epsilon = 1e-5);
}
#[test]
fn parallel_duration_uses_longest_step_and_merges_properties() {
let parallel = Parallel::from_steps([
track(OPACITY, 100.0, 0.0, 1.0).into(),
track(SCALE, 200.0, 1.0, 3.0).into(),
Hold::new(Duration::from_millis(250.0)).into(),
]);
assert_eq!(
parallel.total_duration(),
Some(Duration::from_millis(250.0))
);
let sampled = parallel
.sample_at(Duration::from_millis(50.0))
.expect("parallel sample");
assert_approx_eq!(f32, scalar(&sampled, OPACITY), 0.5, epsilon = 1e-5);
assert_approx_eq!(f32, scalar(&sampled, SCALE), 1.5, epsilon = 1e-5);
}
#[test]
fn parallel_later_steps_override_same_property_inside_group() {
let parallel = Parallel::from_steps([
track(OPACITY, 100.0, 0.0, 1.0).into(),
track(OPACITY, 100.0, 10.0, 20.0).into(),
]);
let sampled = parallel
.sample_at(Duration::from_millis(50.0))
.expect("parallel sample");
assert_approx_eq!(f32, scalar(&sampled, OPACITY), 15.0, epsilon = 1e-5);
}
#[test]
fn timeline_builder_composes_nested_sequence_and_parallel_steps() {
let timeline = Timeline::parallel([
Sequence::new()
.track(track(OPACITY, 100.0, 0.0, 1.0))
.hold(Duration::from_millis(25.0))
.into(),
Parallel::new()
.track(track(SCALE, 150.0, 1.0, 2.0))
.track(track(WIDTH, 150.0, 100.0, 200.0))
.into(),
]);
assert_eq!(timeline.root().steps().len(), 1);
assert_eq!(
timeline.total_duration(),
Some(Duration::from_millis(150.0))
);
let sampled = timeline
.sample_at(Duration::from_millis(75.0))
.expect("timeline sample");
assert_approx_eq!(f32, scalar(&sampled, OPACITY), 0.75, epsilon = 1e-5);
assert_approx_eq!(f32, scalar(&sampled, SCALE), 1.5, epsilon = 1e-5);
assert_approx_eq!(f32, scalar(&sampled, WIDTH), 150.0, epsilon = 1e-5);
}
#[test]
fn completion_snapshot_merges_final_state_from_all_visible_steps() {
let timeline = Timeline::sequence([
TimelineStep::from(track(OPACITY, 100.0, 0.0, 1.0)),
TimelineStep::from(track(SCALE, 100.0, 1.0, 2.0)),
]);
let completed = timeline.completion_snapshot().expect("completion snapshot");
assert_approx_eq!(f32, scalar(&completed, OPACITY), 1.0, epsilon = 1e-5);
assert_approx_eq!(f32, scalar(&completed, SCALE), 2.0, epsilon = 1e-5);
}
#[test]
fn completion_snapshot_respects_track_direction() {
let track = Track::new(
KeyframesBuilder::new()
.with_timing(Timing::new(100.0).with_direction(Direction::Reverse))
.opacity(0.0, 0.0)
.opacity(1.0, 1.0)
.finish(),
);
let completed = track.completion_snapshot().expect("completion snapshot");
assert_approx_eq!(f32, scalar(&completed, OPACITY), 0.0, epsilon = 1e-5);
}
#[test]
fn hold_keeps_direction_aware_completion_snapshot() {
let sequence = Sequence::new()
.track(Track::new(
KeyframesBuilder::new()
.with_timing(Timing::new(100.0).with_direction(Direction::Reverse))
.opacity(0.0, 0.0)
.opacity(1.0, 1.0)
.finish(),
))
.hold(Duration::from_millis(50.0));
let held = sequence
.sample_at(Duration::from_millis(125.0))
.expect("hold should expose previous completion");
assert_approx_eq!(f32, scalar(&held, OPACITY), 0.0, epsilon = 1e-5);
}