fix(replay): store the deal via upstream card_game serializers (schema v4)
Test / test (pull_request) Successful in 36m34s
Test / test (pull_request) Successful in 36m34s
Replays previously persisted only seed + moves and re-dealt the board
from the seed at playback time, so any change to the seed->deal mapping
(RNG bumps, upstream upgrades) silently invalidated every existing
replay. Schema v4 instead embeds a SessionRecording - the upstream
card_game Session serde ({config, initial_state, instructions}) - so
playback rebuilds the exact recorded board; seed/draw_mode/mode remain
caption metadata only.
- core: SessionRecording newtype delegating to Session<Klondike> serde;
GameState::recording() / from_recording(); from_instructions_unchecked
fixture helper; serde_json added to dev-deps (tests only)
- data: Replay v4 (recording replaces moves); v1-v3 files rejected by
the existing version gate
- engine: win-recording and sync upload freeze game.recording();
playback rebuilds from the recording; Playing carries the extracted
move list (+ Box<Replay> for clippy large_enum_variant)
- wasm: replay_export() builds the full v4 upload payload so JS never
hand-assembles it (the old game.js path hardcoded schema_version: 2
and corrupted u64 seeds via Math.round); ReplayPlayer::from_json
enforces schema_version == 4 with a descriptive error
- web: game.js/play.html use replay_export; replay.js surfaces player
construction errors in the caption instead of dying silently
- server: mode validation accepts data-carrying GameMode variants
(Difficulty uploads previously 400'd against the String field)
Both replays on prod are May-era v1 rows with empty move lists - every
shared replay was already unplayable; the viewer now says why.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use chrono::NaiveDate;
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameMode};
|
||||
use solitaire_core::{Foundation, KlondikeInstruction, KlondikePile, Tableau};
|
||||
use solitaire_core::{Foundation, KlondikeInstruction, KlondikePile, SessionRecording, Tableau};
|
||||
use solitaire_core::{Rank, Suit};
|
||||
use solitaire_data::Replay;
|
||||
|
||||
@@ -17,9 +17,11 @@ fn synthetic_replay(move_count: usize) -> Replay {
|
||||
120,
|
||||
1_000,
|
||||
NaiveDate::from_ymd_opt(2026, 5, 2).expect("valid date"),
|
||||
(0..move_count)
|
||||
.map(|_| KlondikeInstruction::RotateStock)
|
||||
.collect(),
|
||||
SessionRecording::from_instructions_unchecked(
|
||||
42,
|
||||
DrawStockConfig::DrawOne,
|
||||
(0..move_count).map(|_| KlondikeInstruction::RotateStock),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -93,12 +95,7 @@ fn overlay_spawns_when_playback_starts() {
|
||||
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -117,12 +114,7 @@ fn overlay_progress_text_reflects_cursor() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 5,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 5, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -138,12 +130,7 @@ fn overlay_stop_button_click_clears_playback() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(overlay_root_count(&mut app), 1);
|
||||
@@ -195,12 +182,7 @@ fn floating_chip_spawns_and_despawns_with_overlay() {
|
||||
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(5),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(5), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -232,12 +214,7 @@ fn overlay_despawns_when_playback_returns_to_inactive() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(3),
|
||||
cursor: 1,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(3), 1, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(overlay_root_count(&mut app), 1);
|
||||
@@ -260,12 +237,7 @@ fn overlay_text_changes_on_completed() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(7),
|
||||
cursor: 7,
|
||||
secs_to_next: 0.0,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(7), 7, 0.0, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(banner_text(&mut app), "\u{258C} replay");
|
||||
@@ -314,30 +286,30 @@ fn scrub_pct_covers_state_corners() {
|
||||
assert_eq!(scrub_pct(&ReplayPlaybackState::Inactive), 0.0);
|
||||
assert_eq!(scrub_pct(&ReplayPlaybackState::Completed), 100.0);
|
||||
assert_eq!(
|
||||
scrub_pct(&ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
}),
|
||||
scrub_pct(&ReplayPlaybackState::playing(
|
||||
synthetic_replay(10),
|
||||
0,
|
||||
0.5,
|
||||
false,
|
||||
)),
|
||||
0.0,
|
||||
);
|
||||
assert_eq!(
|
||||
scrub_pct(&ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 5,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
}),
|
||||
scrub_pct(&ReplayPlaybackState::playing(
|
||||
synthetic_replay(10),
|
||||
5,
|
||||
0.5,
|
||||
false,
|
||||
)),
|
||||
50.0,
|
||||
);
|
||||
assert_eq!(
|
||||
scrub_pct(&ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 10,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
}),
|
||||
scrub_pct(&ReplayPlaybackState::playing(
|
||||
synthetic_replay(10),
|
||||
10,
|
||||
0.5,
|
||||
false,
|
||||
)),
|
||||
100.0,
|
||||
);
|
||||
}
|
||||
@@ -367,12 +339,12 @@ fn format_game_caption_covers_state_corners() {
|
||||
// Mar = 31, Apr = 30, May 2 = 122). Synthetic_replay always
|
||||
// uses this date so the assertion is stable.
|
||||
assert_eq!(
|
||||
format_game_caption(&ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 5,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
}),
|
||||
format_game_caption(&ReplayPlaybackState::playing(
|
||||
synthetic_replay(10),
|
||||
5,
|
||||
0.5,
|
||||
false,
|
||||
)),
|
||||
Some("GAME #2026-122".to_string()),
|
||||
);
|
||||
|
||||
@@ -381,12 +353,7 @@ fn format_game_caption_covers_state_corners() {
|
||||
let mut early_january = synthetic_replay(10);
|
||||
early_january.recorded_at = NaiveDate::from_ymd_opt(2026, 1, 5).expect("valid date");
|
||||
assert_eq!(
|
||||
format_game_caption(&ReplayPlaybackState::Playing {
|
||||
replay: early_january,
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
}),
|
||||
format_game_caption(&ReplayPlaybackState::playing(early_january, 0, 0.5, false,)),
|
||||
Some("GAME #2026-005".to_string()),
|
||||
);
|
||||
}
|
||||
@@ -399,12 +366,7 @@ fn overlay_game_caption_shows_replay_date() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(game_caption_text(&mut app), "GAME #2026-122");
|
||||
@@ -431,12 +393,7 @@ fn overlay_scrub_fill_tracks_cursor() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(8),
|
||||
cursor: 2,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(8), 2, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -447,12 +404,7 @@ fn overlay_scrub_fill_tracks_cursor() {
|
||||
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(8),
|
||||
cursor: 6,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(8), 6, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -497,12 +449,7 @@ fn win_move_marker_pct_is_none_for_completed() {
|
||||
fn win_move_marker_pct_is_none_when_replay_lacks_field() {
|
||||
// Synthetic replay constructor leaves win_move_index as None
|
||||
// (legacy / pre-`ab857bb` path).
|
||||
let state = ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
};
|
||||
let state = ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false);
|
||||
assert_eq!(win_move_marker_pct(&state), None);
|
||||
}
|
||||
|
||||
@@ -511,12 +458,12 @@ fn win_move_marker_pct_is_some_at_correct_position() {
|
||||
// 10 moves, win at index 9 → marker sits at 90 % of the track.
|
||||
// Matches the recording semantic: cursor reaches the marker
|
||||
// exactly when the about-to-apply move IS the win move.
|
||||
let state = ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10).with_win_move_index(Some(9)),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
};
|
||||
let state = ReplayPlaybackState::playing(
|
||||
synthetic_replay(10).with_win_move_index(Some(9)),
|
||||
0,
|
||||
0.5,
|
||||
false,
|
||||
);
|
||||
assert_eq!(win_move_marker_pct(&state), Some(90.0));
|
||||
}
|
||||
|
||||
@@ -524,12 +471,12 @@ fn win_move_marker_pct_is_some_at_correct_position() {
|
||||
fn win_move_marker_pct_clamps_to_track_bounds() {
|
||||
// Defensive: if a malformed replay carried `win_move_index >=
|
||||
// total`, the marker must still sit on the track, not past it.
|
||||
let state = ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(5).with_win_move_index(Some(99)),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
};
|
||||
let state = ReplayPlaybackState::playing(
|
||||
synthetic_replay(5).with_win_move_index(Some(99)),
|
||||
0,
|
||||
0.5,
|
||||
false,
|
||||
);
|
||||
assert_eq!(win_move_marker_pct(&state), Some(100.0));
|
||||
}
|
||||
|
||||
@@ -538,12 +485,12 @@ fn marker_spawned_when_replay_has_win_move_index() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(8).with_win_move_index(Some(7)),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(
|
||||
synthetic_replay(8).with_win_move_index(Some(7)),
|
||||
0,
|
||||
0.5,
|
||||
false,
|
||||
),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -559,12 +506,7 @@ fn marker_not_spawned_when_replay_lacks_win_move_index() {
|
||||
// Default constructor → win_move_index: None (legacy replay).
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(8),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(8), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -579,12 +521,12 @@ fn marker_despawns_when_replay_state_returns_to_inactive() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(8).with_win_move_index(Some(7)),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(
|
||||
synthetic_replay(8).with_win_move_index(Some(7)),
|
||||
0,
|
||||
0.5,
|
||||
false,
|
||||
),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(win_marker_count(&mut app), 1);
|
||||
@@ -608,12 +550,12 @@ fn win_move_marker_carries_hc_background_marker() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(8).with_win_move_index(Some(7)),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(
|
||||
synthetic_replay(8).with_win_move_index(Some(7)),
|
||||
0,
|
||||
0.5,
|
||||
false,
|
||||
),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -669,12 +611,7 @@ fn scrub_notches_spawn_with_overlay() {
|
||||
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -693,12 +630,7 @@ fn scrub_notches_carry_high_contrast_background_marker() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -723,12 +655,7 @@ fn scrub_track_carries_high_contrast_background_marker() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -759,12 +686,7 @@ fn scrub_notches_despawn_with_overlay() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(scrub_notch_count(&mut app), 5);
|
||||
@@ -821,12 +743,7 @@ fn scrub_notch_labels_spawn_with_overlay() {
|
||||
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -844,12 +761,7 @@ fn scrub_notch_labels_carry_helper_strings() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -870,12 +782,7 @@ fn scrub_notch_labels_despawn_with_overlay() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(scrub_notch_label_count(&mut app), 5);
|
||||
@@ -947,12 +854,7 @@ fn keybind_footer_spawns_with_overlay() {
|
||||
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -970,12 +872,7 @@ fn keybind_footer_paints_helper_strings() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -999,12 +896,7 @@ fn keybind_footer_carries_high_contrast_border_marker() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -1025,12 +917,7 @@ fn keybind_footer_despawns_with_overlay() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(keybind_footer_count(&mut app), 1);
|
||||
@@ -1054,12 +941,7 @@ fn scrub_notches_spawn_even_without_win_marker() {
|
||||
// Default constructor → win_move_index: None.
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(8),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(8), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -1144,12 +1026,7 @@ fn format_move_body_handles_stock_cycle() {
|
||||
/// `Inactive` → empty.
|
||||
#[test]
|
||||
fn format_move_log_header_covers_state_branches() {
|
||||
let playing = ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 3,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
};
|
||||
let playing = ReplayPlaybackState::playing(synthetic_replay(10), 3, 0.5, false);
|
||||
assert_eq!(
|
||||
format_move_log_header(&playing),
|
||||
"\u{258C} MOVE LOG \u{00B7} 3/10"
|
||||
@@ -1167,24 +1044,14 @@ fn format_move_log_header_covers_state_branches() {
|
||||
/// `replay.moves[N - 1]` and the row reads `"N | ..."`.
|
||||
#[test]
|
||||
fn format_active_move_row_handles_cursor_zero_and_positive() {
|
||||
let cursor_zero = ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
};
|
||||
let cursor_zero = ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false);
|
||||
assert_eq!(
|
||||
format_active_move_row(&cursor_zero),
|
||||
"",
|
||||
"cursor=0 means no move applied yet; row stays empty",
|
||||
);
|
||||
|
||||
let cursor_three = ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 3,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
};
|
||||
let cursor_three = ReplayPlaybackState::playing(synthetic_replay(10), 3, 0.5, false);
|
||||
// synthetic_replay produces all StockClicks, so the body
|
||||
// is "stock cycle". The displayed index is 3 (cursor),
|
||||
// matching the most-recently-applied move at moves[2].
|
||||
@@ -1208,12 +1075,7 @@ fn move_log_panel_spawns_with_overlay() {
|
||||
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -1232,12 +1094,7 @@ fn move_log_panel_header_paints_helper_string() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(8),
|
||||
cursor: 2,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(8), 2, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -1254,12 +1111,7 @@ fn move_log_active_row_repaints_on_cursor_advance() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -1271,12 +1123,7 @@ fn move_log_active_row_repaints_on_cursor_advance() {
|
||||
// Advance cursor to 2 (most-recently-applied move is moves[1]).
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 2,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 2, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -1292,12 +1139,7 @@ fn move_log_active_row_repaints_on_cursor_advance() {
|
||||
/// early in a replay don't paint stale text.
|
||||
#[test]
|
||||
fn format_kth_recent_row_handles_in_range_and_out_of_range() {
|
||||
let state_at_three = ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 3,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
};
|
||||
let state_at_three = ReplayPlaybackState::playing(synthetic_replay(10), 3, 0.5, false);
|
||||
// k=1 → active (most recent applied). cursor=3 → display=3.
|
||||
assert_eq!(
|
||||
format_kth_recent_row(&state_at_three, 1),
|
||||
@@ -1348,12 +1190,7 @@ fn move_log_prev_rows_spawn_with_panel() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 3,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 3, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -1371,12 +1208,7 @@ fn move_log_prev_rows_paint_helper_strings_at_spawn() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 5,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 5, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -1401,12 +1233,7 @@ fn move_log_prev_rows_repaint_on_cursor_advance() {
|
||||
// Start at cursor=2: offset 1 → k=2 → display=1, offset 2 → k=3 → empty (k > cursor).
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 2,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 2, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -1422,12 +1249,7 @@ fn move_log_prev_rows_repaint_on_cursor_advance() {
|
||||
// Advance to cursor=5 — both offsets now have history.
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 5,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 5, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -1465,12 +1287,7 @@ fn move_log_next_row_text_at_offset(app: &mut App, offset: u8) -> String {
|
||||
/// alongside in-range correctness.
|
||||
#[test]
|
||||
fn format_kth_next_row_handles_in_range_and_out_of_range() {
|
||||
let state_at_three = ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 3,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
};
|
||||
let state_at_three = ReplayPlaybackState::playing(synthetic_replay(10), 3, 0.5, false);
|
||||
// k=1 → moves[3], display=4
|
||||
assert_eq!(
|
||||
format_kth_next_row(&state_at_three, 1),
|
||||
@@ -1498,12 +1315,7 @@ fn move_log_next_rows_spawn_with_panel() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 3,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 3, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -1520,12 +1332,7 @@ fn move_log_next_rows_paint_helper_strings_at_spawn() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 5,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 5, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -1549,12 +1356,7 @@ fn move_log_next_rows_underfill_at_replay_end() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 9,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 9, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -1578,12 +1380,7 @@ fn active_row_wrapper_carries_accent_primary_background() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 3,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 3, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -1621,12 +1418,7 @@ fn active_row_text_uses_high_contrast_color_for_highlight() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 3,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 3, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
|
||||
@@ -1647,24 +1439,14 @@ fn active_row_text_uses_high_contrast_color_for_highlight() {
|
||||
/// dropping it has to also update this test.
|
||||
#[test]
|
||||
fn active_row_format_includes_focus_prefix() {
|
||||
let state = ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 5,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
};
|
||||
let state = ReplayPlaybackState::playing(synthetic_replay(10), 5, 0.5, false);
|
||||
let row = format_active_move_row(&state);
|
||||
assert!(
|
||||
row.starts_with('\u{25B6}'),
|
||||
"active-row format must start with ▶ focus marker; got {row:?}",
|
||||
);
|
||||
// Cursor=0 still returns empty, never just the prefix.
|
||||
let cursor_zero = ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
};
|
||||
let cursor_zero = ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false);
|
||||
assert_eq!(
|
||||
format_active_move_row(&cursor_zero),
|
||||
"",
|
||||
@@ -1679,12 +1461,7 @@ fn move_log_panel_despawns_with_overlay() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(10), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(move_log_panel_count(&mut app), 1);
|
||||
@@ -1728,21 +1505,11 @@ fn unique_button<M: Component>(app: &mut App) -> Entity {
|
||||
}
|
||||
|
||||
fn pressed_paused_state(replay_len: usize, cursor: usize) -> ReplayPlaybackState {
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(replay_len),
|
||||
cursor,
|
||||
secs_to_next: 0.5,
|
||||
paused: true,
|
||||
}
|
||||
ReplayPlaybackState::playing(synthetic_replay(replay_len), cursor, 0.5, true)
|
||||
}
|
||||
|
||||
fn running_state(replay_len: usize, cursor: usize) -> ReplayPlaybackState {
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(replay_len),
|
||||
cursor,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
}
|
||||
ReplayPlaybackState::playing(synthetic_replay(replay_len), cursor, 0.5, false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2191,12 +1958,7 @@ fn dim_layer_spawns_and_despawns_with_overlay() {
|
||||
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(5),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(5), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
@@ -2255,12 +2017,7 @@ fn mini_tableau_panel_spawns_and_despawns_with_overlay() {
|
||||
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(5),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
ReplayPlaybackState::playing(synthetic_replay(5), 0, 0.5, false),
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user