fix(replay): store the deal via upstream card_game serializers (schema v4)
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:
funman300
2026-07-10 09:10:38 -07:00
parent fce0266b47
commit 4cb4212829
20 changed files with 753 additions and 616 deletions
+109 -352
View File
@@ -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!(