The chooser's rules, through the state that acts on them.
3use std::collections::BTreeMap;
5use whiskers_core::wire::VoiceEntry; 6use whiskers_core::{Age, Audience, CharacterId, Child, ChildName, VoiceId, characters}; 7 8use super::*; 9 10fn who(id: &str) -> CharacterId { 11 CharacterId::new(id).unwrap() 12} 13 14fn voice(id: &str) -> VoiceId { 15 VoiceId::new(id).unwrap() 16} 17 18fn ada() -> Audience { 19 Audience::new(Some(&Child { name: ChildName::new("Ada").unwrap(), age: Age::new(6).unwrap() })) 20} 21 22fn listed() -> Vec<VoiceEntry> { 23 vec![ 24 VoiceEntry { id: voice("v-a"), name: "Alice".into() }, 25 VoiceEntry { id: voice("v-b"), name: "Bella".into() }, 26 VoiceEntry { id: voice("v-c"), name: "Cora".into() }, 27 ] 28}
The household has chosen the grey cat (no voice of its own) and given the panda Bella's voice; the base voice is Alice's.
37fn open() -> Chooser { 38 let mut c = chooser(); 39 c.input(Input::Open); 40 c 41} 42 43#[test] 44fn the_characters_wrap_around_both_ways() { 45 let all: Vec<CharacterId> = characters().collect(); 46 let mut c = open(); 47 assert_eq!(c.view().character, all[0]); 48 for expected in all.iter().skip(1).chain(std::iter::once(&all[0])) { 49 assert_eq!(c.input(Input::Right).view.character, *expected); 50 } 51 assert_eq!(c.input(Input::Left).view.character, *all.last().unwrap(), "left of the first is the last"); 52 for expected in all.iter().rev().skip(1) { 53 assert_eq!(c.input(Input::Left).view.character, *expected); 54 } 55} 56 57#[test] 58fn the_drawn_characters_come_first_in_order_and_the_bunny_is_one_of_them() { 59 let first: Vec<String> = characters().take(4).map(|c| c.to_string()).collect(); 60 assert_eq!(first, ["grey-cat", "ginger-cat", "bunny", "panda"]); 61 let mut c = open(); 62 c.input(Input::Right); 63 assert_eq!(c.input(Input::Right).view.character, who("bunny")); 64 assert_eq!(c.input(Input::Right).view.character, who("panda")); 65 assert_eq!(c.input(Input::Left).view.character, who("bunny")); 66 assert_eq!(c.input(Input::Left).view.character, who("ginger-cat")); 67} 68 69#[test] 70fn the_bunny_has_its_own_voice_slot_and_saves_it_like_any_character() { 71 let mut c = open(); 72 c.input(Input::Right); 73 c.input(Input::Right); 74 assert_eq!(c.view().character, who("bunny")); 75 assert_eq!(c.view().voice, None, "no voice of its own: the base voice shows"); 76 c.input(Input::VoiceRight); 77 let out = c.input(Input::Check); 78 assert_eq!(out.effects, [ShellEffect::Save { character: who("bunny"), voice: Some(voice("v-b")) }, ShellEffect::Leave]); 79 assert_eq!(c.saved().0, who("bunny")); 80 assert_eq!(c.saved().1.get(&who("bunny")), Some(&voice("v-b"))); 81 assert_eq!(c.saved().1.get(&who("panda")), Some(&voice("v-b")), "another character's voice is untouched"); 82} 83 84#[test] 85fn the_voices_wrap_around_both_ways() { 86 let mut c = open(); 87 assert_eq!(c.view().voice, None, "the grey cat has none of its own: the base voice shows, by name"); 88 assert_eq!(c.view().voice_name.as_deref(), Some("Alice")); 89 assert_eq!(c.input(Input::VoiceLeft).view.voice, Some(voice("v-c")), "left of the first is the last"); 90 assert_eq!(c.input(Input::VoiceRight).view.voice, Some(voice("v-a"))); 91 for expected in ["v-b", "v-c", "v-a"] { 92 assert_eq!(c.input(Input::VoiceRight).view.voice, Some(voice(expected))); 93 } 94} 95 96#[test] 97fn a_preview_does_not_save() { 98 let mut c = open(); 99 c.input(Input::Right); 100 let out = c.input(Input::VoiceRight); 101 assert!(out.view.unsaved); 102 assert!(out.effects.iter().all(|e| !matches!(e, ShellEffect::Save { .. })), "{:?}", out.effects); 103 assert_eq!(c.saved().0, who("grey-cat")); 104 assert_eq!(c.saved().1, &BTreeMap::from([(who("panda"), voice("v-b"))]), "what is saved is as it was"); 105} 106 107#[test] 108fn the_check_saves_exactly_the_shown_character_and_its_voice() { 109 let mut c = open(); 110 c.input(Input::Right); 111 c.input(Input::Right); 112 c.input(Input::Right); 113 assert_eq!(c.view().character, who("panda")); 114 assert_eq!(c.view().voice, Some(voice("v-b")), "its saved voice"); 115 c.input(Input::VoiceRight); 116 let out = c.input(Input::Check); 117 assert_eq!(out.effects, [ShellEffect::Save { character: who("panda"), voice: Some(voice("v-c")) }, ShellEffect::Leave]); 118 assert_eq!(c.saved().0, who("panda")); 119 assert_eq!(c.saved().1, &BTreeMap::from([(who("panda"), voice("v-c"))]), "its voice, and no other character's"); 120 assert!(!out.view.editing && !out.view.unsaved); 121} 122 123#[test] 124fn saving_a_character_with_the_base_voice_keeps_no_voice_for_it() { 125 let mut c = open(); 126 c.input(Input::Right); 127 assert_eq!(c.view().character, who("ginger-cat")); 128 let out = c.input(Input::Check); 129 assert_eq!(out.effects, [ShellEffect::Save { character: who("ginger-cat"), voice: None }, ShellEffect::Leave]); 130 assert_eq!(c.saved().1.get(&who("ginger-cat")), None); 131} 132 133#[test] 134fn the_cross_leaves_everything_as_it_was() { 135 let mut c = open(); 136 c.input(Input::Right); 137 c.input(Input::Right); 138 c.input(Input::VoiceRight); 139 let out = c.input(Input::Cancel); 140 assert_eq!(out.effects, [ShellEffect::Discard, ShellEffect::Leave]); 141 assert_eq!(c.saved().0, who("grey-cat")); 142 assert_eq!(c.saved().1, &BTreeMap::from([(who("panda"), voice("v-b"))])); 143 assert!(!out.view.editing && !out.view.unsaved); 144 assert_eq!((out.view.character, out.view.voice), (who("grey-cat"), None), "what was shown is what is saved again"); 145 assert_eq!(c.input(Input::Open).view.character, who("grey-cat"), "and opening again starts from it"); 146} 147 148#[test] 149fn the_check_with_nothing_changed_just_closes() { 150 let mut c = open(); 151 let out = c.input(Input::Check); 152 assert_eq!(out.effects, [ShellEffect::Leave], "nothing to save"); 153 let mut c = open(); 154 c.input(Input::Right); 155 c.input(Input::Left); 156 assert_eq!(c.input(Input::Check).effects, [ShellEffect::Leave], "back where it began is no change"); 157} 158 159#[test] 160fn changing_character_shows_that_characters_saved_voice_and_not_the_previewed_one() { 161 let mut c = open(); 162 assert_eq!(c.input(Input::VoiceRight).view.voice, Some(voice("v-b")), "previewing Bella on the grey cat"); 163 let out = c.input(Input::Right); 164 assert_eq!(out.view.character, who("ginger-cat")); 165 assert_eq!(out.view.voice, None, "the ginger cat has none saved: not the Bella that was previewed"); 166 let out = c.input(Input::Right); 167 assert_eq!((out.view.character, out.view.voice), (who("bunny"), None), "the bunny has none saved either"); 168 let out = c.input(Input::Right); 169 assert_eq!((out.view.character, out.view.voice), (who("panda"), Some(voice("v-b"))), "the panda shows its own saved voice"); 170 let out = c.input(Input::VoiceRight); 171 assert_eq!(out.view.voice, Some(voice("v-c"))); 172 assert_eq!(c.input(Input::Left).view.voice, None, "and stepping on leaves that preview behind"); 173} 174 175#[test] 176fn what_is_said_for_each_effect() { 177 let mut c = open(); 178 // A character changes: "Hi <name>", in that character's saved voice, which is the base voice for one with none. 179 let out = c.input(Input::Right); 180 assert_eq!(out.effects, [ShellEffect::PreviewCharacter(Utterance { text: "Hi Ada".into(), voice: None })]); 181 let out = c.input(Input::Right); 182 assert_eq!(out.effects, [ShellEffect::PreviewCharacter(Utterance { text: "Hi Ada".into(), voice: None })], "the bunny has none: the base voice"); 183 let out = c.input(Input::Right); 184 assert_eq!(out.effects, [ShellEffect::PreviewCharacter(Utterance { text: "Hi Ada".into(), voice: Some(voice("v-b")) })], "panda's own voice"); 185 // A voice is chosen: its name, in that voice. 186 let out = c.input(Input::VoiceRight); 187 assert_eq!(out.effects, [ShellEffect::PreviewVoice(Utterance { text: "Hi Ada, this voice is called Cora".into(), voice: Some(voice("v-c")) })]); 188 // Opening, saving and leaving say nothing. 189 let mut c = chooser(); 190 assert_eq!(c.input(Input::Open).effects, [ShellEffect::Enter]); 191} 192 193#[test] 194fn a_character_is_never_lent_another_characters_voice() { 195 let mut c = open(); 196 c.input(Input::Right); 197 c.input(Input::Right); 198 c.input(Input::Right); 199 let out = c.input(Input::Right); 200 assert_eq!(out.view.character, who("koala")); 201 assert_eq!(out.effects, [ShellEffect::PreviewCharacter(Utterance { text: "Hi Ada".into(), voice: None })], "the koala has none: the base voice, not the panda's"); 202} 203 204#[test] 205fn a_saved_voice_the_service_no_longer_lists_is_heard_in_the_base_voice() { 206 let saved = BTreeMap::from([(who("panda"), voice("gone"))]); 207 let mut c = Chooser::new(ada(), who("grey-cat"), saved, listed(), Some(voice("v-a"))); 208 c.input(Input::Open); 209 c.input(Input::Right); 210 let out = c.input(Input::Right); 211 assert_eq!(out.effects, [ShellEffect::PreviewCharacter(Utterance { text: "Hi Ada".into(), voice: None })]); 212} 213 214#[test] 215fn inputs_that_mean_nothing_say_so_and_change_nothing() { 216 let mut c = chooser(); 217 for input in [Input::Left, Input::Right, Input::VoiceLeft, Input::VoiceRight, Input::Check, Input::Cancel] { 218 let out = c.input(input); 219 assert!(out.effects.is_empty(), "{input:?} while closed"); 220 assert_eq!(out.notes, [Note::NotOpen], "{input:?}"); 221 assert!(!out.view.editing); 222 } 223 let mut c = open(); 224 c.input(Input::Right); 225 let out = c.input(Input::Open); 226 assert!(out.effects.is_empty()); 227 assert_eq!(out.notes, [Note::AlreadyOpen]); 228 assert_eq!(out.view.character, who("ginger-cat"), "opening again does not move her back"); 229} 230 231#[test] 232fn with_no_voices_listed_the_voice_arrows_keep_the_voice_she_has() { 233 let saved = BTreeMap::from([(who("panda"), voice("v-b"))]); 234 let mut c = Chooser::new(ada(), who("panda"), saved, vec![], Some(voice("v-a"))); 235 let out = c.input(Input::Open); 236 assert!(!out.view.voices_to_step); 237 for input in [Input::VoiceLeft, Input::VoiceRight] { 238 let out = c.input(input); 239 assert!(out.effects.is_empty()); 240 assert_eq!(out.notes, [Note::NoVoices]); 241 assert_eq!(out.view.voice, Some(voice("v-b"))); 242 } 243 let out = c.input(Input::Right); 244 assert_eq!(out.effects.len(), 1, "the characters still step"); 245} 246 247#[test] 248fn every_transition_leaves_a_trace_of_rule_names_and_counts_and_never_a_word_she_hears() { 249 let mut c = open(); 250 let out = c.input(Input::VoiceRight); 251 let joined = out.trace.join("\n"); 252 assert!(joined.contains(r#""name":"next voice""#), "{joined}"); 253 assert!(joined.contains(r#""name":"a voice says its name""#), "{joined}"); 254 for line in &out.trace { 255 let parsed: serde_json::Value = serde_json::from_str(line).unwrap_or_else(|e| panic!("{line}: {e}")); 256 assert!(parsed["event"].is_string()); 257 } 258 for secret in ["Ada", "Bella", "Cora", "Alice", "this voice is called"] { 259 assert!(!joined.contains(secret), "{secret} is in the trace"); 260 } 261 let kinds: Vec<String> = out.trace.iter().map(|l| serde_json::from_str::<serde_json::Value>(l).unwrap()["event"].as_str().unwrap().to_owned()).collect(); 262 assert_eq!(kinds.first().map(String::as_str), Some("asked")); 263 assert_eq!(kinds.last().map(String::as_str), Some("done")); 264 assert_eq!(kinds.iter().filter(|k| *k == "fired").count(), 2, "one rule moved the voice, the next said its name"); 265} 266 267#[test] 268fn an_utterance_prints_lengths_and_not_the_words() { 269 let u = Utterance { text: "Hi Ada".into(), voice: Some(voice("secret-voice-id")) }; 270 let shown = format!("{u:?}"); 271 assert!(!shown.contains("Ada") && !shown.contains("secret-voice-id"), "{shown}"); 272}