diff options
Diffstat (limited to '')
| -rw-r--r-- | tests/client_core.rs | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/tests/client_core.rs b/tests/client_core.rs new file mode 100644 index 0000000..3dbbfe5 --- /dev/null +++ b/tests/client_core.rs | |||
| @@ -0,0 +1,216 @@ | |||
| 1 | //! Behaviour tests for the client core. | ||
| 2 | //! | ||
| 3 | //! These tests never touch a terminal or a socket. `Screen` applies the | ||
| 4 | //! logical `Effect`s that `ClientCore` emits, so assertions are about what a | ||
| 5 | //! player would see, not about ANSI sequences. | ||
| 6 | |||
| 7 | use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; | ||
| 8 | |||
| 9 | #[derive(Default)] | ||
| 10 | struct Screen { | ||
| 11 | lines: Vec<String>, | ||
| 12 | region_start: Option<usize>, | ||
| 13 | region_len: usize, | ||
| 14 | prompt: String, | ||
| 15 | } | ||
| 16 | |||
| 17 | impl Screen { | ||
| 18 | fn apply( | ||
| 19 | &mut self, | ||
| 20 | effects: &[Effect], | ||
| 21 | sent: &mut Vec<String>, | ||
| 22 | interactive: &mut Vec<InteractiveCommand>, | ||
| 23 | ) { | ||
| 24 | for effect in effects { | ||
| 25 | match effect { | ||
| 26 | Effect::Print(lines) => self.lines.extend(lines.iter().cloned()), | ||
| 27 | Effect::ReplaceRegion(lines) => { | ||
| 28 | if let Some(start) = self.region_start { | ||
| 29 | self.lines.truncate(start); | ||
| 30 | } else { | ||
| 31 | self.region_start = Some(self.lines.len()); | ||
| 32 | } | ||
| 33 | self.lines.extend(lines.iter().cloned()); | ||
| 34 | self.region_len = lines.len(); | ||
| 35 | } | ||
| 36 | Effect::ClearRegion => { | ||
| 37 | if let Some(start) = self.region_start { | ||
| 38 | self.lines.truncate(start); | ||
| 39 | } | ||
| 40 | self.region_start = None; | ||
| 41 | self.region_len = 0; | ||
| 42 | } | ||
| 43 | Effect::SetPrompt { text, .. } => self.prompt = text.clone(), | ||
| 44 | Effect::SendLine(line) => sent.push(line.clone()), | ||
| 45 | Effect::Interactive(command) => interactive.push(command.clone()), | ||
| 46 | Effect::Quit | ||
| 47 | | Effect::Disconnect | ||
| 48 | | Effect::ListHosts | ||
| 49 | | Effect::ForgetHost(_) => {} | ||
| 50 | } | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | fn board(&self) -> &[String] { | ||
| 55 | match self.region_start { | ||
| 56 | Some(start) => &self.lines[start..start + self.region_len], | ||
| 57 | None => &[], | ||
| 58 | } | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | struct Harness { | ||
| 63 | core: ClientCore, | ||
| 64 | screen: Screen, | ||
| 65 | sent: Vec<String>, | ||
| 66 | interactive: Vec<InteractiveCommand>, | ||
| 67 | } | ||
| 68 | |||
| 69 | impl Harness { | ||
| 70 | fn new() -> Self { | ||
| 71 | let mut core = ClientCore::new(); | ||
| 72 | core.set_connected(true); | ||
| 73 | core.set_logged_in(Some("alice".to_string())); | ||
| 74 | Self { | ||
| 75 | core, | ||
| 76 | screen: Screen::default(), | ||
| 77 | sent: Vec::new(), | ||
| 78 | interactive: Vec::new(), | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | fn apply(&mut self, effects: Vec<Effect>) { | ||
| 83 | self.screen | ||
| 84 | .apply(&effects, &mut self.sent, &mut self.interactive); | ||
| 85 | } | ||
| 86 | |||
| 87 | fn key(&mut self, key: Key) { | ||
| 88 | let effects = self.core.handle_key(key); | ||
| 89 | self.apply(effects); | ||
| 90 | if let Some(prompt) = self.core.prompt_effect() { | ||
| 91 | self.apply(vec![prompt]); | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | fn line(&mut self, line: &str) { | ||
| 96 | for c in line.chars() { | ||
| 97 | self.key(Key::Char(c)); | ||
| 98 | } | ||
| 99 | self.key(Key::Enter); | ||
| 100 | } | ||
| 101 | |||
| 102 | fn reply(&mut self, reply: &str) { | ||
| 103 | let effects = self.core.handle_reply(reply); | ||
| 104 | self.apply(effects); | ||
| 105 | if let Some(prompt) = self.core.prompt_effect() { | ||
| 106 | self.apply(vec![prompt]); | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | fn board(&self) -> &[String] { | ||
| 111 | self.screen.board() | ||
| 112 | } | ||
| 113 | |||
| 114 | fn enter_slots(&mut self, bet: &str) { | ||
| 115 | self.line(&format!("play slots {bet}")); | ||
| 116 | self.reply("INFO alice 1 1000"); | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | #[test] | ||
| 121 | fn play_shows_a_seven_line_board_and_fetches_the_balance() { | ||
| 122 | let mut h = Harness::new(); | ||
| 123 | h.line("play slots 50"); | ||
| 124 | |||
| 125 | assert_eq!(h.board().len(), 7); | ||
| 126 | assert!(h.board()[1].contains("Bet: 50 PLN")); | ||
| 127 | assert!(h.sent.contains(&"INFO".to_string())); | ||
| 128 | |||
| 129 | h.reply("INFO alice 1 1000"); | ||
| 130 | assert_eq!(h.board().len(), 7); | ||
| 131 | assert!(h.board()[1].contains("Balance: 1000 PLN")); | ||
| 132 | } | ||
| 133 | |||
| 134 | #[test] | ||
| 135 | fn spin_replaces_the_board_and_keeps_the_controls_line() { | ||
| 136 | let mut h = Harness::new(); | ||
| 137 | h.enter_slots("50"); | ||
| 138 | let lines_before_spin = h.screen.lines.len(); | ||
| 139 | |||
| 140 | h.key(Key::Char(' ')); | ||
| 141 | assert_eq!(h.board().len(), 7); | ||
| 142 | assert!(h.board()[4].contains("Spinning...")); | ||
| 143 | assert!(h.board()[6].contains("[Q] Quit")); | ||
| 144 | assert!(h.sent.iter().any(|line| line == "PLAY_START slots 50")); | ||
| 145 | assert_eq!(h.screen.lines.len(), lines_before_spin); | ||
| 146 | |||
| 147 | h.reply("GAME_RESULT session-1 60 50 1010 OSS two"); | ||
| 148 | assert_eq!(h.board().len(), 7); | ||
| 149 | assert!(h.board()[2].contains("[ O ] [ S ] [ S ]")); | ||
| 150 | assert!(h.board()[4].contains("Two of a kind! Winnings: 60 PLN")); | ||
| 151 | assert!(h.board()[1].contains("Balance: 1010 PLN")); | ||
| 152 | assert!(h.board()[6].contains("[Q] Quit")); | ||
| 153 | assert_eq!(h.screen.lines.len(), lines_before_spin); | ||
| 154 | } | ||
| 155 | |||
| 156 | #[test] | ||
| 157 | fn changing_the_bet_after_a_spin_does_not_leave_stale_lines() { | ||
| 158 | let mut h = Harness::new(); | ||
| 159 | h.enter_slots("50"); | ||
| 160 | h.key(Key::Char(' ')); | ||
| 161 | h.reply("GAME_RESULT session-1 60 50 1010 OSS two"); | ||
| 162 | let lines_before_bet = h.screen.lines.len(); | ||
| 163 | |||
| 164 | h.key(Key::Char('+')); | ||
| 165 | |||
| 166 | assert_eq!(h.board().len(), 7); | ||
| 167 | assert!(h.board()[1].contains("Bet: 60 PLN")); | ||
| 168 | assert!(h.board()[2].contains("[ O ] [ S ] [ S ]")); | ||
| 169 | assert_eq!(h.screen.lines.len(), lines_before_bet); | ||
| 170 | assert_eq!( | ||
| 171 | h.screen | ||
| 172 | .lines | ||
| 173 | .iter() | ||
| 174 | .filter(|line| line.contains("=== SLOTS ===")) | ||
| 175 | .count(), | ||
| 176 | 1 | ||
| 177 | ); | ||
| 178 | } | ||
| 179 | |||
| 180 | #[test] | ||
| 181 | fn quitting_clears_the_board_and_returns_the_prompt() { | ||
| 182 | let mut h = Harness::new(); | ||
| 183 | h.enter_slots("50"); | ||
| 184 | |||
| 185 | h.key(Key::Char('q')); | ||
| 186 | |||
| 187 | assert!(h.board().is_empty()); | ||
| 188 | assert!(h.screen.lines.iter().any(|line| line == "Game exited.")); | ||
| 189 | assert_eq!(h.screen.prompt, ""); | ||
| 190 | } | ||
| 191 | |||
| 192 | #[test] | ||
| 193 | fn a_server_error_replaces_the_status_line() { | ||
| 194 | let mut h = Harness::new(); | ||
| 195 | h.enter_slots("50"); | ||
| 196 | h.key(Key::Char(' ')); | ||
| 197 | |||
| 198 | h.reply("ERROR Insufficient balance"); | ||
| 199 | |||
| 200 | assert_eq!(h.board().len(), 7); | ||
| 201 | assert!(h.board()[4].contains("ERROR: Insufficient balance")); | ||
| 202 | assert!(h.board()[2].contains("[ ? ]")); | ||
| 203 | assert!(!h.board().iter().any(|line| line.contains("Spinning"))); | ||
| 204 | } | ||
| 205 | |||
| 206 | #[test] | ||
| 207 | fn an_empty_reply_is_an_error_not_a_silent_spin() { | ||
| 208 | let mut h = Harness::new(); | ||
| 209 | h.enter_slots("50"); | ||
| 210 | h.key(Key::Char(' ')); | ||
| 211 | |||
| 212 | h.reply(""); | ||
| 213 | |||
| 214 | assert_eq!(h.board().len(), 7); | ||
| 215 | assert!(h.board()[4].contains("No response from server")); | ||
| 216 | } | ||
