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:
root
2026-04-27 04:37:32 +00:00
parent de01566e47
commit c06458cf80
2 changed files with 136 additions and 0 deletions
+76
View File
@@ -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();
+60
View File
@@ -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);
}
} }