feat(engine): playability improvements — input intelligence, audio, HUD, onboarding (#27–#30, #37, #39–#40, #44, #48–#49)
Task #27: Double-click auto-move — best_destination() finds optimal target (foundation over tableau); handle_double_click() fires MoveRequestEvent. Task #28: Hint system — find_hint() returns first legal from/to/count triple; H key tints the source stack HintHighlight (yellow pulse via tick_hint_highlight). Task #29: No-moves detection — has_legal_moves() checks stock/waste/all face-up cards; check_no_moves system fires InfoToastEvent("No moves available") once per stalemate (debounced so it fires only once until the state changes). Task #30: Forfeit — G key fires ForfeitEvent; StatsPlugin records abandoned game, persists stats, starts a new deal. Task #37: Mute-all (M) and mute-music (Shift+M) toggles; MuteState resource applied in apply_volume_on_change. Task #39: Daily challenge HUD constraint label (time limit / target score). Task #40: Undo-count HUD label; amber colour when undos > 0. Task #44: Win-streak and level line on pause screen. Task #48: Undo sound routes UndoRequestEvent → lib.flip audio channel. Task #49: Onboarding banner rich-text key highlights — D and H rendered as orange KeyHighlightSpan children so they stand out from body text. Also registers CursorPlugin in solitaire_app (tasks #31/#32 wire-up). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -231,6 +231,65 @@ pub async fn delete_account(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use jsonwebtoken::{decode, DecodingKey, Validation};
|
||||
|
||||
const TEST_SECRET: &str = "test_secret_for_unit_tests_only";
|
||||
|
||||
fn decode_token(token: &str) -> Claims {
|
||||
let mut validation = Validation::default();
|
||||
validation.leeway = 60;
|
||||
decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(TEST_SECRET.as_bytes()),
|
||||
&validation,
|
||||
)
|
||||
.unwrap()
|
||||
.claims
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_access_token_decodes_with_correct_claims() {
|
||||
let token = make_access_token("user-123", TEST_SECRET).unwrap();
|
||||
let claims = decode_token(&token);
|
||||
assert_eq!(claims.sub, "user-123");
|
||||
assert_eq!(claims.kind, "access");
|
||||
let now = Utc::now().timestamp() as usize;
|
||||
// expiry should be roughly 24 hours in the future (allow ±60s for test execution)
|
||||
assert!(claims.exp > now + 86_400 - 60);
|
||||
assert!(claims.exp < now + 86_400 + 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_refresh_token_decodes_with_correct_claims() {
|
||||
let token = make_refresh_token("user-456", TEST_SECRET).unwrap();
|
||||
let claims = decode_token(&token);
|
||||
assert_eq!(claims.sub, "user-456");
|
||||
assert_eq!(claims.kind, "refresh");
|
||||
let now = Utc::now().timestamp() as usize;
|
||||
// expiry should be roughly 30 days in the future (allow ±60s for test execution)
|
||||
assert!(claims.exp > now + 30 * 86_400 - 60);
|
||||
assert!(claims.exp < now + 30 * 86_400 + 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_access_token_wrong_secret_fails_decode() {
|
||||
let token = make_access_token("user-789", TEST_SECRET).unwrap();
|
||||
let result = decode::<Claims>(
|
||||
&token,
|
||||
&DecodingKey::from_secret(b"wrong_secret"),
|
||||
&Validation::default(),
|
||||
);
|
||||
assert!(result.is_err(), "decoding with wrong secret must fail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn access_and_refresh_tokens_have_different_kinds() {
|
||||
let access = make_access_token("u", TEST_SECRET).unwrap();
|
||||
let refresh = make_refresh_token("u", TEST_SECRET).unwrap();
|
||||
let a_claims = decode_token(&access);
|
||||
let r_claims = decode_token(&refresh);
|
||||
assert_ne!(a_claims.kind, r_claims.kind);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn username_chars_ok_accepts_alphanumeric_and_underscore() {
|
||||
|
||||
@@ -194,4 +194,24 @@ mod tests {
|
||||
assert!(g.target_score.is_none());
|
||||
assert!(g.max_time_secs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_goal_all_variants_have_sane_ranges() {
|
||||
for variant_idx in 0u64..6 {
|
||||
let g = generate_goal("2026-04-26", variant_idx);
|
||||
assert!(!g.description.is_empty(), "variant {variant_idx}: description must not be empty");
|
||||
if let Some(t) = g.max_time_secs {
|
||||
assert!(
|
||||
(60..=3600).contains(&t),
|
||||
"variant {variant_idx}: max_time_secs {t} outside [60, 3600]"
|
||||
);
|
||||
}
|
||||
if let Some(s) = g.target_score {
|
||||
assert!(
|
||||
(1_000..=10_000).contains(&s),
|
||||
"variant {variant_idx}: target_score {s} outside [1000, 10000]"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user