use std::collections::HashMap;
use serde_json::json;
use crate::cdp::client::CdpClient;
use crate::cdp::types::{DispatchMouseEventParams, MouseButton, MouseEventType};
use crate::element::{ElementError, js_exception, resolve_uid, wait_for_stabilization};
use crate::element_ref::ElementRef;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PointerVerb {
Click,
DblClick,
}
impl PointerVerb {
const fn word(self) -> &'static str {
match self {
Self::Click => "click",
Self::DblClick => "double-click",
}
}
async fn js_fallback(self, client: &CdpClient, object_id: &str) -> Result<(), ElementError> {
match self {
Self::Click => js_click(client, object_id).await,
Self::DblClick => js_dblclick(client, object_id).await,
}
}
async fn dispatch_at(self, client: &CdpClient, point: (f64, f64)) -> Result<(), ElementError> {
match self {
Self::Click => dispatch_click_at(client, point.0, point.1).await,
Self::DblClick => dblclick_at_coords(client, point.0, point.1).await,
}
}
}
pub async fn click(
client: &CdpClient,
uid_map: &HashMap<String, ElementRef>,
uid: &str,
on_intercept: crate::hit_test::OnIntercept,
) -> Result<crate::hit_test::Dispatched, ElementError> {
pointer(client, uid_map, uid, PointerVerb::Click, on_intercept).await
}
pub async fn dblclick(
client: &CdpClient,
uid_map: &HashMap<String, ElementRef>,
uid: &str,
on_intercept: crate::hit_test::OnIntercept,
) -> Result<crate::hit_test::Dispatched, ElementError> {
pointer(client, uid_map, uid, PointerVerb::DblClick, on_intercept).await
}
async fn pointer(
client: &CdpClient,
uid_map: &HashMap<String, ElementRef>,
uid: &str,
verb: PointerVerb,
on_intercept: crate::hit_test::OnIntercept,
) -> Result<crate::hit_test::Dispatched, ElementError> {
let resolved = resolve_uid(client, uid_map, uid).await?;
if resolved.center.is_none() {
verb.js_fallback(client, &resolved.object_id).await?;
return Ok(crate::hit_test::Dispatched::js().named(Some(uid.to_string()), None, None));
}
let outcome = aim_and_dispatch(
client,
verb,
&resolved.object_id,
resolved.center,
on_intercept,
&format!("uid={uid}"),
)
.await
.map_err(|e| e.naming(Some(uid.to_string()), None, None))?;
Ok(outcome.named(Some(uid.to_string()), None, None))
}
pub async fn aim_and_dispatch(
client: &CdpClient,
verb: PointerVerb,
object_id: &str,
fallback_center: Option<(f64, f64)>,
on_intercept: crate::hit_test::OnIntercept,
target: &str,
) -> Result<crate::hit_test::Dispatched, ElementError> {
use crate::hit_test::{Aim, Dispatched};
use crate::verdict::Delivery;
let (point, delivery, receiver, unaimable) = match crate::hit_test::aim(client, object_id).await
{
Aim::NoBox => {
verb.js_fallback(client, object_id).await?;
return Ok(Dispatched::js());
}
Aim::Unprobed => {
let Some(center) = fallback_center else {
verb.js_fallback(client, object_id).await?;
return Ok(Dispatched::js());
};
(center, Delivery::NotProbed, None, None)
}
Aim::At {
point,
delivery,
receiver,
unaimable,
} => (point, delivery, receiver, unaimable),
};
if matches!(delivery, Delivery::NotSettled | Delivery::OffTarget) {
return Ok(Dispatched::skipped(delivery, point, None).unaimed(unaimable));
}
if delivery == Delivery::Intercepted
&& crate::hit_test::should_refuse_intercept(on_intercept, receiver.as_ref())
{
return Err(refusal(
Dispatched::skipped(delivery, point, receiver).under(on_intercept),
verb.word(),
target,
));
}
verb.dispatch_at(client, point).await?;
Ok(Dispatched::landed(delivery, point, receiver))
}
fn refusal(dispatched: crate::hit_test::Dispatched, verb: &str, target: &str) -> ElementError {
let message = dispatched
.refusal_message(verb, target)
.unwrap_or_else(|| format!("Refused to {verb} {target}"));
ElementError::Refused(Box::new(crate::hit_test::Refused::new(message, dispatched)))
}
async fn dispatch_click_at(client: &CdpClient, cx: f64, cy: f64) -> Result<(), ElementError> {
client.ensure_foreground().await;
let nav_events = client.page_events();
client.mark_dispatch();
if client.touch_emulation_enabled() {
client
.send_input(
"Input.dispatchTouchEvent",
json!({
"type": "touchStart",
"touchPoints": [{"x": cx, "y": cy, "id": 0}],
}),
)
.await
.map_err(|e| ElementError::Action(format!("touchStart failed: {e}")))?;
client
.send_input(
"Input.dispatchTouchEvent",
json!({"type": "touchEnd", "touchPoints": []}),
)
.await
.map_err(|e| ElementError::Action(format!("touchEnd failed: {e}")))?;
} else {
client
.send_input(
"Input.dispatchMouseEvent",
DispatchMouseEventParams {
event_type: MouseEventType::MousePressed,
x: cx,
y: cy,
button: Some(MouseButton::Left),
buttons: Some(1),
click_count: Some(1),
modifiers: None,
timestamp: None,
delta_x: None,
delta_y: None,
pointer_type: Some("mouse".into()),
},
)
.await
.map_err(|e| ElementError::Action(format!("mousePressed failed: {e}")))?;
client
.send_input(
"Input.dispatchMouseEvent",
DispatchMouseEventParams {
event_type: MouseEventType::MouseReleased,
x: cx,
y: cy,
button: Some(MouseButton::Left),
buttons: Some(0),
click_count: Some(1),
modifiers: None,
timestamp: None,
delta_x: None,
delta_y: None,
pointer_type: Some("mouse".into()),
},
)
.await
.map_err(|e| ElementError::Action(format!("mouseReleased failed: {e}")))?;
}
wait_for_stabilization(client, nav_events).await;
Ok(())
}
pub async fn click_at_coords(client: &CdpClient, x: f64, y: f64) -> Result<(), ElementError> {
dispatch_click_at(client, x, y).await
}
pub async fn js_click(client: &CdpClient, object_id: &str) -> Result<(), ElementError> {
let nav_events = client.page_events();
client.mark_dispatch();
let result: serde_json::Value = client
.call(
"Runtime.callFunctionOn",
json!({
"objectId": object_id,
"functionDeclaration": "function() { this.click(); }",
"returnByValue": true,
}),
)
.await
.map_err(|e| ElementError::Action(format!("JS click fallback failed: {e}")))?;
if let Some(thrown) = js_exception(&result) {
return Err(ElementError::Action(format!("JS click threw: {thrown}")));
}
wait_for_stabilization(client, nav_events).await;
Ok(())
}
pub async fn js_dblclick(client: &CdpClient, object_id: &str) -> Result<(), ElementError> {
let nav_events = client.page_events();
client.mark_dispatch();
client
.call::<_, serde_json::Value>(
"Runtime.callFunctionOn",
json!({
"objectId": object_id,
"functionDeclaration": "function() { this.dispatchEvent(new MouseEvent('dblclick', {bubbles:true, cancelable:true})); }",
"returnByValue": true,
}),
)
.await
.map_err(|e| ElementError::Action(format!("JS dblclick failed: {e}")))?;
wait_for_stabilization(client, nav_events).await;
Ok(())
}
pub async fn dblclick_at_coords(client: &CdpClient, x: f64, y: f64) -> Result<(), ElementError> {
client.ensure_foreground().await;
let nav_events = client.page_events();
client.mark_dispatch();
for click_count in [1, 2] {
client
.send_input(
"Input.dispatchMouseEvent",
DispatchMouseEventParams {
event_type: MouseEventType::MousePressed,
x,
y,
button: Some(MouseButton::Left),
buttons: Some(1),
click_count: Some(click_count),
modifiers: None,
timestamp: None,
delta_x: None,
delta_y: None,
pointer_type: Some("mouse".into()),
},
)
.await
.map_err(|e| ElementError::Action(format!("mousePressed failed: {e}")))?;
client
.send_input(
"Input.dispatchMouseEvent",
DispatchMouseEventParams {
event_type: MouseEventType::MouseReleased,
x,
y,
button: Some(MouseButton::Left),
buttons: Some(0),
click_count: Some(click_count),
modifiers: None,
timestamp: None,
delta_x: None,
delta_y: None,
pointer_type: Some("mouse".into()),
},
)
.await
.map_err(|e| ElementError::Action(format!("mouseReleased failed: {e}")))?;
}
wait_for_stabilization(client, nav_events).await;
Ok(())
}
pub async fn hover(
client: &CdpClient,
uid_map: &HashMap<String, ElementRef>,
uid: &str,
) -> Result<(), ElementError> {
let resolved = resolve_uid(client, uid_map, uid).await?;
let (x, y) = resolved.center.ok_or_else(|| {
ElementError::NotInteractable(format!("Element uid={uid} has no visible box model."))
})?;
client.ensure_foreground().await;
client
.send_input(
"Input.dispatchMouseEvent",
DispatchMouseEventParams {
event_type: MouseEventType::MouseMoved,
x,
y,
button: None,
buttons: None,
click_count: None,
modifiers: None,
timestamp: None,
delta_x: None,
delta_y: None,
pointer_type: Some("mouse".into()),
},
)
.await
.map_err(|e| ElementError::Action(format!("hover failed: {e}")))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::PointerVerb;
#[test]
fn each_pointer_verb_refuses_in_its_own_word() {
assert_eq!(PointerVerb::Click.word(), "click");
assert_eq!(PointerVerb::DblClick.word(), "double-click");
}
}