Fix raw native mouse drag state across down/move/up (#872)
* Fix native mouse down/move/up state * Allow clippy too_many_arguments for mouse helper --------- Co-authored-by: Daniel Vershinin <d@sgml.me>
This commit is contained in:
co-authored by
Daniel Vershinin
parent
4be4605543
commit
5b5dffe0a2
+256
-33
@@ -13,7 +13,8 @@ use super::cdp::chrome::LaunchOptions;
|
||||
use super::cdp::client::CdpClient;
|
||||
use super::cdp::types::{
|
||||
AttachToTargetParams, AttachToTargetResult, CdpEvent, ConsoleApiCalledEvent,
|
||||
CreateTargetResult, ExceptionThrownEvent, TargetCreatedEvent, TargetDestroyedEvent,
|
||||
CreateTargetResult, DispatchMouseEventParams, ExceptionThrownEvent, TargetCreatedEvent,
|
||||
TargetDestroyedEvent,
|
||||
};
|
||||
use super::cookies;
|
||||
use super::diff;
|
||||
@@ -104,6 +105,13 @@ pub enum BackendType {
|
||||
WebDriver,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct MouseState {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub buttons: i32,
|
||||
}
|
||||
|
||||
pub struct DaemonState {
|
||||
pub browser: Option<BrowserManager>,
|
||||
pub appium: Option<AppiumManager>,
|
||||
@@ -129,6 +137,7 @@ pub struct DaemonState {
|
||||
pub tracked_requests: Vec<TrackedRequest>,
|
||||
pub request_tracking: bool,
|
||||
pub active_frame_id: Option<String>,
|
||||
pub mouse_state: MouseState,
|
||||
/// Shared slot for stream server to receive CDP client when browser launches.
|
||||
pub stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>>,
|
||||
/// Stream server instance kept alive so the broadcast channel remains open.
|
||||
@@ -165,11 +174,16 @@ impl DaemonState {
|
||||
tracked_requests: Vec::new(),
|
||||
request_tracking: false,
|
||||
active_frame_id: None,
|
||||
mouse_state: MouseState::default(),
|
||||
stream_client: None,
|
||||
stream_server: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_input_state(&mut self) {
|
||||
self.mouse_state = MouseState::default();
|
||||
}
|
||||
|
||||
/// Create state with an optional stream client slot and server instance
|
||||
/// (for daemon startup with stream server).
|
||||
pub fn new_with_stream(
|
||||
@@ -708,6 +722,7 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
|
||||
let _ = mgr.close().await;
|
||||
}
|
||||
state.browser = None;
|
||||
state.reset_input_state();
|
||||
state.update_stream_client().await;
|
||||
}
|
||||
if let Err(e) = auto_launch(state).await {
|
||||
@@ -919,6 +934,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
|
||||
|
||||
if let Ok(cdp) = env::var("AGENT_BROWSER_CDP") {
|
||||
let mgr = BrowserManager::connect_cdp(&cdp).await?;
|
||||
state.reset_input_state();
|
||||
state.browser = Some(mgr);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
@@ -927,6 +943,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
|
||||
}
|
||||
|
||||
if env::var("AGENT_BROWSER_AUTO_CONNECT").is_ok() {
|
||||
state.reset_input_state();
|
||||
state.browser = Some(connect_auto_with_fresh_tab().await?);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
@@ -935,6 +952,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
|
||||
}
|
||||
|
||||
let mgr = BrowserManager::launch(options, engine.as_deref()).await?;
|
||||
state.reset_input_state();
|
||||
state.browser = Some(mgr);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
@@ -1041,6 +1059,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
if let Some(ref mut b) = state.browser {
|
||||
b.close().await?;
|
||||
state.browser = None;
|
||||
state.reset_input_state();
|
||||
state.update_stream_client().await;
|
||||
}
|
||||
} else {
|
||||
@@ -1077,6 +1096,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
)?;
|
||||
|
||||
if let Some(url) = cdp_url {
|
||||
state.reset_input_state();
|
||||
state.browser = Some(BrowserManager::connect_cdp(url).await?);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
@@ -1084,6 +1104,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
}
|
||||
|
||||
if let Some(port) = cdp_port {
|
||||
state.reset_input_state();
|
||||
state.browser = Some(BrowserManager::connect_cdp(&port.to_string()).await?);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
@@ -1091,6 +1112,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
}
|
||||
|
||||
if auto_connect {
|
||||
state.reset_input_state();
|
||||
state.browser = Some(connect_auto_with_fresh_tab().await?);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
@@ -1109,6 +1131,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
let (ws_url, provider_session) = providers::connect_provider(provider).await?;
|
||||
match BrowserManager::connect_cdp(&ws_url).await {
|
||||
Ok(mgr) => {
|
||||
state.reset_input_state();
|
||||
state.browser = Some(mgr);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
@@ -1195,6 +1218,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
state.domain_filter = Some(DomainFilter::new(domains));
|
||||
}
|
||||
|
||||
state.reset_input_state();
|
||||
state.browser = Some(BrowserManager::launch(options, engine.as_deref()).await?);
|
||||
state.subscribe_to_browser_events();
|
||||
state.update_stream_client().await;
|
||||
@@ -1245,6 +1269,7 @@ async fn launch_ios(cmd: &Value, state: &mut DaemonState) -> Result<Value, Strin
|
||||
|
||||
state.appium = Some(appium);
|
||||
state.backend_type = BackendType::WebDriver;
|
||||
state.reset_input_state();
|
||||
|
||||
Ok(json!({
|
||||
"launched": true,
|
||||
@@ -1288,6 +1313,7 @@ async fn launch_safari(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
state.safari_driver = Some(driver);
|
||||
state.webdriver_backend = Some(WebDriverBackend::new(client));
|
||||
state.backend_type = BackendType::WebDriver;
|
||||
state.reset_input_state();
|
||||
|
||||
Ok(json!({
|
||||
"launched": true,
|
||||
@@ -1477,6 +1503,7 @@ async fn handle_close(state: &mut DaemonState) -> Result<Value, String> {
|
||||
mgr.close().await?;
|
||||
}
|
||||
state.browser = None;
|
||||
state.reset_input_state();
|
||||
state.update_stream_client().await;
|
||||
|
||||
// Close WebDriver sessions
|
||||
@@ -5643,28 +5670,110 @@ async fn handle_device_list() -> Result<Value, String> {
|
||||
// Input event handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn handle_input_mouse(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
||||
fn mouse_button_mask(button: &str) -> i32 {
|
||||
match button {
|
||||
"left" => 1,
|
||||
"right" => 2,
|
||||
"middle" => 4,
|
||||
"back" => 8,
|
||||
"forward" => 16,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn primary_button_from_mask(buttons: i32) -> &'static str {
|
||||
if buttons & 1 != 0 {
|
||||
"left"
|
||||
} else if buttons & 2 != 0 {
|
||||
"right"
|
||||
} else if buttons & 4 != 0 {
|
||||
"middle"
|
||||
} else if buttons & 8 != 0 {
|
||||
"back"
|
||||
} else if buttons & 16 != 0 {
|
||||
"forward"
|
||||
} else {
|
||||
"none"
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_mouse_event_params(
|
||||
mouse_state: &mut MouseState,
|
||||
event_type: &str,
|
||||
x: Option<f64>,
|
||||
y: Option<f64>,
|
||||
button: Option<&str>,
|
||||
buttons: Option<i32>,
|
||||
click_count: Option<i32>,
|
||||
delta_x: Option<f64>,
|
||||
delta_y: Option<f64>,
|
||||
modifiers: Option<i32>,
|
||||
) -> DispatchMouseEventParams {
|
||||
let x = x.unwrap_or(mouse_state.x);
|
||||
let y = y.unwrap_or(mouse_state.y);
|
||||
mouse_state.x = x;
|
||||
mouse_state.y = y;
|
||||
|
||||
let mut next_buttons = buttons.unwrap_or(mouse_state.buttons);
|
||||
if buttons.is_none() {
|
||||
match event_type {
|
||||
"mousePressed" => {
|
||||
next_buttons |= mouse_button_mask(button.unwrap_or("left"));
|
||||
}
|
||||
"mouseReleased" => {
|
||||
next_buttons &= !mouse_button_mask(button.unwrap_or("left"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
mouse_state.buttons = next_buttons;
|
||||
|
||||
DispatchMouseEventParams {
|
||||
event_type: event_type.to_string(),
|
||||
x,
|
||||
y,
|
||||
button: Some(
|
||||
button
|
||||
.unwrap_or(primary_button_from_mask(next_buttons))
|
||||
.to_string(),
|
||||
),
|
||||
buttons: Some(next_buttons),
|
||||
click_count,
|
||||
delta_x,
|
||||
delta_y,
|
||||
modifiers,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_input_mouse(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||
let session_id = mgr.active_session_id()?.to_string();
|
||||
let event_type = cmd
|
||||
.get("type")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("mouseMoved");
|
||||
let x = cmd.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let y = cmd.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let params = build_mouse_event_params(
|
||||
&mut state.mouse_state,
|
||||
event_type,
|
||||
cmd.get("x").and_then(|v| v.as_f64()),
|
||||
cmd.get("y").and_then(|v| v.as_f64()),
|
||||
cmd.get("button").and_then(|v| v.as_str()),
|
||||
cmd.get("buttons")
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|v| v as i32),
|
||||
cmd.get("clickCount")
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|v| v as i32),
|
||||
cmd.get("deltaX").and_then(|v| v.as_f64()),
|
||||
cmd.get("deltaY").and_then(|v| v.as_f64()),
|
||||
cmd.get("modifiers")
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|v| v as i32),
|
||||
);
|
||||
|
||||
mgr.client
|
||||
.send_command(
|
||||
"Input.dispatchMouseEvent",
|
||||
Some(json!({
|
||||
"type": event_type, "x": x, "y": y,
|
||||
"button": cmd.get("button").and_then(|v| v.as_str()).unwrap_or("none"),
|
||||
"clickCount": cmd.get("clickCount").and_then(|v| v.as_i64()).unwrap_or(0),
|
||||
"deltaX": cmd.get("deltaX").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
"deltaY": cmd.get("deltaY").and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||
})),
|
||||
Some(&session_id),
|
||||
)
|
||||
.send_command_typed::<_, Value>("Input.dispatchMouseEvent", ¶ms, Some(&session_id))
|
||||
.await?;
|
||||
Ok(json!({ "dispatched": event_type }))
|
||||
}
|
||||
@@ -5765,48 +5874,72 @@ async fn handle_inserttext(cmd: &Value, state: &DaemonState) -> Result<Value, St
|
||||
Ok(json!({ "inserted": true }))
|
||||
}
|
||||
|
||||
async fn handle_mousemove(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
||||
async fn handle_mousemove(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||
let session_id = mgr.active_session_id()?.to_string();
|
||||
let x = cmd.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let y = cmd.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let params = build_mouse_event_params(
|
||||
&mut state.mouse_state,
|
||||
"mouseMoved",
|
||||
Some(x),
|
||||
Some(y),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
mgr.client
|
||||
.send_command(
|
||||
"Input.dispatchMouseEvent",
|
||||
Some(json!({ "type": "mouseMoved", "x": x, "y": y })),
|
||||
Some(&session_id),
|
||||
)
|
||||
.send_command_typed::<_, Value>("Input.dispatchMouseEvent", ¶ms, Some(&session_id))
|
||||
.await?;
|
||||
Ok(json!({ "moved": true }))
|
||||
}
|
||||
|
||||
async fn handle_mousedown(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
||||
async fn handle_mousedown(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||
let session_id = mgr.active_session_id()?.to_string();
|
||||
let button = cmd.get("button").and_then(|v| v.as_str()).unwrap_or("left");
|
||||
let params = build_mouse_event_params(
|
||||
&mut state.mouse_state,
|
||||
"mousePressed",
|
||||
None,
|
||||
None,
|
||||
Some(button),
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
mgr.client
|
||||
.send_command(
|
||||
"Input.dispatchMouseEvent",
|
||||
Some(json!({ "type": "mousePressed", "x": 0, "y": 0, "button": button, "clickCount": 1 })),
|
||||
Some(&session_id),
|
||||
)
|
||||
.send_command_typed::<_, Value>("Input.dispatchMouseEvent", ¶ms, Some(&session_id))
|
||||
.await?;
|
||||
Ok(json!({ "pressed": true }))
|
||||
}
|
||||
|
||||
async fn handle_mouseup(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
||||
async fn handle_mouseup(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||
let session_id = mgr.active_session_id()?.to_string();
|
||||
let button = cmd.get("button").and_then(|v| v.as_str()).unwrap_or("left");
|
||||
let params = build_mouse_event_params(
|
||||
&mut state.mouse_state,
|
||||
"mouseReleased",
|
||||
None,
|
||||
None,
|
||||
Some(button),
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
mgr.client
|
||||
.send_command(
|
||||
"Input.dispatchMouseEvent",
|
||||
Some(json!({ "type": "mouseReleased", "x": 0, "y": 0, "button": button, "clickCount": 1 })),
|
||||
Some(&session_id),
|
||||
)
|
||||
.send_command_typed::<_, Value>("Input.dispatchMouseEvent", ¶ms, Some(&session_id))
|
||||
.await?;
|
||||
Ok(json!({ "released": true }))
|
||||
}
|
||||
@@ -5862,6 +5995,96 @@ mod tests {
|
||||
assert_eq!(state.session_id, "default");
|
||||
assert!(!state.tracing_state.active);
|
||||
assert!(!state.recording_state.active);
|
||||
assert_eq!(state.mouse_state.x, 0.0);
|
||||
assert_eq!(state.mouse_state.y, 0.0);
|
||||
assert_eq!(state.mouse_state.buttons, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mouse_event_params_preserve_position_and_buttons() {
|
||||
let mut mouse_state = MouseState::default();
|
||||
|
||||
let move_params = build_mouse_event_params(
|
||||
&mut mouse_state,
|
||||
"mouseMoved",
|
||||
Some(120.0),
|
||||
Some(240.0),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(move_params.x, 120.0);
|
||||
assert_eq!(move_params.y, 240.0);
|
||||
assert_eq!(move_params.buttons, Some(0));
|
||||
|
||||
let down_params = build_mouse_event_params(
|
||||
&mut mouse_state,
|
||||
"mousePressed",
|
||||
None,
|
||||
None,
|
||||
Some("left"),
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(down_params.x, 120.0);
|
||||
assert_eq!(down_params.y, 240.0);
|
||||
assert_eq!(down_params.button.as_deref(), Some("left"));
|
||||
assert_eq!(down_params.buttons, Some(1));
|
||||
assert_eq!(mouse_state.buttons, 1);
|
||||
|
||||
let drag_move_params = build_mouse_event_params(
|
||||
&mut mouse_state,
|
||||
"mouseMoved",
|
||||
Some(150.0),
|
||||
Some(260.0),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(drag_move_params.buttons, Some(1));
|
||||
assert_eq!(drag_move_params.button.as_deref(), Some("left"));
|
||||
assert_eq!(mouse_state.x, 150.0);
|
||||
assert_eq!(mouse_state.y, 260.0);
|
||||
|
||||
let up_params = build_mouse_event_params(
|
||||
&mut mouse_state,
|
||||
"mouseReleased",
|
||||
None,
|
||||
None,
|
||||
Some("left"),
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(up_params.x, 150.0);
|
||||
assert_eq!(up_params.y, 260.0);
|
||||
assert_eq!(up_params.buttons, Some(0));
|
||||
assert_eq!(mouse_state.buttons, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_input_state_clears_mouse_state() {
|
||||
let mut state = DaemonState::new();
|
||||
state.mouse_state.x = 12.0;
|
||||
state.mouse_state.y = 34.0;
|
||||
state.mouse_state.buttons = 1;
|
||||
|
||||
state.reset_input_state();
|
||||
|
||||
assert_eq!(state.mouse_state.x, 0.0);
|
||||
assert_eq!(state.mouse_state.y, 0.0);
|
||||
assert_eq!(state.mouse_state.buttons, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user