use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use serde_json::{Value, json};
use tokio::time::sleep;
use crate::cdp::core::{CdpCore, Xorshift, human_drag_track, seed_from_clock};
use crate::keys::KeyInput;
use crate::locator::{self, Query};
use crate::{Error, Result};
#[derive(Clone)]
pub struct ChromiumElement {
core: Arc<CdpCore>,
object_id: String,
}
impl ChromiumElement {
pub(crate) fn new(core: Arc<CdpCore>, object_id: String) -> Self {
Self { core, object_id }
}
pub fn object_id(&self) -> &str {
&self.object_id
}
async fn call_value(&self, declaration: &str, args: Vec<Value>) -> Result<Value> {
self.core
.call_value(&self.object_id, declaration, args)
.await
}
async fn call_to_element(
&self,
declaration: &str,
args: Vec<Value>,
what: &str,
) -> Result<ChromiumElement> {
match self
.core
.call_handle(&self.object_id, declaration, args)
.await?
{
Some(oid) => Ok(ChromiumElement::new(self.core.clone(), oid)),
None => Err(Error::ElementNotFound(what.to_string())),
}
}
async fn call_to_elements(
&self,
declaration: &str,
args: Vec<Value>,
) -> Result<Vec<ChromiumElement>> {
let Some(arr) = self
.core
.call_handle(&self.object_id, declaration, args)
.await?
else {
return Ok(Vec::new());
};
let oids = self.core.array_object_ids(&arr).await?;
Ok(oids
.into_iter()
.map(|oid| ChromiumElement::new(self.core.clone(), oid))
.collect())
}
pub async fn text(&self) -> Result<String> {
let v = self
.call_value(
"function(){ return this.innerText ?? this.textContent ?? ''; }",
vec![],
)
.await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn attr(&self, name: &str) -> Result<Option<String>> {
let v = self
.call_value(
"function(name){ return this.getAttribute(name); }",
vec![json!({ "value": name })],
)
.await?;
Ok(v.as_str().map(str::to_string))
}
pub async fn attrs(&self) -> Result<HashMap<String, String>> {
let v = self
.call_value(
"function(){ const o={}; for (const a of (this.attributes||[])) o[a.name]=a.value; return o; }",
vec![],
)
.await?;
let mut map = HashMap::new();
if let Some(o) = v.as_object() {
for (k, val) in o {
if let Some(s) = val.as_str() {
map.insert(k.clone(), s.to_string());
}
}
}
Ok(map)
}
pub async fn property(&self, name: &str) -> Result<Value> {
self.call_value(
"function(n){ return this[n]; }",
vec![json!({ "value": name })],
)
.await
}
pub async fn value(&self) -> Result<String> {
let v = self
.call_value("function(){ return this.value ?? ''; }", vec![])
.await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn tag(&self) -> Result<String> {
let v = self
.call_value(
"function(){ return this.tagName ? this.tagName.toLowerCase() : ''; }",
vec![],
)
.await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn html(&self) -> Result<String> {
let v = self
.call_value("function(){ return this.outerHTML ?? ''; }", vec![])
.await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn inner_html(&self) -> Result<String> {
let v = self
.call_value("function(){ return this.innerHTML ?? ''; }", vec![])
.await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn is_displayed(&self) -> Result<bool> {
let v = self
.call_value(
"function(){ const r=this.getClientRects(); const s=getComputedStyle(this); \
return r.length>0 && s.visibility!=='hidden' && s.display!=='none'; }",
vec![],
)
.await?;
Ok(v.as_bool().unwrap_or(false))
}
pub async fn is_enabled(&self) -> Result<bool> {
let v = self
.call_value("function(){ return !this.disabled; }", vec![])
.await?;
Ok(v.as_bool().unwrap_or(true))
}
pub async fn is_checked(&self) -> Result<bool> {
let v = self
.call_value("function(){ return !!this.checked; }", vec![])
.await?;
Ok(v.as_bool().unwrap_or(false))
}
pub async fn rect(&self) -> Result<ElementRect> {
let v = self
.call_value(
"function(){ const r=this.getBoundingClientRect(); return { \
x:r.left+scrollX, y:r.top+scrollY, vx:r.left, vy:r.top, w:r.width, h:r.height }; }",
vec![],
)
.await?;
let f = |k: &str| v.get(k).and_then(Value::as_f64).unwrap_or(0.0);
Ok(ElementRect {
x: f("x"),
y: f("y"),
viewport_x: f("vx"),
viewport_y: f("vy"),
width: f("w"),
height: f("h"),
})
}
pub async fn size(&self) -> Result<(f64, f64)> {
let r = self.rect().await?;
Ok((r.width, r.height))
}
pub async fn run_js(&self, body: &str) -> Result<Value> {
let decl = format!("function(){{ {body} }}");
self.call_value(&decl, vec![]).await
}
pub async fn ele(&self, selector: &str) -> Result<ChromiumElement> {
let (decl, arg) = query_decl(selector, true);
self.call_to_element(&decl, vec![arg], selector).await
}
pub async fn eles(&self, selector: &str) -> Result<Vec<ChromiumElement>> {
let (decl, arg) = query_decl(selector, false);
self.call_to_elements(&decl, vec![arg]).await
}
pub async fn parent(&self) -> Result<ChromiumElement> {
self.call_to_element("function(){ return this.parentElement; }", vec![], "parent")
.await
}
pub async fn parent_n(&self, level: usize) -> Result<ChromiumElement> {
self.call_to_element(
"function(n){ let e=this; for (let i=0;i<n&&e;i++) e=e.parentElement; return e; }",
vec![json!({ "value": level })],
"parent_n",
)
.await
}
pub async fn children(&self) -> Result<Vec<ChromiumElement>> {
self.call_to_elements("function(){ return Array.from(this.children); }", vec![])
.await
}
pub async fn child(&self, index: usize) -> Result<ChromiumElement> {
self.call_to_element(
"function(i){ return this.children[i] || null; }",
vec![json!({ "value": index })],
"child",
)
.await
}
pub async fn next(&self) -> Result<ChromiumElement> {
self.call_to_element(
"function(){ return this.nextElementSibling; }",
vec![],
"next",
)
.await
}
pub async fn prev(&self) -> Result<ChromiumElement> {
self.call_to_element(
"function(){ return this.previousElementSibling; }",
vec![],
"prev",
)
.await
}
pub async fn siblings(&self) -> Result<Vec<ChromiumElement>> {
self.call_to_elements(
"function(){ return this.parentElement ? \
Array.from(this.parentElement.children).filter(c=>c!==this) : []; }",
vec![],
)
.await
}
pub async fn scroll_into_view(&self) -> Result<()> {
self.call_value(
"function(){ this.scrollIntoView({ block:'center', inline:'center' }); }",
vec![],
)
.await?;
Ok(())
}
pub(crate) async fn center_point(&self) -> Result<(f64, f64)> {
self.scroll_into_view().await?;
let v = self
.call_value(
"function(){ const r=this.getBoundingClientRect(); \
return [r.left + r.width/2, r.top + r.height/2]; }",
vec![],
)
.await?;
let x = v.get(0).and_then(Value::as_f64).unwrap_or(0.0);
let y = v.get(1).and_then(Value::as_f64).unwrap_or(0.0);
if x <= 0.0 && y <= 0.0 {
return Err(Error::Other("元素无可见内容区(可能不可见)".into()));
}
Ok((x, y))
}
pub async fn click(&self) -> Result<()> {
let (x, y) = self.center_point().await?;
self.core
.dispatch_mouse("mouseMoved", x, y, "none", 0, 0)
.await?;
self.core
.dispatch_mouse("mousePressed", x, y, "left", 1, 1)
.await?;
self.core
.dispatch_mouse("mouseReleased", x, y, "left", 0, 1)
.await?;
Ok(())
}
pub async fn hover(&self) -> Result<()> {
let (x, y) = self.center_point().await?;
self.core
.dispatch_mouse("mouseMoved", x, y, "none", 0, 0)
.await
}
pub async fn drag(&self, dx: f64, dy: f64, duration: f64) -> Result<()> {
let (x0, y0) = self.center_point().await?;
self.drag_from(x0, y0, dx, dy, duration).await
}
pub async fn drag_to(&self, x: f64, y: f64, duration: f64) -> Result<()> {
let (x0, y0) = self.center_point().await?;
self.drag_from(x0, y0, x - x0, y - y0, duration).await
}
async fn drag_from(&self, x0: f64, y0: f64, dx: f64, dy: f64, duration: f64) -> Result<()> {
let mut rng = Xorshift::new(seed_from_clock());
let steps = human_drag_track(dx, dy, duration, rng.next_u64());
self.core
.dispatch_mouse("mouseMoved", x0, y0, "none", 0, 0)
.await?;
sleep(Duration::from_millis(rng.range_ms(20, 60))).await;
self.core
.dispatch_mouse("mousePressed", x0, y0, "left", 1, 1)
.await?;
sleep(Duration::from_millis(rng.range_ms(70, 140))).await;
for (ox, oy, delay) in steps {
self.core
.dispatch_mouse_fire("mouseMoved", x0 + ox, y0 + oy, "none", 1, 0)?;
if delay > 0 {
sleep(Duration::from_millis(delay)).await;
}
}
sleep(Duration::from_millis(rng.range_ms(60, 130))).await;
self.core
.dispatch_mouse("mouseReleased", x0 + dx, y0 + dy, "left", 0, 1)
.await?;
Ok(())
}
pub async fn focus(&self) -> Result<()> {
self.call_value("function(){ this.focus(); }", vec![])
.await?;
Ok(())
}
pub async fn input(&self, text: &str) -> Result<()> {
self.focus().await?;
self.core.insert_text(text).await
}
pub async fn input_human(&self, text: &str) -> Result<()> {
self.focus().await?;
let mut rng = Xorshift::new(seed_from_clock());
for ch in text.chars() {
let s = ch.to_string();
self.core.press_key(&s).await?;
let delay = 30 + (rng.next_u64() % 110);
sleep(Duration::from_millis(delay)).await;
}
Ok(())
}
pub async fn input_keys(&self, parts: &[KeyInput]) -> Result<()> {
self.focus().await?;
for p in parts {
match p {
KeyInput::Text(t) => self.core.insert_text(t).await?,
KeyInput::Key(k) => self.core.press_key(k).await?,
}
}
Ok(())
}
pub async fn clear(&self) -> Result<()> {
self.call_value(
"function(){ this.focus(); if ('value' in this) { this.value=''; \
this.dispatchEvent(new Event('input',{bubbles:true})); \
this.dispatchEvent(new Event('change',{bubbles:true})); } \
else if (this.isContentEditable) { this.textContent=''; } }",
vec![],
)
.await?;
Ok(())
}
pub async fn select_value(&self, value: &str) -> Result<()> {
self.call_value(
"function(val){ this.value=val; \
this.dispatchEvent(new Event('input',{bubbles:true})); \
this.dispatchEvent(new Event('change',{bubbles:true})); }",
vec![json!({ "value": value })],
)
.await?;
Ok(())
}
pub async fn set_checked(&self, checked: bool) -> Result<()> {
self.call_value(
"function(c){ if (this.checked!==c) { this.checked=c; \
this.dispatchEvent(new Event('input',{bubbles:true})); \
this.dispatchEvent(new Event('change',{bubbles:true})); } }",
vec![json!({ "value": checked })],
)
.await?;
Ok(())
}
pub async fn set_files(&self, paths: &[&str]) -> Result<()> {
let files: Vec<String> = paths.iter().map(|p| p.to_string()).collect();
self.core
.send(
"DOM.setFileInputFiles",
json!({ "objectId": self.object_id, "files": files }),
)
.await?;
Ok(())
}
pub async fn screenshot_bytes(&self) -> Result<Vec<u8>> {
self.scroll_into_view().await?;
let v = self
.call_value(
"function(){ const r=this.getBoundingClientRect(); \
return [r.left+scrollX, r.top+scrollY, Math.max(r.width,1), Math.max(r.height,1)]; }",
vec![],
)
.await?;
let x = v.get(0).and_then(Value::as_f64).unwrap_or(0.0);
let y = v.get(1).and_then(Value::as_f64).unwrap_or(0.0);
let w = v.get(2).and_then(Value::as_f64).unwrap_or(1.0);
let h = v.get(3).and_then(Value::as_f64).unwrap_or(1.0);
let clip = json!({ "x": x, "y": y, "width": w, "height": h, "scale": 1 });
let r = self
.core
.send(
"Page.captureScreenshot",
json!({ "format": "png", "clip": clip, "captureBeyondViewport": true }),
)
.await?;
let data = r["data"]
.as_str()
.ok_or_else(|| Error::msg("CDP: 无元素截图数据"))?;
crate::util::base64_decode(data).ok_or_else(|| Error::msg("CDP: 元素截图 base64 解码失败"))
}
pub async fn get_screenshot(&self, path: impl AsRef<Path>) -> Result<PathBuf> {
let path = path.as_ref().to_path_buf();
let bytes = self.screenshot_bytes().await?;
tokio::fs::write(&path, &bytes)
.await
.map_err(|e| Error::msg(format!("写入截图 {} 失败: {e}", path.display())))?;
Ok(path)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ElementRect {
pub x: f64,
pub y: f64,
pub viewport_x: f64,
pub viewport_y: f64,
pub width: f64,
pub height: f64,
}
pub(crate) fn query_decl(selector: &str, single: bool) -> (String, Value) {
match locator::parse(selector) {
Query::Css(sel) => {
let decl = if single {
"function(s){ return this.querySelector(s); }"
} else {
"function(s){ return Array.from(this.querySelectorAll(s)); }"
};
(decl.to_string(), json!({ "value": sel }))
}
Query::Xpath(xp) => {
let decl = if single {
"function(xp){ return document.evaluate(xp, this, null, 9, null).singleNodeValue; }"
} else {
"function(xp){ const it=document.evaluate(xp, this, null, 7, null); \
const a=[]; for (let i=0;i<it.snapshotLength;i++) a.push(it.snapshotItem(i)); return a; }"
};
(decl.to_string(), json!({ "value": xp }))
}
}
}