diff --git a/crates/core/src/actionability/mod.rs b/crates/core/src/actionability/mod.rs index 45c04b2..ba4c338 100644 --- a/crates/core/src/actionability/mod.rs +++ b/crates/core/src/actionability/mod.rs @@ -117,15 +117,15 @@ fn check_with_stability( } fn visibility_check(entry: &RefEntry) -> ActionabilityCheck { - let Some(bounds) = entry.bounds else { - return unknown("visible", "bounds unavailable"); - }; if state::has_state(&entry.states, state::HIDDEN) { return fail("visible", "entry state contains hidden"); } if state::has_state(&entry.states, state::OFFSCREEN) { return fail("visible", "entry state contains offscreen"); } + let Some(bounds) = entry.bounds else { + return unknown("visible", "bounds unavailable"); + }; if !bounds_are_visible(Some(bounds)) { return fail("visible", "bounds are zero-sized"); } diff --git a/crates/core/src/actionability_tests.rs b/crates/core/src/actionability_tests.rs index 03788c4..40e448d 100644 --- a/crates/core/src/actionability_tests.rs +++ b/crates/core/src/actionability_tests.rs @@ -99,6 +99,34 @@ fn offscreen_state_fails_visibility_before_action_dispatch() { assert!(err.message.contains("visible")); } +#[test] +fn hidden_entry_fails_visibility_even_when_bounds_are_none() { + let mut entry = entry(); + entry.states.push(crate::state::HIDDEN.into()); + entry.bounds = None; + entry.bounds_hash = None; + + let err = check(&entry, &ActionRequest::headless(Action::Click)).unwrap_err(); + + assert_eq!(err.code, ErrorCode::ActionFailed); + assert!(err.message.contains("visible")); + assert!(err.message.contains("hidden")); +} + +#[test] +fn offscreen_entry_fails_visibility_even_when_bounds_are_none() { + let mut entry = entry(); + entry.states.push(crate::state::OFFSCREEN.into()); + entry.bounds = None; + entry.bounds_hash = None; + + let err = check(&entry, &ActionRequest::headless(Action::Click)).unwrap_err(); + + assert_eq!(err.code, ErrorCode::ActionFailed); + assert!(err.message.contains("visible")); + assert!(err.message.contains("offscreen")); +} + #[test] fn text_input_requires_editable_target() { let err = check( diff --git a/crates/core/src/commands/mouse_wheel.rs b/crates/core/src/commands/mouse_wheel.rs index 3235c86..2b5979b 100644 --- a/crates/core/src/commands/mouse_wheel.rs +++ b/crates/core/src/commands/mouse_wheel.rs @@ -13,3 +13,7 @@ pub fn execute(args: MouseWheelArgs, adapter: &dyn PlatformAdapter) -> Result, +} + +struct WheelCaptureAdapter { + captured: Mutex>, + fail: bool, +} + +impl WheelCaptureAdapter { + fn recording() -> Self { + Self { + captured: Mutex::new(None), + fail: false, + } + } + + fn failing() -> Self { + Self { + captured: Mutex::new(None), + fail: true, + } + } +} + +impl ObservationOps for WheelCaptureAdapter {} +impl ActionOps for WheelCaptureAdapter {} +impl SystemOps for WheelCaptureAdapter {} + +impl InputOps for WheelCaptureAdapter { + fn mouse_wheel( + &self, + x: f64, + y: f64, + dy: i32, + dx: i32, + modifiers: &[Modifier], + ) -> Result<(), AdapterError> { + *self.captured.lock().unwrap() = Some(WheelCall { + x, + y, + dy, + dx, + modifiers: modifiers.to_vec(), + }); + if self.fail { + return Err(AdapterError::not_supported("mouse_wheel")); + } + Ok(()) + } +} + +#[test] +fn requested_wheel_args_reach_the_adapter_unchanged() { + let adapter = WheelCaptureAdapter::recording(); + + execute( + MouseWheelArgs { + x: 10.0, + y: 20.0, + dy: -3, + dx: 5, + modifiers: vec![Modifier::Shift, Modifier::Alt], + }, + &adapter, + ) + .unwrap(); + + let captured = adapter.captured.lock().unwrap(); + let call = captured + .as_ref() + .expect("mouse_wheel must have been called"); + assert_eq!( + *call, + WheelCall { + x: 10.0, + y: 20.0, + dy: -3, + dx: 5, + modifiers: vec![Modifier::Shift, Modifier::Alt], + } + ); +} + +#[test] +fn returns_scrolled_envelope_with_requested_deltas() { + let adapter = WheelCaptureAdapter::recording(); + + let value = execute( + MouseWheelArgs { + x: 0.0, + y: 0.0, + dy: 7, + dx: -2, + modifiers: Vec::new(), + }, + &adapter, + ) + .unwrap(); + + assert_eq!(value, json!({ "scrolled": true, "dy": 7, "dx": -2 })); +} + +#[test] +fn adapter_error_propagates_as_err() { + let adapter = WheelCaptureAdapter::failing(); + + let result = execute( + MouseWheelArgs { + x: 1.0, + y: 2.0, + dy: 1, + dx: 0, + modifiers: Vec::new(), + }, + &adapter, + ); + + assert!(result.is_err()); +} diff --git a/crates/core/src/ref_action_wait.rs b/crates/core/src/ref_action_wait.rs index 8c12563..bf2ba2d 100644 --- a/crates/core/src/ref_action_wait.rs +++ b/crates/core/src/ref_action_wait.rs @@ -226,6 +226,13 @@ fn execute_poll_loop( trace_resolve_error(ctx.context, ctx.ref_id, &err); return Err(err); } + last_report = Some(json!({ + "resolve_error": { + "code": code.as_str(), + "message": err.message.clone(), + "details": err.details.clone(), + } + })); sleep_poll_interval(deadline); } }