use crate::actor::mouse::MouseButton;
use crate::browser::cdp::CdpClient;
use crate::error::{BrowsingError, Result};
use serde_json::json;
use std::sync::Arc;
pub struct Element {
client: Arc<CdpClient>,
session_id: String,
backend_node_id: u32,
}
impl Element {
pub fn new(client: Arc<CdpClient>, session_id: String, backend_node_id: u32) -> Self {
Self {
client,
session_id,
backend_node_id,
}
}
async fn get_node_id(&self) -> Result<u32> {
let params = json!({
"backendNodeIds": [self.backend_node_id]
});
let result = self
.client
.send_command("DOM.pushNodesByBackendIdsToFrontend", params)
.await?;
let node_ids = result
.get("nodeIds")
.and_then(|v| v.as_array())
.ok_or_else(|| BrowsingError::Dom("No nodeIds in response".to_string()))?;
let node_id = node_ids
.first()
.and_then(|v| v.as_u64())
.ok_or_else(|| BrowsingError::Dom("Invalid nodeId".to_string()))?;
Ok(node_id as u32)
}
pub async fn click(
&self,
button: MouseButton,
click_count: u32,
modifiers: Option<Vec<String>>,
) -> Result<()> {
let layout_metrics = self
.client
.send_command("Page.getLayoutMetrics", json!({}))
.await?;
let viewport_width = layout_metrics
.get("layoutViewport")
.and_then(|v| v.get("clientWidth"))
.and_then(|v| v.as_f64())
.unwrap_or(1920.0);
let viewport_height = layout_metrics
.get("layoutViewport")
.and_then(|v| v.get("clientHeight"))
.and_then(|v| v.as_f64())
.unwrap_or(1080.0);
let mut center_x = viewport_width / 2.0;
let mut center_y = viewport_height / 2.0;
let quads_result = self
.client
.send_command(
"DOM.getContentQuads",
json!({ "backendNodeId": self.backend_node_id }),
)
.await;
if let Ok(quads_result) = quads_result {
if let Some(quads) = quads_result.get("quads").and_then(|v| v.as_array()) {
if let Some(first_quad) = quads.first().and_then(|v| v.as_array()) {
if first_quad.len() >= 8 {
let x_coords: Vec<f64> = first_quad
.iter()
.step_by(2)
.filter_map(|v| v.as_f64())
.collect();
let y_coords: Vec<f64> = first_quad
.iter()
.skip(1)
.step_by(2)
.filter_map(|v| v.as_f64())
.collect();
if !x_coords.is_empty() && !y_coords.is_empty() {
center_x = x_coords.iter().sum::<f64>() / x_coords.len() as f64;
center_y = y_coords.iter().sum::<f64>() / y_coords.len() as f64;
}
}
}
}
}
center_x = center_x.max(0.0).min(viewport_width - 1.0);
center_y = center_y.max(0.0).min(viewport_height - 1.0);
let _ = self
.client
.send_command(
"DOM.scrollIntoViewIfNeeded",
json!({ "backendNodeId": self.backend_node_id }),
)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let mut modifier_value = 0u32;
if let Some(modifiers) = modifiers {
for mod_str in modifiers {
match mod_str.as_str() {
"Alt" => modifier_value |= 1,
"Control" => modifier_value |= 2,
"Meta" => modifier_value |= 4,
"Shift" => modifier_value |= 8,
_ => {}
}
}
}
let move_params = json!({
"type": "mouseMoved",
"x": center_x,
"y": center_y,
});
let _ = self
.client
.send_command("Input.dispatchMouseEvent", move_params)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let press_params = json!({
"type": "mousePressed",
"x": center_x,
"y": center_y,
"button": button.to_cdp_string(),
"clickCount": click_count,
"modifiers": modifier_value,
});
let _ = self
.client
.send_command("Input.dispatchMouseEvent", press_params)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(80)).await;
let release_params = json!({
"type": "mouseReleased",
"x": center_x,
"y": center_y,
"button": button.to_cdp_string(),
"clickCount": click_count,
"modifiers": modifier_value,
});
self.client
.send_command("Input.dispatchMouseEvent", release_params)
.await?;
Ok(())
}
pub async fn fill(&self, text: &str) -> Result<()> {
let node_id = self.get_node_id().await?;
let focus_params = json!({ "nodeId": node_id });
let _ = self.client.send_command("DOM.focus", focus_params).await;
let script = format!(
r#"
(() => {{
const node = arguments[0];
node.value = '';
node.focus();
node.value = {};
node.dispatchEvent(new Event('input', {{ bubbles: true }}));
node.dispatchEvent(new Event('change', {{ bubbles: true }}));
return node.value;
}})
"#,
serde_json::to_string(text)?
);
let eval_params = json!({
"expression": script,
"returnByValue": true,
});
self.client
.send_command("Runtime.evaluate", eval_params)
.await?;
Ok(())
}
pub async fn text(&self) -> Result<String> {
let _node_id = self.get_node_id().await?;
let script = r#"
(() => {
const node = arguments[0];
return node.textContent || node.innerText || '';
})
"#
.to_string();
let eval_params = json!({
"expression": script,
"returnByValue": true,
});
let result = self
.client
.send_command("Runtime.evaluate", eval_params)
.await?;
let text = result
.get("result")
.and_then(|v| v.get("value"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
Ok(text)
}
pub async fn get_bounding_box(&self) -> Result<Option<(f64, f64, f64, f64)>> {
let quads_result = self
.client
.send_command(
"DOM.getContentQuads",
json!({ "backendNodeId": self.backend_node_id }),
)
.await;
if let Ok(quads_result) = quads_result {
if let Some(quads) = quads_result.get("quads").and_then(|v| v.as_array()) {
if let Some(first_quad) = quads.first().and_then(|v| v.as_array()) {
if first_quad.len() >= 8 {
let x_coords: Vec<f64> = first_quad
.iter()
.step_by(2)
.filter_map(|v| v.as_f64())
.collect();
let y_coords: Vec<f64> = first_quad
.iter()
.skip(1)
.step_by(2)
.filter_map(|v| v.as_f64())
.collect();
if !x_coords.is_empty() && !y_coords.is_empty() {
let min_x = x_coords.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_x = x_coords.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let min_y = y_coords.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_y = y_coords.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let width = max_x - min_x;
let height = max_y - min_y;
return Ok(Some((min_x, min_y, width, height)));
}
}
}
}
}
Ok(None)
}
pub async fn screenshot(&self, format: Option<&str>, quality: Option<u32>) -> Result<String> {
let (x, y, width, height) = self.get_bounding_box().await?.ok_or_else(|| {
BrowsingError::Browser("Element is not visible or has no bounding box".to_string())
})?;
let format = format.unwrap_or("png");
let mut params = json!({
"format": format,
"clip": {
"x": x,
"y": y,
"width": width,
"height": height,
"scale": 1.0
}
});
if format == "jpeg" {
if let Some(q) = quality {
params["quality"] = json!(q);
}
}
let result = self
.client
.send_command_with_session("Page.captureScreenshot", params, Some(&self.session_id))
.await?;
let data = result
.get("data")
.and_then(|v| v.as_str())
.ok_or_else(|| BrowsingError::Browser("No screenshot data".to_string()))?;
Ok(data.to_string())
}
}