use crate::actor::{Element, Mouse, get_key_info};
use crate::browser::cdp::CdpClient;
use crate::error::{BrowsingError, Result};
use serde_json::json;
use std::sync::Arc;
pub struct Page {
client: Arc<CdpClient>,
session_id: String,
mouse: Option<Mouse>,
}
impl Page {
pub fn new(client: Arc<CdpClient>, session_id: String) -> Self {
Self {
client,
session_id,
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
{
if 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" {
if 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 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(())
}
}