113 lines
3.6 KiB
Rust
113 lines
3.6 KiB
Rust
|
|
//! Focused coverage for keyboard quick-toggle activation edges.
|
||
|
|
//!
|
||
|
|
//! Scope: exercise the keyboard aggregator's recent-press tracking directly.
|
||
|
|
//! Targets: `client/src/input/keyboard.rs`.
|
||
|
|
//! Why: the swap key needs to stay reliable even when a tap begins and ends
|
||
|
|
//! before the next launcher/input poll cycle observes the key state.
|
||
|
|
|
||
|
|
mod keymap {
|
||
|
|
pub use lesavka_client::input::keymap::*;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[allow(warnings)]
|
||
|
|
mod keyboard_activation_contract {
|
||
|
|
include!(env!("LESAVKA_CLIENT_KEYBOARD_SRC"));
|
||
|
|
|
||
|
|
use evdev::AttributeSet;
|
||
|
|
use evdev::uinput::VirtualDevice;
|
||
|
|
use serial_test::serial;
|
||
|
|
use std::thread;
|
||
|
|
|
||
|
|
fn open_virtual_device(vdev: &mut VirtualDevice) -> Option<evdev::Device> {
|
||
|
|
for _ in 0..40 {
|
||
|
|
if let Ok(mut nodes) = vdev.enumerate_dev_nodes_blocking() {
|
||
|
|
if let Some(Ok(path)) = nodes.next() {
|
||
|
|
if let Ok(dev) = evdev::Device::open(path) {
|
||
|
|
let _ = dev.set_nonblocking(true);
|
||
|
|
return Some(dev);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
thread::sleep(std::time::Duration::from_millis(10));
|
||
|
|
}
|
||
|
|
None
|
||
|
|
}
|
||
|
|
|
||
|
|
fn build_keyboard(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
|
||
|
|
let mut keys = AttributeSet::<evdev::KeyCode>::new();
|
||
|
|
keys.insert(evdev::KeyCode::KEY_A);
|
||
|
|
keys.insert(evdev::KeyCode::KEY_ENTER);
|
||
|
|
|
||
|
|
let mut vdev = VirtualDevice::builder()
|
||
|
|
.ok()?
|
||
|
|
.name(name)
|
||
|
|
.with_keys(&keys)
|
||
|
|
.ok()?
|
||
|
|
.build()
|
||
|
|
.ok()?;
|
||
|
|
|
||
|
|
let dev = open_virtual_device(&mut vdev)?;
|
||
|
|
Some((vdev, dev))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn new_aggregator(
|
||
|
|
dev: evdev::Device,
|
||
|
|
) -> (
|
||
|
|
KeyboardAggregator,
|
||
|
|
tokio::sync::broadcast::Receiver<KeyboardReport>,
|
||
|
|
) {
|
||
|
|
let (tx, rx) = tokio::sync::broadcast::channel(16);
|
||
|
|
(KeyboardAggregator::new(dev, false, tx, None), rx)
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[serial]
|
||
|
|
fn take_key_activation_consumes_a_fast_tap_once() {
|
||
|
|
let Some((mut vdev, dev)) = build_keyboard("lesavka-kbd-activation-tap") else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
let (mut agg, _rx) = new_aggregator(dev);
|
||
|
|
|
||
|
|
vdev.emit(&[
|
||
|
|
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_A.0, 1),
|
||
|
|
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_A.0, 0),
|
||
|
|
])
|
||
|
|
.expect("emit key tap");
|
||
|
|
thread::sleep(std::time::Duration::from_millis(20));
|
||
|
|
agg.process_events();
|
||
|
|
|
||
|
|
assert!(agg.take_key_activation(evdev::KeyCode::KEY_A));
|
||
|
|
assert!(!agg.take_key_activation(evdev::KeyCode::KEY_A));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[serial]
|
||
|
|
fn process_events_clears_stale_recent_key_presses_before_polling() {
|
||
|
|
let Some((_vdev, dev)) = build_keyboard("lesavka-kbd-activation-clear") else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
let (mut agg, _rx) = new_aggregator(dev);
|
||
|
|
agg.recent_key_presses.insert(evdev::KeyCode::KEY_A);
|
||
|
|
|
||
|
|
agg.process_events();
|
||
|
|
|
||
|
|
assert!(!agg.take_key_activation(evdev::KeyCode::KEY_A));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[serial]
|
||
|
|
fn reset_state_clears_recent_key_presses_even_when_idle() {
|
||
|
|
let Some((_vdev, dev)) = build_keyboard("lesavka-kbd-activation-reset") else {
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
let (mut agg, mut rx) = new_aggregator(dev);
|
||
|
|
agg.recent_key_presses.insert(evdev::KeyCode::KEY_A);
|
||
|
|
|
||
|
|
agg.reset_state();
|
||
|
|
|
||
|
|
assert!(agg.recent_key_presses.is_empty());
|
||
|
|
let pkt = rx.try_recv().expect("empty report after idle reset");
|
||
|
|
assert_eq!(pkt.data, vec![0; 8]);
|
||
|
|
}
|
||
|
|
}
|