//! Behaviour tests for the client core. //! //! These tests never touch a terminal or a socket. `Screen` applies the //! logical `Effect`s that `ClientCore` emits, so assertions are about what a //! player would see, not about ANSI sequences. use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; #[derive(Default)] struct Screen { lines: Vec, region_start: Option, region_len: usize, prompt: String, } impl Screen { fn apply( &mut self, effects: &[Effect], sent: &mut Vec, interactive: &mut Vec, ) { for effect in effects { match effect { Effect::Print(lines) => self.lines.extend(lines.iter().cloned()), Effect::ReplaceRegion(lines) => { if let Some(start) = self.region_start { self.lines.truncate(start); } else { self.region_start = Some(self.lines.len()); } self.lines.extend(lines.iter().cloned()); self.region_len = lines.len(); } Effect::ClearRegion => { if let Some(start) = self.region_start { self.lines.truncate(start); } self.region_start = None; self.region_len = 0; } Effect::SetPrompt { text, .. } => self.prompt = text.clone(), Effect::SendLine(line) => sent.push(line.clone()), Effect::Interactive(command) => interactive.push(command.clone()), Effect::Quit | Effect::Disconnect | Effect::ListHosts | Effect::ForgetHost(_) => {} } } } fn board(&self) -> &[String] { match self.region_start { Some(start) => &self.lines[start..start + self.region_len], None => &[], } } } struct Harness { core: ClientCore, screen: Screen, sent: Vec, interactive: Vec, } impl Harness { fn new() -> Self { let mut core = ClientCore::new(); core.set_connected(true); core.set_logged_in(Some("alice".to_string())); Self { core, screen: Screen::default(), sent: Vec::new(), interactive: Vec::new(), } } fn apply(&mut self, effects: Vec) { self.screen .apply(&effects, &mut self.sent, &mut self.interactive); } fn key(&mut self, key: Key) { let effects = self.core.handle_key(key); self.apply(effects); if let Some(prompt) = self.core.prompt_effect() { self.apply(vec![prompt]); } } fn line(&mut self, line: &str) { for c in line.chars() { self.key(Key::Char(c)); } self.key(Key::Enter); } fn reply(&mut self, reply: &str) { let effects = self.core.handle_reply(reply); self.apply(effects); if let Some(prompt) = self.core.prompt_effect() { self.apply(vec![prompt]); } } fn board(&self) -> &[String] { self.screen.board() } fn enter_slots(&mut self, bet: &str) { self.line(&format!("play slots {bet}")); self.reply("INFO alice 1 1000"); } } #[test] fn play_shows_a_seven_line_board_and_fetches_the_balance() { let mut h = Harness::new(); h.line("play slots 50"); assert_eq!(h.board().len(), 7); assert!(h.board()[1].contains("Bet: 50 PLN")); assert!(h.sent.contains(&"INFO".to_string())); h.reply("INFO alice 1 1000"); assert_eq!(h.board().len(), 7); assert!(h.board()[1].contains("Balance: 1000 PLN")); } #[test] fn spin_replaces_the_board_and_keeps_the_controls_line() { let mut h = Harness::new(); h.enter_slots("50"); let lines_before_spin = h.screen.lines.len(); h.key(Key::Char(' ')); assert_eq!(h.board().len(), 7); assert!(h.board()[4].contains("Spinning...")); assert!(h.board()[6].contains("[Q] Quit")); assert!(h.sent.iter().any(|line| line == "PLAY_START slots 50")); assert_eq!(h.screen.lines.len(), lines_before_spin); h.reply("GAME_RESULT session-1 60 50 1010 OSS two"); assert_eq!(h.board().len(), 7); assert!(h.board()[2].contains("[ O ] [ S ] [ S ]")); assert!(h.board()[4].contains("Two of a kind! Winnings: 60 PLN")); assert!(h.board()[1].contains("Balance: 1010 PLN")); assert!(h.board()[6].contains("[Q] Quit")); assert_eq!(h.screen.lines.len(), lines_before_spin); } #[test] fn changing_the_bet_after_a_spin_does_not_leave_stale_lines() { let mut h = Harness::new(); h.enter_slots("50"); h.key(Key::Char(' ')); h.reply("GAME_RESULT session-1 60 50 1010 OSS two"); let lines_before_bet = h.screen.lines.len(); h.key(Key::Char('+')); assert_eq!(h.board().len(), 7); assert!(h.board()[1].contains("Bet: 60 PLN")); assert!(h.board()[2].contains("[ O ] [ S ] [ S ]")); assert_eq!(h.screen.lines.len(), lines_before_bet); assert_eq!( h.screen .lines .iter() .filter(|line| line.contains("=== SLOTS ===")) .count(), 1 ); } #[test] fn quitting_clears_the_board_and_returns_the_prompt() { let mut h = Harness::new(); h.enter_slots("50"); h.key(Key::Char('q')); assert!(h.board().is_empty()); assert!(h.screen.lines.iter().any(|line| line == "Game exited.")); assert_eq!(h.screen.prompt, ""); } #[test] fn a_server_error_replaces_the_status_line() { let mut h = Harness::new(); h.enter_slots("50"); h.key(Key::Char(' ')); h.reply("ERROR Insufficient balance"); assert_eq!(h.board().len(), 7); assert!(h.board()[4].contains("ERROR: Insufficient balance")); assert!(h.board()[2].contains("[ ? ]")); assert!(!h.board().iter().any(|line| line.contains("Spinning"))); } #[test] fn an_empty_reply_is_an_error_not_a_silent_spin() { let mut h = Harness::new(); h.enter_slots("50"); h.key(Key::Char(' ')); h.reply(""); assert_eq!(h.board().len(), 7); assert!(h.board()[4].contains("No response from server")); }