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::cdp::shadow::ChromiumShadowRoot;
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 s_ele(&self, selector: &str) -> Result<crate::static_element::StaticElement> {
crate::static_element::StaticElement::parse(&self.html().await?)?.ele(selector)
}
pub async fn s_eles(
&self,
selector: &str,
) -> Result<Vec<crate::static_element::StaticElement>> {
crate::static_element::StaticElement::parse(&self.html().await?)?.eles(selector)
}
pub async fn table(&self) -> Result<Vec<Vec<String>>> {
crate::static_element::StaticElement::parse(&self.html().await?)?.table()
}
pub async fn table_records(&self) -> Result<Vec<std::collections::HashMap<String, String>>> {
crate::static_element::StaticElement::parse(&self.html().await?)?.table_records()
}
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 is_in_viewport(&self) -> Result<bool> {
let v = self
.call_value(
"function(){ const r=this.getBoundingClientRect(); \
return r.bottom>0 && r.right>0 && r.top<innerHeight && r.left<innerWidth; }",
vec![],
)
.await?;
Ok(v.as_bool().unwrap_or(false))
}
pub async fn is_covered(&self) -> Result<bool> {
let v = self
.call_value(
"function(){ const r=this.getBoundingClientRect(); \
const x=r.left+r.width/2, y=r.top+r.height/2; const el=document.elementFromPoint(x,y); \
if(!el) return false; return !(el===this || this.contains(el) || el.contains(this)); }",
vec![],
)
.await?;
Ok(v.as_bool().unwrap_or(false))
}
pub async fn is_clickable(&self) -> Result<bool> {
Ok(self.is_displayed().await?
&& self.is_enabled().await?
&& self.is_in_viewport().await?
&& !self.is_covered().await?)
}
pub async fn location(&self) -> Result<(f64, f64)> {
let r = self.rect().await?;
Ok((r.x, r.y))
}
pub async fn style(&self, name: &str) -> Result<String> {
let v = self
.call_value(
"function(n){ return getComputedStyle(this).getPropertyValue(n); }",
vec![json!({ "value": name })],
)
.await?;
Ok(v.as_str().unwrap_or("").trim().to_string())
}
pub async fn remove(&self) -> Result<()> {
self.call_value("function(){ this.remove(); }", vec![])
.await?;
Ok(())
}
pub fn wait(&self) -> ChromiumElementWait {
ChromiumElementWait { ele: self.clone() }
}
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 parent_until(&self, selector: &str) -> Result<ChromiumElement> {
let css = match locator::parse(selector) {
Query::Css(s) => s,
Query::Xpath(_) => {
return Err(Error::msg("parent_until 仅支持 CSS 选择器(xpath 改用 ele)"));
}
};
self.call_to_element(
"function(sel){ return this.parentElement ? this.parentElement.closest(sel) : null; }",
vec![json!({ "value": css })],
"parent_until",
)
.await
}
pub async fn nexts(&self) -> Result<Vec<ChromiumElement>> {
self.call_to_elements(
"function(){ const a=[]; let e=this.nextElementSibling; while(e){a.push(e); e=e.nextElementSibling;} return a; }",
vec![],
)
.await
}
pub async fn prevs(&self) -> Result<Vec<ChromiumElement>> {
self.call_to_elements(
"function(){ const a=[]; let e=this.previousElementSibling; while(e){a.unshift(e); e=e.previousElementSibling;} return a; }",
vec![],
)
.await
}
pub async fn shadow_root(&self) -> Result<ChromiumShadowRoot> {
match self
.core
.call_handle(
&self.object_id,
"function(){ return this.shadowRoot; }",
vec![],
)
.await?
{
Some(oid) => Ok(ChromiumShadowRoot::new(self.core.clone(), oid)),
None => Err(Error::msg("元素没有 open shadowRoot")),
}
}
pub async fn content_frame(&self) -> Result<crate::cdp::frame::ChromiumFrame> {
crate::cdp::frame::ChromiumFrame::from_iframe(self.core.clone(), &self.object_id).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 shortcut(&self, keys: &[&str]) -> Result<()> {
self.focus().await?;
self.core.key_combo(keys).await
}
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 select_text(&self, text: &str) -> Result<()> {
self.call_value(
"function(t){ const opts=Array.from(this.options||[]); \
const o=opts.find(o=>o.text===t || (o.textContent||'').trim()===t); \
if(o){ this.value=o.value; \
this.dispatchEvent(new Event('input',{bubbles:true})); \
this.dispatchEvent(new Event('change',{bubbles:true})); } }",
vec![json!({ "value": text })],
)
.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 upload(&self, path: &str) -> Result<()> {
self.set_files(&[path]).await
}
pub async fn click_to_upload(&self, paths: &[&str], timeout: Option<Duration>) -> Result<bool> {
let mut events = self.core.conn.subscribe();
let sid = self.core.session_id.clone();
self.core
.send(
"Page.setInterceptFileChooserDialog",
json!({ "enabled": true }),
)
.await?;
self.click().await?;
let dur = timeout.unwrap_or_else(|| self.core.timeout());
let backend = tokio::time::timeout(dur, async {
loop {
match events.recv().await {
Ok(ev)
if ev.method == "Page.fileChooserOpened"
&& ev.session_id.as_deref() == Some(&sid) =>
{
return ev.params["backendNodeId"].as_i64();
}
Ok(_) => continue,
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
Err(_) => return None,
}
}
})
.await
.ok()
.flatten();
let _ = self
.core
.send(
"Page.setInterceptFileChooserDialog",
json!({ "enabled": false }),
)
.await;
let Some(bn) = backend else {
return Ok(false);
};
let files: Vec<String> = paths.iter().map(|p| p.to_string()).collect();
self.core
.send(
"DOM.setFileInputFiles",
json!({ "files": files, "backendNodeId": bn }),
)
.await?;
Ok(true)
}
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)
}
}
pub struct ChromiumElementWait {
ele: ChromiumElement,
}
impl ChromiumElementWait {
pub async fn displayed(&self, timeout: Option<Duration>) -> Result<bool> {
self.poll(timeout, |e| async move {
e.is_displayed().await.unwrap_or(false)
})
.await
}
pub async fn hidden(&self, timeout: Option<Duration>) -> Result<bool> {
self.poll(timeout, |e| async move {
!e.is_displayed().await.unwrap_or(false)
})
.await
}
pub async fn deleted(&self, timeout: Option<Duration>) -> Result<bool> {
let deadline =
std::time::Instant::now() + timeout.unwrap_or_else(|| self.ele.core.timeout());
loop {
let gone = match self
.ele
.call_value("function(){ return this.isConnected===false; }", vec![])
.await
{
Ok(v) => v.as_bool().unwrap_or(false),
Err(_) => true, };
if gone {
return Ok(true);
}
if std::time::Instant::now() >= deadline {
return Ok(false);
}
sleep(Duration::from_millis(80)).await;
}
}
pub async fn clickable(&self, timeout: Option<Duration>) -> Result<bool> {
self.poll(timeout, |e| async move {
e.is_clickable().await.unwrap_or(false)
})
.await
}
async fn poll<F, Fut>(&self, timeout: Option<Duration>, pred: F) -> Result<bool>
where
F: Fn(ChromiumElement) -> Fut,
Fut: std::future::Future<Output = bool>,
{
let deadline =
std::time::Instant::now() + timeout.unwrap_or_else(|| self.ele.core.timeout());
loop {
if pred(self.ele.clone()).await {
return Ok(true);
}
if std::time::Instant::now() >= deadline {
return Ok(false);
}
sleep(Duration::from_millis(80)).await;
}
}
}
#[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 }))
}
}
}