use std::sync::Arc;
use std::time::Duration;
use serde_json::{Value, json};
use tokio::time::{Instant, sleep, timeout_at};
use crate::cdp::core::CdpCore;
use crate::cdp::element::ChromiumElement;
use crate::cdp::interceptor::CdpIntercept;
use crate::cdp::listener::CdpListen;
use crate::keys::KeyInput;
use crate::locator::{self, Query};
use crate::{Error, Result};
#[derive(Clone)]
pub struct ChromiumTab {
core: Arc<CdpCore>,
}
impl ChromiumTab {
pub(crate) fn new(core: Arc<CdpCore>) -> Self {
Self { core }
}
pub fn set_timeout(&self, d: Duration) {
self.core.set_timeout(d);
}
pub fn timeout(&self) -> Duration {
self.core.timeout()
}
pub async fn get(&self, url: &str) -> Result<()> {
let mut events = self.core.conn.subscribe();
self.core
.send("Page.navigate", json!({ "url": url }))
.await?;
let sid = self.core.session_id.clone();
let deadline = Instant::now() + self.core.timeout();
let _ = timeout_at(deadline, async {
loop {
match events.recv().await {
Ok(ev)
if ev.method == "Page.loadEventFired"
&& ev.session_id.as_deref() == Some(&sid) =>
{
break;
}
Ok(_) => continue,
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
Err(_) => break,
}
}
})
.await;
Ok(())
}
pub async fn run_js(&self, expression: &str) -> Result<Value> {
self.core.eval_value(expression).await
}
pub async fn title(&self) -> Result<String> {
Ok(self
.run_js("document.title")
.await?
.as_str()
.unwrap_or("")
.to_string())
}
pub async fn url(&self) -> Result<String> {
Ok(self
.run_js("location.href")
.await?
.as_str()
.unwrap_or("")
.to_string())
}
pub async fn html(&self) -> Result<String> {
Ok(self
.run_js("document.documentElement.outerHTML")
.await?
.as_str()
.unwrap_or("")
.to_string())
}
pub async fn ele(&self, selector: &str) -> Result<ChromiumElement> {
match self
.core
.eval_handle(&doc_query_expr(selector, true))
.await?
{
Some(oid) => Ok(ChromiumElement::new(self.core.clone(), oid)),
None => Err(Error::ElementNotFound(selector.to_string())),
}
}
pub async fn wait_ele(
&self,
selector: &str,
timeout: Option<Duration>,
) -> Result<ChromiumElement> {
let deadline = Instant::now() + timeout.unwrap_or_else(|| self.core.timeout());
loop {
if let Some(oid) = self
.core
.eval_handle(&doc_query_expr(selector, true))
.await?
{
return Ok(ChromiumElement::new(self.core.clone(), oid));
}
if Instant::now() >= deadline {
return Err(Error::ElementNotFound(selector.to_string()));
}
sleep(Duration::from_millis(100)).await;
}
}
pub async fn eles(&self, selector: &str) -> Result<Vec<ChromiumElement>> {
let Some(arr) = self
.core
.eval_handle(&doc_query_expr(selector, false))
.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 ele_text(&self, selector: &str) -> Result<Option<String>> {
match self.ele(selector).await {
Ok(el) => Ok(Some(el.text().await?)),
Err(Error::ElementNotFound(_)) => Ok(None),
Err(e) => Err(e),
}
}
pub async fn click(&self, selector: &str) -> Result<bool> {
match self.ele(selector).await {
Ok(el) => {
el.click().await?;
Ok(true)
}
Err(Error::ElementNotFound(_)) => Ok(false),
Err(e) => Err(e),
}
}
pub async fn input(&self, selector: &str, text: &str) -> Result<bool> {
match self.ele(selector).await {
Ok(el) => {
el.input(text).await?;
Ok(true)
}
Err(Error::ElementNotFound(_)) => Ok(false),
Err(e) => Err(e),
}
}
pub async fn mouse_move(&self, x: f64, y: f64) -> Result<()> {
self.core
.dispatch_mouse("mouseMoved", x, y, "none", 0, 0)
.await
}
pub async fn mouse_down(&self, x: f64, y: f64) -> Result<()> {
self.core
.dispatch_mouse("mousePressed", x, y, "left", 1, 1)
.await
}
pub async fn mouse_up(&self, x: f64, y: f64) -> Result<()> {
self.core
.dispatch_mouse("mouseReleased", x, y, "left", 0, 1)
.await
}
pub async fn mouse_drag(&self, x: f64, y: f64) -> Result<()> {
self.core
.dispatch_mouse("mouseMoved", x, y, "none", 1, 0)
.await
}
pub async fn press_key(&self, key: &str) -> Result<()> {
self.core.press_key(key).await
}
pub async fn type_keys(&self, parts: &[KeyInput]) -> Result<()> {
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 screenshot_bytes(&self) -> Result<Vec<u8>> {
let r = self
.core
.send("Page.captureScreenshot", json!({ "format": "png" }))
.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 screenshot_full_bytes(&self) -> Result<Vec<u8>> {
let r = self
.core
.send(
"Page.captureScreenshot",
json!({ "format": "png", "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 fn listen(&self) -> CdpListen {
CdpListen::new(self.core.clone())
}
pub fn intercept(&self) -> CdpIntercept {
CdpIntercept::new(self.core.clone())
}
}
fn doc_query_expr(selector: &str, single: bool) -> String {
match locator::parse(selector) {
Query::Css(sel) => {
let s = serde_json::to_string(&sel).unwrap_or_else(|_| "\"\"".into());
if single {
format!("document.querySelector({s})")
} else {
format!("Array.from(document.querySelectorAll({s}))")
}
}
Query::Xpath(xp) => {
let s = serde_json::to_string(&xp).unwrap_or_else(|_| "\"\"".into());
if single {
format!("document.evaluate({s}, document, null, 9, null).singleNodeValue")
} else {
format!(
"(function(){{ const it=document.evaluate({s}, document, null, 7, null); \
const a=[]; for (let i=0;i<it.snapshotLength;i++) a.push(it.snapshotItem(i)); return a; }})()"
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::doc_query_expr;
#[test]
fn css_query_expr() {
assert_eq!(
doc_query_expr("css:h1", true),
"document.querySelector(\"h1\")"
);
assert_eq!(
doc_query_expr("#a .b", false),
"Array.from(document.querySelectorAll(\"#a .b\"))"
);
}
#[test]
fn xpath_query_expr() {
let s = doc_query_expr("xpath://div[@id=\"x\"]", true);
assert!(s.starts_with("document.evaluate("));
assert!(s.contains("singleNodeValue"));
}
}