use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}; use evil_keys::key::{parse_key, parse_sequence}; use evil_keys::{DispatchResult, Dispatcher, Key, KeyTrie}; use proptest::prelude::*; fn valid_key_string() -> impl Strategy { prop_oneof![ prop::char::range('a', 'z').prop_map(|c| c.to_string()), prop::char::range('A', 'Z').prop_map(|c| c.to_string()), prop::char::range('0', '9').prop_map(|c| c.to_string()), Just("SPC".to_string()), Just("Esc".to_string()), Just("Tab".to_string()), Just("Enter".to_string()), Just("-".to_string()), Just("[".to_string()), Just("]".to_string()), Just("/".to_string()), Just("?".to_string()), (1u8..=12).prop_map(|n| format!("F{n}")), prop::char::range('a', 'z').prop_map(|c| format!("C-{c}")), ] } fn test_dispatcher() -> Dispatcher<&'static str> { let mut trie = KeyTrie::new("normal"); trie.bind("j", "down").unwrap(); trie.bind("k", "up").unwrap(); trie.bind("Esc", "escape").unwrap(); trie.group("g", "+goto", |g| { g.bind("g", "goto_first")?; g.bind("t", "next_tab")?; Ok(()) }) .unwrap(); let mut d = Dispatcher::new(); d.add_mode("normal", trie).unwrap(); d.set_active("normal").unwrap(); d } fn press(code: KeyCode) -> KeyEvent { KeyEvent { code, modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE, } } fn press_char(c: char) -> KeyEvent { press(KeyCode::Char(c)) } proptest! { #[test] fn key_parse_display_roundtrip(s in valid_key_string()) { let key = parse_key(&s).unwrap(); let displayed = key.to_string(); let reparsed = parse_key(&displayed).unwrap(); prop_assert_eq!(key, reparsed); } #[test] fn key_parse_never_panics(s in "\\PC{0,20}") { let _ = parse_key(&s); } #[test] fn key_display_never_panics(c in any::().prop_filter("ascii", |c| c.is_ascii() && !c.is_control())) { let key = Key { code: KeyCode::Char(c), modifiers: KeyModifiers::NONE }; let _ = key.to_string(); } #[test] fn non_press_always_ignored(code_char in prop::char::range('a', 'z')) { let mut d = test_dispatcher(); let release = KeyEvent { code: KeyCode::Char(code_char), modifiers: KeyModifiers::NONE, kind: KeyEventKind::Release, state: KeyEventState::NONE, }; prop_assert_eq!(d.dispatch(release), DispatchResult::Ignored); let repeat = KeyEvent { code: KeyCode::Char(code_char), modifiers: KeyModifiers::NONE, kind: KeyEventKind::Repeat, state: KeyEventState::NONE, }; prop_assert_eq!(d.dispatch(repeat), DispatchResult::Ignored); } #[test] fn count_take_always_at_least_1(digits in prop::collection::vec(prop::char::range('1', '9'), 0..10)) { let mut count = evil_keys::count::CountState::new(); for d in digits { count.push_digit(d); } prop_assert!(count.take() >= 1); } #[test] fn escape_always_clears_pending(keys_before in prop::collection::vec(prop::char::range('a', 'z'), 0..5)) { let mut d = test_dispatcher(); for c in keys_before { let _ = d.dispatch(press_char(c)); } let _ = d.dispatch(press(KeyCode::Esc)); prop_assert!(d.pending_keys().is_empty()); } #[test] fn trie_bound_keys_always_found(keys in prop::collection::vec(valid_key_string(), 1..10)) { let mut trie: KeyTrie = KeyTrie::new("test"); let mut successful = vec![]; for (i, key) in keys.iter().enumerate() { if trie.bind(key, i).is_ok() { successful.push(key.clone()); } } for key in &successful { let seq = parse_sequence(key).unwrap(); prop_assert!(matches!( trie.search(&seq), evil_keys::SearchResult::Found(_) )); } } #[test] fn search_empty_trie_never_panics(keys in prop::collection::vec(valid_key_string(), 0..5)) { let trie: KeyTrie<()> = KeyTrie::new("test"); let parsed: Vec = keys .iter() .filter_map(|k| parse_key(k).ok()) .collect(); let _ = trie.search(&parsed); } }