//! 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 { 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::::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, ) { 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]); } }