summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/client_core.rs174
-rw-r--r--tests/client_ui.rs131
2 files changed, 304 insertions, 1 deletions
diff --git a/tests/client_core.rs b/tests/client_core.rs
index 3dbbfe5..79b3dd8 100644
--- a/tests/client_core.rs
+++ b/tests/client_core.rs
@@ -5,6 +5,7 @@
5//! player would see, not about ANSI sequences. 5//! player would see, not about ANSI sequences.
6 6
7use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; 7use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key};
8use zagan_bet::client::ui::command_names;
8 9
9#[derive(Default)] 10#[derive(Default)]
10struct Screen { 11struct Screen {
@@ -12,6 +13,7 @@ struct Screen {
12 region_start: Option<usize>, 13 region_start: Option<usize>,
13 region_len: usize, 14 region_len: usize,
14 prompt: String, 15 prompt: String,
16 prompt_cursor: u16,
15} 17}
16 18
17impl Screen { 19impl Screen {
@@ -40,7 +42,10 @@ impl Screen {
40 self.region_start = None; 42 self.region_start = None;
41 self.region_len = 0; 43 self.region_len = 0;
42 } 44 }
43 Effect::SetPrompt { text, .. } => self.prompt = text.clone(), 45 Effect::SetPrompt { text, cursor_col } => {
46 self.prompt = text.clone();
47 self.prompt_cursor = *cursor_col;
48 }
44 Effect::SendLine(line) => sent.push(line.clone()), 49 Effect::SendLine(line) => sent.push(line.clone()),
45 Effect::Interactive(command) => interactive.push(command.clone()), 50 Effect::Interactive(command) => interactive.push(command.clone()),
46 Effect::Quit 51 Effect::Quit
@@ -214,3 +219,170 @@ fn an_empty_reply_is_an_error_not_a_silent_spin() {
214 assert_eq!(h.board().len(), 7); 219 assert_eq!(h.board().len(), 7);
215 assert!(h.board()[4].contains("No response from server")); 220 assert!(h.board()[4].contains("No response from server"));
216} 221}
222
223#[test]
224fn empty_enter_does_not_print_help() {
225 let mut h = Harness::new();
226 h.line("games");
227 let lines_before = h.screen.lines.clone();
228
229 h.key(Key::Enter);
230
231 assert_eq!(h.screen.lines, lines_before);
232 assert_eq!(h.screen.prompt, "");
233}
234
235#[test]
236fn history_does_not_duplicate_consecutive_commands() {
237 let mut h = Harness::new();
238 h.line("games");
239 h.line("games");
240
241 h.key(Key::Up);
242 assert_eq!(h.screen.prompt, "games");
243 h.key(Key::Up);
244 assert_eq!(h.screen.prompt, "games");
245}
246
247#[test]
248fn history_recalls_previous_commands() {
249 let mut h = Harness::new();
250 h.line("games");
251 h.line("help");
252
253 h.key(Key::Up);
254 assert_eq!(h.screen.prompt, "help");
255 h.key(Key::Up);
256 assert_eq!(h.screen.prompt, "games");
257}
258
259#[test]
260fn tab_completes_a_unique_command() {
261 let mut h = Harness::new();
262 for c in "con".chars() {
263 h.key(Key::Char(c));
264 }
265
266 h.key(Key::Tab);
267
268 assert_eq!(h.screen.prompt, "connect");
269 assert_eq!(h.screen.prompt_cursor, 9);
270}
271
272#[test]
273fn tab_completion_list_wraps() {
274 let mut h = Harness::new();
275
276 h.key(Key::Tab);
277
278 assert!(h
279 .screen
280 .lines
281 .iter()
282 .any(|line| line == "Possible completions:"));
283 for line in h
284 .screen
285 .lines
286 .iter()
287 .skip_while(|line| line.as_str() != "Possible completions:")
288 .skip(1)
289 {
290 assert!(
291 line.len() <= 76,
292 "completion line is too wide ({}): {line:?}",
293 line.len()
294 );
295 }
296 assert_eq!(h.screen.prompt, "");
297}
298
299#[test]
300fn play_without_a_bet_uses_the_default() {
301 let mut h = Harness::new();
302 h.line("play slots");
303
304 assert_eq!(h.board().len(), 7);
305 assert!(h.board()[1].contains("Bet: 10 PLN"));
306}
307
308#[test]
309fn an_invalid_bet_does_not_start_a_game() {
310 let mut h = Harness::new();
311 h.line("play slots 5");
312
313 assert!(h.board().is_empty());
314 assert!(h
315 .screen
316 .lines
317 .iter()
318 .any(|line| line.contains("Bet must be at least 10 PLN")));
319}
320
321#[test]
322fn help_lists_every_command() {
323 let mut h = Harness::new();
324 h.line("help");
325
326 let transcript = h.screen.lines.join("\n");
327 for command in command_names() {
328 assert!(
329 transcript.contains(command),
330 "help is missing '{command}'"
331 );
332 }
333}
334
335#[test]
336fn send_keeps_the_whole_message() {
337 let mut h = Harness::new();
338 h.line("send hello world");
339
340 assert_eq!(h.sent.last().map(String::as_str), Some("hello world"));
341}
342
343#[test]
344fn play_requires_a_connection_and_a_login() {
345 let mut h = Harness::new();
346 h.core.set_connected(false);
347 h.line("play slots");
348 assert!(h
349 .screen
350 .lines
351 .iter()
352 .any(|line| line.contains("Not connected")));
353
354 let mut h = Harness::new();
355 h.core.set_logged_in(None);
356 h.line("play slots");
357 assert!(h
358 .screen
359 .lines
360 .iter()
361 .any(|line| line.contains("Not logged in")));
362}
363
364#[test]
365fn ctrl_c_quits_from_the_prompt() {
366 let mut core = ClientCore::new();
367 assert_eq!(core.handle_key(Key::CtrlC), vec![Effect::Quit]);
368}
369
370#[test]
371fn ctrl_c_quits_from_a_game() {
372 let mut h = Harness::new();
373 h.enter_slots("50");
374
375 assert_eq!(h.core.handle_key(Key::CtrlC), vec![Effect::Quit]);
376}
377
378#[test]
379fn prompt_cursor_tracks_the_input() {
380 let mut h = Harness::new();
381 h.key(Key::Char('a'));
382 assert_eq!(h.screen.prompt, "a");
383 assert_eq!(h.screen.prompt_cursor, 3);
384
385 h.key(Key::Backspace);
386 assert_eq!(h.screen.prompt, "");
387 assert_eq!(h.screen.prompt_cursor, 2);
388}
diff --git a/tests/client_ui.rs b/tests/client_ui.rs
new file mode 100644
index 0000000..78faed0
--- /dev/null
+++ b/tests/client_ui.rs
@@ -0,0 +1,131 @@
1use zagan_bet::client::ui::{
2 command_names, completion_lines, help_lines, input_cursor_column, lines_to_move_up,
3 parse_bet_arg, record_history,
4};
5
6#[test]
7fn help_has_no_source_indentation() {
8 for line in help_lines() {
9 assert!(
10 !line.starts_with(" "),
11 "help line kept its source indentation: {line:?}"
12 );
13 }
14}
15
16#[test]
17fn help_documents_every_command() {
18 let help = help_lines().join("\n");
19 for command in command_names() {
20 assert!(help.contains(command), "help text is missing '{command}'");
21 }
22}
23
24#[test]
25fn completion_knows_every_command() {
26 let commands = command_names();
27 for expected in [
28 "connect",
29 "games",
30 "play",
31 "hosts",
32 "send",
33 "logout",
34 "quit",
35 ] {
36 assert!(
37 commands.contains(&expected),
38 "tab completion is missing '{expected}'"
39 );
40 }
41}
42
43#[test]
44fn history_records_a_command_once() {
45 let mut history = Vec::new();
46 record_history(&mut history, "info");
47 record_history(&mut history, "info");
48
49 assert_eq!(history, vec!["info"]);
50}
51
52#[test]
53fn history_keeps_distinct_commands_in_order() {
54 let mut history = Vec::new();
55 record_history(&mut history, "info");
56 record_history(&mut history, "ping");
57 record_history(&mut history, "info");
58
59 assert_eq!(history, vec!["info", "ping", "info"]);
60}
61
62#[test]
63fn blank_commands_are_not_recorded() {
64 let mut history = Vec::new();
65 record_history(&mut history, "");
66 record_history(&mut history, " ");
67
68 assert!(history.is_empty());
69}
70
71#[test]
72fn bet_accepts_ten_or_more() {
73 assert_eq!(parse_bet_arg("10"), Ok(10));
74 assert_eq!(parse_bet_arg("50"), Ok(50));
75 assert_eq!(parse_bet_arg(" 250 "), Ok(250));
76}
77
78#[test]
79fn bet_rejects_less_than_ten() {
80 assert!(parse_bet_arg("9").is_err());
81 assert!(parse_bet_arg("0").is_err());
82}
83
84#[test]
85fn bet_rejects_non_numbers() {
86 assert!(parse_bet_arg("abc").is_err());
87 assert!(parse_bet_arg("-50").is_err());
88 assert!(parse_bet_arg("").is_err());
89}
90
91#[test]
92fn redraw_moves_up_by_line_count_minus_one() {
93 assert_eq!(lines_to_move_up(0), 0);
94 assert_eq!(lines_to_move_up(1), 0);
95 assert_eq!(lines_to_move_up(7), 6);
96 assert_eq!(lines_to_move_up(70_000), u16::MAX);
97}
98
99#[test]
100fn input_cursor_is_after_the_removed_character() {
101 assert_eq!(input_cursor_column(10, 8), 18);
102 assert_eq!(input_cursor_column(10, 0), 10);
103 assert_eq!(input_cursor_column(2, 5), 7);
104 assert_eq!(input_cursor_column(70_000, 70_000), u16::MAX);
105}
106
107#[test]
108fn completions_wrap_within_the_requested_width() {
109 let commands = command_names();
110 let lines = completion_lines(&commands, 40);
111
112 assert_eq!(lines.first().map(String::as_str), Some("Possible completions:"));
113 for line in &lines[1..] {
114 assert!(
115 line.len() <= 40,
116 "completion line is too wide ({} > 40): {line:?}",
117 line.len()
118 );
119 }
120
121 let listed: Vec<&str> = lines[1..]
122 .iter()
123 .flat_map(|line| line.split_whitespace())
124 .collect();
125 for command in commands {
126 assert!(
127 listed.contains(&command),
128 "completion list is missing '{command}'"
129 );
130 }
131}