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");
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn win_cascade_adds_anim_to_all_52_cards() {
|
||||
let mut app = app_with_anim();
|
||||
|
||||
@@ -882,4 +882,64 @@ mod tests {
|
||||
let mut cursor = events.get_cursor();
|
||||
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