//! Integration coverage for the client mouse input contract using uinput. //! //! Scope: exercise `MouseAggregator` with synthetic relative and absolute input //! devices so event translation stays deterministic. //! Targets: `client/src/input/mouse.rs`. //! Why: mouse handling is event-rich and high-risk for regressions without //! end-to-end event-path tests. use evdev::uinput::VirtualDevice; use evdev::{ AbsInfo, AbsoluteAxisCode, AttributeSet, Device, EventType, InputEvent, KeyCode, RelativeAxisCode, UinputAbsSetup, }; use lesavka_client::input::mouse::MouseAggregator; use serial_test::serial; use std::path::PathBuf; use std::thread; use std::time::Duration; use tokio::sync::broadcast; fn open_virtual_node(vdev: &mut VirtualDevice) -> Option { let mut node = None; for _ in 0..40 { if let Ok(mut nodes) = vdev.enumerate_dev_nodes_blocking() { if let Some(Ok(path)) = nodes.next() { node = Some(path); break; } } thread::sleep(Duration::from_millis(10)); } node } fn open_virtual_device(vdev: &mut VirtualDevice) -> Option { let node = open_virtual_node(vdev)?; let dev = Device::open(node).ok()?; dev.set_nonblocking(true).ok()?; Some(dev) } fn build_relative_mouse(name: &str) -> Option<(VirtualDevice, Device)> { let mut keys = AttributeSet::::new(); keys.insert(KeyCode::BTN_LEFT); keys.insert(KeyCode::BTN_RIGHT); let mut rel = AttributeSet::::new(); rel.insert(RelativeAxisCode::REL_X); rel.insert(RelativeAxisCode::REL_Y); rel.insert(RelativeAxisCode::REL_WHEEL); let mut vdev = VirtualDevice::builder() .ok()? .name(name) .with_keys(&keys) .ok()? .with_relative_axes(&rel) .ok()? .build() .ok()?; let dev = open_virtual_device(&mut vdev)?; Some((vdev, dev)) } fn build_absolute_touch_mouse(name: &str) -> Option<(VirtualDevice, Device)> { let mut keys = AttributeSet::::new(); keys.insert(KeyCode::BTN_TOUCH); let abs = AbsInfo::new(0, 0, 1000, 0, 0, 0); let mut vdev = VirtualDevice::builder() .ok()? .name(name) .with_keys(&keys) .ok()? .with_absolute_axis(&UinputAbsSetup::new(AbsoluteAxisCode::ABS_X, abs)) .ok()? .with_absolute_axis(&UinputAbsSetup::new(AbsoluteAxisCode::ABS_Y, abs)) .ok()? .with_absolute_axis(&UinputAbsSetup::new( AbsoluteAxisCode::ABS_MT_TRACKING_ID, AbsInfo::new(0, -1, 10, 0, 0, 0), )) .ok()? .build() .ok()?; let dev = open_virtual_device(&mut vdev)?; Some((vdev, dev)) } #[test] #[serial] fn relative_mouse_events_emit_expected_packets() { let Some((mut vdev, dev)) = build_relative_mouse("lesavka-test-mouse-rel") else { return; }; let (tx, mut rx) = broadcast::channel(32); let mut agg = MouseAggregator::new(dev, true, tx); agg.set_grab(false); agg.set_send(true); vdev.emit(&[ InputEvent::new(EventType::KEY.0, KeyCode::BTN_LEFT.0, 1), InputEvent::new(EventType::RELATIVE.0, RelativeAxisCode::REL_X.0, 12), InputEvent::new(EventType::RELATIVE.0, RelativeAxisCode::REL_Y.0, -5), InputEvent::new(EventType::RELATIVE.0, RelativeAxisCode::REL_WHEEL.0, 1), ]) .expect("emit relative press/move events"); thread::sleep(Duration::from_millis(25)); agg.process_events(); let pkt = rx.try_recv().expect("mouse report"); assert_eq!(pkt.data[0], 1); assert_eq!(pkt.data[1], 12); assert_eq!(pkt.data[2], (-5_i8) as u8); assert_eq!(pkt.data[3], 1); vdev.emit(&[InputEvent::new(EventType::KEY.0, KeyCode::BTN_LEFT.0, 0)]) .expect("emit button release"); thread::sleep(Duration::from_millis(25)); agg.process_events(); let release_pkt = rx.try_recv().expect("release report"); assert_eq!(release_pkt.data[0], 0); } #[test] #[serial] fn absolute_touch_events_translate_into_motion_and_reset() { let Some((mut vdev, dev)) = build_absolute_touch_mouse("lesavka-test-mouse-abs") else { return; }; let (tx, mut rx) = broadcast::channel(32); let mut agg = MouseAggregator::new(dev, true, tx); vdev.emit(&[ InputEvent::new(EventType::KEY.0, KeyCode::BTN_TOUCH.0, 1), InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_X.0, 100), InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_Y.0, 100), ]) .expect("emit initial touch frame"); thread::sleep(Duration::from_millis(20)); agg.process_events(); vdev.emit(&[ InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_X.0, 300), InputEvent::new(EventType::ABSOLUTE.0, AbsoluteAxisCode::ABS_Y.0, 500), ]) .expect("emit movement frame"); thread::sleep(Duration::from_millis(20)); agg.process_events(); let pkt = rx.try_recv().expect("absolute movement report"); assert_eq!(pkt.data[0], 1); assert_ne!(pkt.data[1], 0); assert_ne!(pkt.data[2], 0); agg.set_send(false); agg.reset_state(); assert!( rx.try_recv().is_err(), "send-disabled reset should not emit" ); agg.set_send(true); agg.reset_state(); let reset_pkt = rx.try_recv().expect("reset report"); assert_eq!(reset_pkt.data, vec![0, 0, 0, 0]); }