Files
ui-agregator/crates/evil-keys/tests/proptest_invariants.rs
T
Alexander bee1f82405 test(evil-keys): add integration and property-based tests
20 integration scenarios (5gg, SPC leader, escape progression, timeout,
mode switch, which-key, stress tests) and 8 proptest invariants (roundtrip,
never-panic, non-Press ignored, count >= 1, escape clears state).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-10 10:59:43 +02:00

145 lines
4.4 KiB
Rust

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<Value = String> {
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::<char>().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<usize> = 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<Key> = keys
.iter()
.filter_map(|k| parse_key(k).ok())
.collect();
let _ = trie.search(&parsed);
}
}