//! Tests for slash commands: `/context` and `/play` staying local to the //! screen instead of going to the worker, `/dm` wrapping its words into a //! turn, and Tab completing command names on the prompt. The harness //! lives in `screen_tests.rs`. use std::cell::RefCell; use super::tests::{ SLASH_CONTEXT, SLASH_SETTINGS, done, play_script, play_script_on, play_staged, press, typing, }; use super::*; /// A stage with a fixed roster and a fixed answer for who is on it, that /// records every slug `take_stage` is asked to put there. `fails`, when /// set, is the error every `take_stage` call returns instead of /// recording the slug and succeeding. struct FakeStage { characters: Vec, on_stage: Option, taken: RefCell>, fails: Option, } impl FakeStage { fn new(characters: &[&str], on_stage: Option<&str>) -> Self { Self { characters: characters.iter().map(|slug| slug.to_string()).collect(), on_stage: on_stage.map(str::to_string), taken: RefCell::new(Vec::new()), fails: None, } } fn failing(characters: &[&str], on_stage: Option<&str>, error: &str) -> Self { Self { fails: Some(error.to_string()), ..Self::new(characters, on_stage) } } } impl Stage for FakeStage { fn characters(&self) -> Vec { self.characters.clone() } fn on_stage(&self) -> Option { self.on_stage.clone() } fn take_stage(&self, slug: &str) -> Result<(), String> { if let Some(error) = &self.fails { return Err(error.clone()); } self.taken.borrow_mut().push(slug.to_string()); Ok(()) } } #[test] fn slash_context_puts_the_command_and_the_prompt_in_the_transcript() { let mut steps = typing("/context"); steps.push(press(Key::Enter)); let played = play_script(steps); assert!(played.transcript().contains("> /context")); assert!(played.transcript().contains(SLASH_CONTEXT)); } #[test] fn slash_context_sends_nothing_to_the_worker() { let mut steps = typing("/context"); steps.push(press(Key::Enter)); let played = play_script(steps); assert!(played.nothing_submitted()); } #[test] fn slash_context_clears_the_prompt_for_the_next_input() { let mut steps = typing("/context"); steps.push(press(Key::Enter)); let played = play_script(steps); assert_eq!(played.prompt(), ">"); } #[test] fn a_trailing_space_still_names_the_command() { let mut steps = typing("/context "); steps.push(press(Key::Enter)); let played = play_script(steps); assert!(played.transcript().contains(SLASH_CONTEXT)); assert!(played.nothing_submitted()); } #[test] fn slash_settings_puts_the_command_and_the_report_in_the_transcript() { let mut steps = typing("/settings"); steps.push(press(Key::Enter)); let played = play_script(steps); assert!(played.transcript().contains("> /settings")); assert!(played.transcript().contains(SLASH_SETTINGS)); } #[test] fn slash_settings_sends_nothing_to_the_worker() { let mut steps = typing("/settings"); steps.push(press(Key::Enter)); let played = play_script(steps); assert!(played.nothing_submitted()); } #[test] fn slash_settings_clears_the_prompt_for_the_next_input() { let mut steps = typing("/settings"); steps.push(press(Key::Enter)); let played = play_script(steps); assert_eq!(played.prompt(), ">"); } #[test] fn tab_completes_a_slash_command_prefix() { let mut steps = typing("/c"); steps.push(press(Key::Tab)); let played = play_script(steps); assert_eq!(played.prompt(), "> /context"); } #[test] fn tab_on_a_complete_command_cycles_around_to_it() { let mut steps = typing("/context"); steps.push(press(Key::Tab)); let played = play_script(steps); assert_eq!(played.prompt(), "> /context"); } #[test] fn tab_leaves_input_without_a_leading_slash_alone() { let mut steps = typing("hi"); steps.push(press(Key::Tab)); let played = play_script(steps); assert_eq!(played.prompt(), "> hi"); } #[test] fn tab_leaves_an_unknown_slash_prefix_alone() { let mut steps = typing("/x"); steps.push(press(Key::Tab)); let played = play_script(steps); assert_eq!(played.prompt(), "> /x"); } #[test] fn tab_leaves_an_input_of_several_rows_alone() { let steps = vec![ press(Key::Paste("/c\nthe rest of it".to_string())), press(Key::Tab), ]; let played = play_script_on(10, steps); assert_eq!(played.prompt(), " the rest of it"); } #[test] fn tab_completes_play() { let mut steps = typing("/pl"); steps.push(press(Key::Tab)); let played = play_script(steps); assert_eq!(played.prompt(), "> /play"); } #[test] fn tab_completes_settings() { let mut steps = typing("/se"); steps.push(press(Key::Tab)); let played = play_script(steps); assert_eq!(played.prompt(), "> /settings"); } #[test] fn slash_dm_echoes_the_typed_line() { let mut steps = typing("/dm keep it light"); steps.push(press(Key::Enter)); let played = play_script(steps); assert!(played.transcript().contains("> /dm keep it light")); } #[test] fn slash_dm_sends_the_wrapped_text_to_the_worker() { let mut steps = typing("/dm keep it light"); steps.push(press(Key::Enter)); let played = play_script(steps); assert_eq!( played.sent(), ( Speaker::Player, format!("{OUT_OF_CHARACTER_MARKER}keep it light") ) ); } #[test] fn slash_dm_recalls_the_typed_line_with_up() { let mut steps = typing("/dm keep it light"); steps.push(press(Key::Enter)); steps.push(done()); steps.push(press(Key::Up)); let played = play_script(steps); assert_eq!(played.prompt(), "> /dm keep it light"); } #[test] fn bare_slash_dm_starts_no_turn_and_shows_the_failure_aside() { let mut steps = typing("/dm"); steps.push(press(Key::Enter)); let played = play_script(steps); assert!(played.transcript().contains("> /dm")); assert!(played.transcript().contains(DM_NEEDS_WORDS)); assert!(played.nothing_submitted()); } #[test] fn tab_completes_dm() { let mut steps = typing("/d"); steps.push(press(Key::Tab)); let played = play_script(steps); assert_eq!(played.prompt(), "> /dm"); } #[test] fn slash_dmx_is_not_the_dm_command() { let mut steps = typing("/dmx hello"); steps.push(press(Key::Enter)); let played = play_script(steps); assert_eq!(played.sent(), (Speaker::Player, "/dmx hello".to_string())); } #[test] fn slash_play_lists_characters_and_marks_the_one_on_stage() { let stage = FakeStage::new(&["maren", "tomas"], Some("maren")); let mut steps = typing("/play"); steps.push(press(Key::Enter)); let played = play_staged(&stage, steps); assert!(played.transcript().contains("maren (on stage)")); assert!(played.transcript().contains("tomas")); assert!(!played.transcript().contains("tomas (on stage)")); } #[test] fn slash_play_with_no_characters_says_so() { let stage = FakeStage::new(&[], None); let mut steps = typing("/play"); steps.push(press(Key::Enter)); let played = play_staged(&stage, steps); assert!( played .transcript() .contains("the world has no characters yet") ); } #[test] fn slash_play_switches_and_the_stage_closure_records_the_call() { let stage = FakeStage::new(&["maren", "tomas"], Some("tomas")); let mut steps = typing("/play maren"); steps.push(press(Key::Enter)); let played = play_staged(&stage, steps); assert_eq!(*stage.taken.borrow(), vec!["maren".to_string()]); assert!(played.transcript().contains("maren takes the stage")); } #[test] fn slash_play_shows_the_error_when_taking_the_stage_fails() { let stage = FakeStage::failing( &["maren", "tomas"], Some("tomas"), "the campaign log could not be written", ); let mut steps = typing("/play maren"); steps.push(press(Key::Enter)); let played = play_staged(&stage, steps); assert!( played .transcript() .contains("the campaign log could not be written") ); } #[test] fn slash_play_with_an_unknown_slug_lists_what_exists() { let stage = FakeStage::new(&["maren"], None); let mut steps = typing("/play bob"); steps.push(press(Key::Enter)); let played = play_staged(&stage, steps); assert!(played.transcript().contains("no character named 'bob'")); assert!(played.transcript().contains("maren")); assert!(stage.taken.borrow().is_empty()); } #[test] fn slash_play_on_the_active_character_writes_no_event() { let stage = FakeStage::new(&["maren"], Some("maren")); let mut steps = typing("/play maren"); steps.push(press(Key::Enter)); let played = play_staged(&stage, steps); assert!(played.transcript().contains("maren is already on stage")); assert!(stage.taken.borrow().is_empty()); } #[test] fn the_unbound_notice_appears_in_the_transcript_when_composed() { let stage = FakeStage::new(&["maren", "tomas"], None); let played = play_staged(&stage, vec![]); assert!(played.transcript().contains("none is on stage yet")); } #[test] fn character_roster_marks_the_one_on_stage() { let characters = vec!["maren".to_string(), "tomas".to_string()]; let roster = character_roster(&characters, Some("maren")); assert_eq!(roster, "maren (on stage)\ntomas"); } #[test] fn character_roster_with_no_characters_says_so() { let roster = character_roster(&[], None); assert_eq!(roster, "the world has no characters yet"); } #[test] fn play_outcome_names_nobody() { let characters = vec!["maren".to_string()]; assert!(matches!( play_outcome(&characters, None, "bob"), PlayOutcome::Unknown )); } #[test] fn play_outcome_names_who_is_already_on_stage() { let characters = vec!["maren".to_string()]; assert!(matches!( play_outcome(&characters, Some("maren"), "maren"), PlayOutcome::AlreadyOnStage )); } #[test] fn play_outcome_names_a_switch() { let characters = vec!["maren".to_string(), "tomas".to_string()]; assert!(matches!( play_outcome(&characters, Some("tomas"), "maren"), PlayOutcome::Switch )); } #[test] fn unknown_character_lists_what_exists() { let characters = vec!["maren".to_string()]; let text = unknown_character(&characters, "bob"); assert_eq!(text, "no character named 'bob'; the world has: maren"); } #[test] fn unknown_character_with_no_characters_says_so() { let text = unknown_character(&[], "bob"); assert_eq!( text, "no character named 'bob'; the world has no characters yet" ); } #[test] fn unbound_notice_names_several_unbound_characters() { let characters = vec!["maren".to_string(), "tomas".to_string()]; assert!(unbound_notice(&characters, None).is_some()); } #[test] fn unbound_notice_says_nothing_with_one_character() { let characters = vec!["maren".to_string()]; assert_eq!(unbound_notice(&characters, None), None); } #[test] fn unbound_notice_says_nothing_with_no_characters() { assert_eq!(unbound_notice(&[], None), None); }