test(engine): integration coverage for draw_three_master and zen_winner
Closes the audit gap: the two achievements that previously had only unit-level condition tests now also have full-flow tests that fire a GameWonEvent and assert the unlock state through the same plugin ordering production uses (update_stats_on_win runs before evaluate_on_win, so the freshly bumped stat is visible to the condition closure). Four tests, headless under MinimalPlugins: - draw_three_master_fires_on_tenth_draw_three_win — pre-seed 9 wins, fire a Draw3 win, assert unlock - draw_three_master_does_not_fire_at_nine_wins — pre-seed 8, fire a Draw3 win bumping to 9, assert still locked - zen_winner_fires_on_zen_mode_win — Zen-mode win unlocks the badge - zen_winner_does_not_fire_for_classic_win — Classic win in same fixture leaves it locked After this commit every advertised achievement has an integration test that exercises the production unlock path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -509,6 +509,173 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// draw_three_master integration
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn draw_three_master_fires_on_tenth_draw_three_win() {
|
||||
let mut app = headless_app();
|
||||
|
||||
// Pre-seed nine prior Draw-Three wins. The pending GameWonEvent will
|
||||
// trigger update_stats_on_win first (StatsUpdate runs before
|
||||
// evaluate_on_win), bumping draw_three_wins to 10 — the unlock
|
||||
// threshold for the draw_three_master achievement.
|
||||
app.world_mut().resource_mut::<StatsResource>().0.draw_three_wins = 9;
|
||||
|
||||
// The current game must be in DrawThree mode so update_on_win
|
||||
// increments draw_three_wins (and not draw_one_wins).
|
||||
app.world_mut()
|
||||
.resource_mut::<GameStateResource>()
|
||||
.0
|
||||
.draw_mode = solitaire_core::game_state::DrawMode::DrawThree;
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 500,
|
||||
time_seconds: 240,
|
||||
});
|
||||
app.update();
|
||||
|
||||
// Sanity-check that the win was actually attributed to Draw-Three so
|
||||
// the achievement reads the correct counter.
|
||||
let stats = &app.world().resource::<StatsResource>().0;
|
||||
assert_eq!(stats.draw_three_wins, 10);
|
||||
|
||||
let unlocked = app
|
||||
.world()
|
||||
.resource::<AchievementsResource>()
|
||||
.0
|
||||
.iter()
|
||||
.find(|r| r.id == "draw_three_master")
|
||||
.map(|r| r.unlocked)
|
||||
.unwrap_or(false);
|
||||
assert!(unlocked, "draw_three_master must unlock at the 10th Draw-Three win");
|
||||
|
||||
// Verify the AchievementUnlockedEvent fired for this id.
|
||||
let events = app.world().resource::<Messages<AchievementUnlockedEvent>>();
|
||||
let mut cursor = events.get_cursor();
|
||||
let fired: Vec<String> = cursor.read(events).map(|e| e.0.id.clone()).collect();
|
||||
assert!(
|
||||
fired.contains(&"draw_three_master".to_string()),
|
||||
"AchievementUnlockedEvent for draw_three_master must fire; got {fired:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_three_master_does_not_fire_at_nine_wins() {
|
||||
let mut app = headless_app();
|
||||
|
||||
// Pre-seed eight prior Draw-Three wins. The pending GameWonEvent
|
||||
// brings draw_three_wins to 9 — one short of the threshold.
|
||||
app.world_mut().resource_mut::<StatsResource>().0.draw_three_wins = 8;
|
||||
app.world_mut()
|
||||
.resource_mut::<GameStateResource>()
|
||||
.0
|
||||
.draw_mode = solitaire_core::game_state::DrawMode::DrawThree;
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 500,
|
||||
time_seconds: 240,
|
||||
});
|
||||
app.update();
|
||||
|
||||
let stats = &app.world().resource::<StatsResource>().0;
|
||||
assert_eq!(stats.draw_three_wins, 9);
|
||||
|
||||
let unlocked = app
|
||||
.world()
|
||||
.resource::<AchievementsResource>()
|
||||
.0
|
||||
.iter()
|
||||
.find(|r| r.id == "draw_three_master")
|
||||
.map(|r| r.unlocked)
|
||||
.unwrap_or(false);
|
||||
assert!(!unlocked, "draw_three_master must remain locked at 9 Draw-Three wins");
|
||||
|
||||
let events = app.world().resource::<Messages<AchievementUnlockedEvent>>();
|
||||
let mut cursor = events.get_cursor();
|
||||
let fired: Vec<String> = cursor.read(events).map(|e| e.0.id.clone()).collect();
|
||||
assert!(
|
||||
!fired.contains(&"draw_three_master".to_string()),
|
||||
"draw_three_master must not fire below threshold; got {fired:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// zen_winner integration
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn zen_winner_fires_on_zen_mode_win() {
|
||||
let mut app = headless_app();
|
||||
|
||||
// Put the active game in Zen mode. evaluate_on_win reads
|
||||
// GameStateResource.mode directly to populate last_win_is_zen.
|
||||
app.world_mut()
|
||||
.resource_mut::<GameStateResource>()
|
||||
.0
|
||||
.mode = solitaire_core::game_state::GameMode::Zen;
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 0,
|
||||
time_seconds: 600,
|
||||
});
|
||||
app.update();
|
||||
|
||||
let unlocked = app
|
||||
.world()
|
||||
.resource::<AchievementsResource>()
|
||||
.0
|
||||
.iter()
|
||||
.find(|r| r.id == "zen_winner")
|
||||
.map(|r| r.unlocked)
|
||||
.unwrap_or(false);
|
||||
assert!(unlocked, "zen_winner must unlock when the game mode is Zen");
|
||||
|
||||
let events = app.world().resource::<Messages<AchievementUnlockedEvent>>();
|
||||
let mut cursor = events.get_cursor();
|
||||
let fired: Vec<String> = cursor.read(events).map(|e| e.0.id.clone()).collect();
|
||||
assert!(
|
||||
fired.contains(&"zen_winner".to_string()),
|
||||
"AchievementUnlockedEvent for zen_winner must fire; got {fired:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zen_winner_does_not_fire_for_classic_win() {
|
||||
let mut app = headless_app();
|
||||
|
||||
// Default GameMode is Classic; assert and rely on it.
|
||||
assert_eq!(
|
||||
app.world().resource::<GameStateResource>().0.mode,
|
||||
solitaire_core::game_state::GameMode::Classic
|
||||
);
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 1000,
|
||||
time_seconds: 300,
|
||||
});
|
||||
app.update();
|
||||
|
||||
let unlocked = app
|
||||
.world()
|
||||
.resource::<AchievementsResource>()
|
||||
.0
|
||||
.iter()
|
||||
.find(|r| r.id == "zen_winner")
|
||||
.map(|r| r.unlocked)
|
||||
.unwrap_or(false);
|
||||
assert!(!unlocked, "zen_winner must remain locked outside Zen mode");
|
||||
|
||||
let events = app.world().resource::<Messages<AchievementUnlockedEvent>>();
|
||||
let mut cursor = events.get_cursor();
|
||||
let fired: Vec<String> = cursor.read(events).map(|e| e.0.id.clone()).collect();
|
||||
assert!(
|
||||
!fired.contains(&"zen_winner".to_string()),
|
||||
"zen_winner must not fire on a Classic-mode win; got {fired:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn press(app: &mut App, key: KeyCode) {
|
||||
let mut input = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
|
||||
input.release(key);
|
||||
|
||||
Reference in New Issue
Block a user