test(engine): add missing coverage for settings and animation plugins
settings_plugin: tests for cycle_unlocked (wrap, advance, single-element, unknown-current, empty), volume floor clamping, and O-key screen toggle. animation_plugin: tests for anim_speed_to_secs mapping (Fast < Normal, Instant = 0), toast auto-dismiss on expired timer, toast survival when timer positive, InfoToastEvent spawning a ToastOverlay, and SettingsChangedEvent updating EffectiveSlideDuration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -484,6 +484,82 @@ mod tests {
|
|||||||
assert!(pos.x.abs() < 1e-3, "card must not move during delay period");
|
assert!(pos.x.abs() < 1e-3, "card must not move during delay period");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn anim_speed_fast_is_less_than_normal() {
|
||||||
|
assert!(anim_speed_to_secs(&AnimSpeed::Fast) < anim_speed_to_secs(&AnimSpeed::Normal));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn anim_speed_instant_is_zero() {
|
||||||
|
assert_eq!(anim_speed_to_secs(&AnimSpeed::Instant), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toast_dismissed_after_timer_reaches_zero() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins).add_plugins(AnimationPlugin);
|
||||||
|
|
||||||
|
// Manually spawn a toast with a timer that's already expired.
|
||||||
|
app.world_mut().spawn((ToastOverlay, ToastTimer(-0.001)));
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
// The toast entity must have been despawned.
|
||||||
|
let remaining = app
|
||||||
|
.world_mut()
|
||||||
|
.query::<&ToastTimer>()
|
||||||
|
.iter(app.world())
|
||||||
|
.count();
|
||||||
|
assert_eq!(remaining, 0, "expired toast must be despawned");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toast_not_dismissed_before_timer_reaches_zero() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins).add_plugins(AnimationPlugin);
|
||||||
|
|
||||||
|
// Large positive timer — should survive one update.
|
||||||
|
app.world_mut().spawn((ToastOverlay, ToastTimer(100.0)));
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
let remaining = app
|
||||||
|
.world_mut()
|
||||||
|
.query::<&ToastTimer>()
|
||||||
|
.iter(app.world())
|
||||||
|
.count();
|
||||||
|
assert_eq!(remaining, 1, "unexpired toast must not be despawned");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn info_toast_event_spawns_toast_overlay() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins).add_plugins(AnimationPlugin);
|
||||||
|
|
||||||
|
app.world_mut().send_event(InfoToastEvent("hello".to_string()));
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
let count = app
|
||||||
|
.world_mut()
|
||||||
|
.query::<&ToastOverlay>()
|
||||||
|
.iter(app.world())
|
||||||
|
.count();
|
||||||
|
assert_eq!(count, 1, "InfoToastEvent must spawn exactly one ToastOverlay");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_changed_event_updates_slide_duration() {
|
||||||
|
use solitaire_data::Settings;
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins).add_plugins(AnimationPlugin);
|
||||||
|
|
||||||
|
let mut fast_settings = Settings::default();
|
||||||
|
fast_settings.animation_speed = AnimSpeed::Fast;
|
||||||
|
app.world_mut().send_event(SettingsChangedEvent(fast_settings));
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
let dur = app.world().resource::<EffectiveSlideDuration>().slide_secs;
|
||||||
|
assert!((dur - anim_speed_to_secs(&AnimSpeed::Fast)).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn win_cascade_adds_anim_to_all_52_cards() {
|
fn win_cascade_adds_anim_to_all_52_cards() {
|
||||||
let mut app = app_with_anim();
|
let mut app = app_with_anim();
|
||||||
|
|||||||
@@ -882,4 +882,64 @@ mod tests {
|
|||||||
let mut cursor = events.get_cursor();
|
let mut cursor = events.get_cursor();
|
||||||
assert_eq!(cursor.read(events).count(), 0);
|
assert_eq!(cursor.read(events).count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn volume_clamped_at_zero_does_not_emit_event() {
|
||||||
|
let mut app = headless_app();
|
||||||
|
app.world_mut().resource_mut::<SettingsResource>().0.sfx_volume = 0.0;
|
||||||
|
|
||||||
|
press(&mut app, KeyCode::BracketLeft);
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
let after = app.world().resource::<SettingsResource>().0.sfx_volume;
|
||||||
|
assert!(after >= 0.0, "volume must not go below zero");
|
||||||
|
|
||||||
|
let events = app.world().resource::<Events<SettingsChangedEvent>>();
|
||||||
|
let mut cursor = events.get_cursor();
|
||||||
|
assert_eq!(cursor.read(events).count(), 0, "no event when clamped at floor");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pressing_o_toggles_settings_screen_flag() {
|
||||||
|
let mut app = headless_app();
|
||||||
|
assert!(!app.world().resource::<SettingsScreen>().0, "screen is closed initially");
|
||||||
|
|
||||||
|
press(&mut app, KeyCode::KeyO);
|
||||||
|
app.update();
|
||||||
|
assert!(app.world().resource::<SettingsScreen>().0, "O opens settings");
|
||||||
|
|
||||||
|
press(&mut app, KeyCode::KeyO);
|
||||||
|
app.update();
|
||||||
|
assert!(!app.world().resource::<SettingsScreen>().0, "second O closes settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
// cycle_unlocked pure-function tests
|
||||||
|
#[test]
|
||||||
|
fn cycle_unlocked_wraps_at_end() {
|
||||||
|
// [0, 1, 2] → cycling from 2 wraps to 0
|
||||||
|
assert_eq!(cycle_unlocked(&[0, 1, 2], 2), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_unlocked_advances_normally() {
|
||||||
|
assert_eq!(cycle_unlocked(&[0, 1, 2], 0), 1);
|
||||||
|
assert_eq!(cycle_unlocked(&[0, 1, 2], 1), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_unlocked_single_element_stays() {
|
||||||
|
// Only one unlockable — cycling always returns it.
|
||||||
|
assert_eq!(cycle_unlocked(&[0], 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_unlocked_current_not_in_list_falls_back_to_second() {
|
||||||
|
// current=5 is not in [0,1,2]; falls back to pos=0, so next = unlocked[1] = 1
|
||||||
|
assert_eq!(cycle_unlocked(&[0, 1, 2], 5), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_unlocked_empty_returns_zero() {
|
||||||
|
assert_eq!(cycle_unlocked(&[], 0), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user