use crate::actor::{Element, Mouse, MouseButton, get_key_info};
use crate::browser::cdp::CdpClient;
use crate::error::{BrowsingError, Result};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShadowElementInfo {
pub tag_name: String,
pub text: Option<String>,
pub id: Option<String>,
pub class: Option<String>,
pub name: Option<String>,
pub visible: bool,
pub bounding_box: Option<serde_json::Value>,
}
pub struct Page {
client: Arc<CdpClient>,
session_id: Arc<str>,
mouse: Option<Mouse>,
}
impl Page {
pub fn new(client: Arc<CdpClient>, session_id: impl Into<Arc<str>>) -> Self {
Self {
client,
session_id: session_id.into(),
mouse: None,
}
}
pub async fn mouse(&mut self) -> &mut Mouse {
if self.mouse.is_none() {
self.mouse = Some(Mouse::new(
Arc::clone(&self.client),
self.session_id.clone(),
));
}
self.mouse.as_mut().unwrap()
}
pub async fn reload(&self) -> Result<()> {
self.client.send_command("Page.reload", json!({})).await?;
Ok(())
}
pub async fn go_back(&self) -> Result<()> {
self.evaluate("history.back()").await?;
Ok(())
}
pub async fn go_forward(&self) -> Result<()> {
self.evaluate("history.forward()").await?;
Ok(())
}
pub async fn goto(&self, url: &str) -> Result<()> {
let params = json!({
"url": url
});
self.client.send_command("Page.navigate", params).await?;
Ok(())
}
pub async fn get_element(&self, backend_node_id: u32) -> Element {
Element::new(
Arc::clone(&self.client),
self.session_id.clone(),
backend_node_id,
)
}
pub async fn get_elements_by_css_selector(&self, selector: &str) -> Result<Vec<Element>> {
let document_result = self
.client
.send_command("DOM.getDocument", json!({ "depth": 0 }))
.await?;
let root_node_id = document_result
.get("root")
.and_then(|v| v.get("nodeId"))
.and_then(|v| v.as_u64())
.ok_or_else(|| BrowsingError::Dom("No root node found".to_string()))?;
let query_params = json!({
"nodeId": root_node_id,
"selector": selector
});
let query_result = self
.client
.send_command("DOM.querySelectorAll", query_params)
.await?;
let node_ids = query_result
.get("nodeIds")
.and_then(|v| v.as_array())
.ok_or_else(|| BrowsingError::Dom("No nodeIds in query result".to_string()))?;
let mut elements = Vec::new();
for node_id_value in node_ids {
if let Some(node_id) = node_id_value.as_u64() {
let describe_params = json!({
"nodeId": node_id
});
if let Ok(describe_result) = self
.client
.send_command("DOM.describeNode", describe_params)
.await
&& let Some(backend_node_id) = describe_result
.get("node")
.and_then(|v| v.get("backendNodeId"))
.and_then(|v| v.as_u64())
{
elements.push(Element::new(
Arc::clone(&self.client),
self.session_id.clone(),
backend_node_id as u32,
));
}
}
}
Ok(elements)
}
pub async fn evaluate(&self, expression: &str) -> Result<String> {
let params = json!({
"expression": expression,
"returnByValue": true,
"awaitPromise": true
});
let result = self.client.send_command("Runtime.evaluate", params).await?;
if let Some(exception) = result.get("exceptionDetails") {
return Err(BrowsingError::Dom(format!(
"JavaScript evaluation failed: {exception}"
)));
}
let value = result.get("result").and_then(|v| v.get("value"));
match value {
Some(serde_json::Value::String(s)) => Ok(s.clone()),
Some(v) => Ok(serde_json::to_string(v)?),
None => Ok(String::new()),
}
}
pub async fn screenshot(&self, format: Option<&str>, quality: Option<u32>) -> Result<String> {
self.screenshot_with_options(format, quality, false, None)
.await
}
pub async fn screenshot_with_options(
&self,
format: Option<&str>,
quality: Option<u32>,
full_page: bool,
clip: Option<(f64, f64, f64, f64)>,
) -> Result<String> {
let format = format.unwrap_or("png");
let mut params = json!({
"format": format,
"captureBeyondViewport": full_page
});
if format == "jpeg"
&& let Some(q) = quality {
params["quality"] = json!(q);
}
if let Some((x, y, width, height)) = clip {
params["clip"] = json!({
"x": x,
"y": y,
"width": width,
"height": height,
"scale": 1.0
});
}
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())
}
pub async fn press(&self, key: &str) -> Result<()> {
if key.contains('+') {
let parts: Vec<&str> = key.split('+').collect();
let modifiers = &parts[..parts.len() - 1];
let main_key = parts.last().unwrap();
let mut modifier_value = 0u32;
let modifier_map: std::collections::HashMap<&str, u32> =
[("Alt", 1), ("Control", 2), ("Meta", 4), ("Shift", 8)]
.iter()
.cloned()
.collect();
for mod_str in modifiers {
if let Some(&val) = modifier_map.get(mod_str) {
modifier_value |= val;
}
}
for mod_str in modifiers {
let (code, vk_code) = get_key_info(mod_str);
let mut params = json!({
"type": "keyDown",
"key": mod_str,
"code": code
});
if let Some(vk) = vk_code {
params["windowsVirtualKeyCode"] = json!(vk);
}
self.client
.send_command("Input.dispatchKeyEvent", params)
.await?;
}
let (main_code, main_vk_code) = get_key_info(main_key);
let mut main_down_params = json!({
"type": "keyDown",
"key": main_key,
"code": main_code,
"modifiers": modifier_value
});
if let Some(vk) = main_vk_code {
main_down_params["windowsVirtualKeyCode"] = json!(vk);
}
self.client
.send_command("Input.dispatchKeyEvent", main_down_params)
.await?;
let mut main_up_params = json!({
"type": "keyUp",
"key": main_key,
"code": main_code,
"modifiers": modifier_value
});
if let Some(vk) = main_vk_code {
main_up_params["windowsVirtualKeyCode"] = json!(vk);
}
self.client
.send_command("Input.dispatchKeyEvent", main_up_params)
.await?;
for mod_str in modifiers.iter().rev() {
let (code, vk_code) = get_key_info(mod_str);
let mut params = json!({
"type": "keyUp",
"key": mod_str,
"code": code
});
if let Some(vk) = vk_code {
params["windowsVirtualKeyCode"] = json!(vk);
}
self.client
.send_command("Input.dispatchKeyEvent", params)
.await?;
}
} else {
let (code, vk_code) = get_key_info(key);
let mut key_down_params = json!({
"type": "keyDown",
"key": key,
"code": code
});
if let Some(vk) = vk_code {
key_down_params["windowsVirtualKeyCode"] = json!(vk);
}
self.client
.send_command("Input.dispatchKeyEvent", key_down_params)
.await?;
let mut key_up_params = json!({
"type": "keyUp",
"key": key,
"code": code
});
if let Some(vk) = vk_code {
key_up_params["windowsVirtualKeyCode"] = json!(vk);
}
self.client
.send_command("Input.dispatchKeyEvent", key_up_params)
.await?;
}
Ok(())
}
pub async fn verify_backend_node(&self, backend_node_id: u32) -> bool {
let params = serde_json::json!({
"backendNodeId": backend_node_id,
"depth": 0
});
self.client
.send_command_with_session("DOM.describeNode", params, Some(&self.session_id))
.await
.is_ok()
}
pub async fn set_viewport_size(&self, width: u32, height: u32) -> Result<()> {
let params = json!({
"width": width,
"height": height,
"deviceScaleFactor": 1.0,
"mobile": false
});
self.client
.send_command("Emulation.setDeviceMetricsOverride", params)
.await?;
Ok(())
}
pub async fn query_shadow_dom(
&self,
host_selector: &str,
inner_selector: &str,
) -> Result<Option<ShadowElementInfo>> {
let script = format!(
r#"(function() {{
var host = document.querySelector({});
if (!host || !host.shadowRoot) return null;
var el = host.shadowRoot.querySelector({});
if (!el) return null;
var rect = el.getBoundingClientRect();
return JSON.stringify({{
tag_name: el.tagName.toLowerCase(),
text: el.textContent || '',
id: el.id || null,
class: el.className || null,
name: el.getAttribute('name') || null,
visible: !!(el.offsetParent || el.getClientRects().length > 0),
bounding_box: {{ x: rect.x, y: rect.y, width: rect.width, height: rect.height }}
}});
}})()"#,
serde_json::json!(host_selector),
serde_json::json!(inner_selector)
);
let result = self.evaluate(&script).await?;
if result.is_empty() || result == "null" {
return Ok(None);
}
let info: ShadowElementInfo = serde_json::from_str(&result)
.map_err(|e| BrowsingError::Dom(format!("Shadow DOM parse error: {e}")))?;
Ok(Some(info))
}
pub async fn query_all_shadow_dom(
&self,
host_selector: &str,
inner_selector: &str,
) -> Result<Vec<ShadowElementInfo>> {
let script = format!(
r#"(function() {{
var host = document.querySelector({});
if (!host || !host.shadowRoot) return '[]';
var els = host.shadowRoot.querySelectorAll({});
var result = [];
for (var i = 0; i < els.length; i++) {{
var el = els[i];
var rect = el.getBoundingClientRect();
result.push({{
tag_name: el.tagName.toLowerCase(),
text: el.textContent || '',
id: el.id || null,
class: el.className || null,
name: el.getAttribute('name') || null,
visible: !!(el.offsetParent || el.getClientRects().length > 0),
bounding_box: {{ x: rect.x, y: rect.y, width: rect.width, height: rect.height }}
}});
}}
return JSON.stringify(result);
}})()"#,
serde_json::json!(host_selector),
serde_json::json!(inner_selector)
);
let result = self.evaluate(&script).await?;
if result.is_empty() {
return Ok(Vec::new());
}
let infos: Vec<ShadowElementInfo> = serde_json::from_str(&result)
.map_err(|e| BrowsingError::Dom(format!("Shadow DOM parse error: {e}")))?;
Ok(infos)
}
pub async fn has_shadow_root(&self, selector: &str) -> Result<bool> {
let script = format!(
r#"(function() {{
var el = document.querySelector({});
return !!(el && el.shadowRoot);
}})()"#,
serde_json::json!(selector)
);
let result = self.evaluate(&script).await?;
Ok(result.trim() == "true")
}
pub async fn get_shadow_host_elements(&self) -> Result<Vec<String>> {
let script = r#"(function() {
var hosts = [];
var all = document.querySelectorAll('*');
for (var i = 0; i < all.length; i++) {
if (all[i].shadowRoot) {
var tag = all[i].tagName.toLowerCase();
var id = all[i].id ? '#' + all[i].id : '';
var cls = all[i].className ? '.' + all[i].className.split(' ').join('.') : '';
hosts.push(tag + id + cls);
}
}
return JSON.stringify(hosts);
})()"#;
let result = self.evaluate(script).await?;
let hosts: Vec<String> = serde_json::from_str(&result).unwrap_or_default();
Ok(hosts)
}
pub async fn drag_and_drop(
&self,
start_x: f64,
start_y: f64,
end_x: f64,
end_y: f64,
) -> Result<()> {
let mouse = Mouse::new(Arc::clone(&self.client), self.session_id.clone());
mouse.drag(start_x, start_y, end_x, end_y, MouseButton::Left).await
}
pub async fn drag_element_to(
&self,
selector: &str,
end_x: f64,
end_y: f64,
) -> Result<()> {
let script = format!(
r#"(function() {{
var el = document.querySelector({});
if (!el) return null;
var rect = el.getBoundingClientRect();
return JSON.stringify({{ x: rect.x + rect.width/2, y: rect.y + rect.height/2 }});
}})()"#,
serde_json::json!(selector)
);
let result = self.evaluate(&script).await?;
if result.is_empty() || result == "null" {
return Err(BrowsingError::Dom(format!("Element not found: {selector}")));
}
let coords: serde_json::Value = serde_json::from_str(&result)
.map_err(|e| BrowsingError::Dom(format!("Drag parse error: {e}")))?;
let start_x = coords.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
let start_y = coords.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
self.drag_and_drop(start_x, start_y, end_x, end_y).await
}
}