pub mod actions;
pub mod input;
pub mod isolated_eval;
pub mod reads;
pub mod refresh;
pub mod screenshot;
pub mod traversal;
use std::sync::Arc;
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
use tokio::sync::Mutex;
use crate::error::{Result, ZendriverError};
use crate::query::selectors::{QueryScope, RemoteRef, SelectorKind};
use crate::tab::Tab;
#[derive(Clone, Debug)]
pub struct Element {
pub(crate) inner: Arc<ElementInner>,
}
#[derive(Debug)]
pub(crate) struct ElementInner {
pub(crate) tab: Tab,
pub(crate) backend_node_id: Mutex<Option<i64>>,
pub(crate) remote_object_id: Mutex<Option<String>>,
#[allow(dead_code)] pub(crate) origin: ElementOrigin,
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub(crate) enum ElementOrigin {
Query {
scope_kind: ScopeKind,
selector: SelectorKind,
nth: usize,
},
Traversal {
parent: Box<ElementOrigin>,
kind: TraversalKind,
},
Evaluation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] pub(crate) enum ScopeKind {
TabMain,
ElementSubtree,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] pub(crate) enum TraversalKind {
Parent,
NthChild(usize),
}
impl Element {
pub(crate) fn synthesize_query(
r: RemoteRef,
scope: &QueryScope<'_>,
selector: &SelectorKind,
nth: usize,
) -> Self {
let scope_kind = match scope {
QueryScope::Tab(_) | QueryScope::Frame(_) => ScopeKind::TabMain,
QueryScope::Element(_) => ScopeKind::ElementSubtree,
};
Self {
inner: Arc::new(ElementInner {
tab: scope.synthesize_tab(),
backend_node_id: Mutex::new(Some(r.backend_node_id)),
remote_object_id: Mutex::new(Some(r.remote_object_id)),
origin: ElementOrigin::Query {
scope_kind,
selector: selector.clone(),
nth,
},
}),
}
}
#[allow(dead_code)] pub(crate) fn from_jsret(tab: Tab, backend_node_id: i64, remote_object_id: String) -> Self {
Self {
inner: Arc::new(ElementInner {
tab,
backend_node_id: Mutex::new(Some(backend_node_id)),
remote_object_id: Mutex::new(Some(remote_object_id)),
origin: ElementOrigin::Evaluation,
}),
}
}
pub(crate) fn synthesize_traversal(
tab: Tab,
backend_node_id: i64,
remote_object_id: String,
parent_origin: ElementOrigin,
kind: TraversalKind,
) -> Self {
Self {
inner: Arc::new(ElementInner {
tab,
backend_node_id: Mutex::new(Some(backend_node_id)),
remote_object_id: Mutex::new(Some(remote_object_id)),
origin: ElementOrigin::Traversal {
parent: Box::new(parent_origin),
kind,
},
}),
}
}
#[must_use]
pub fn tab(&self) -> &Tab {
&self.inner.tab
}
pub(crate) async fn remote_object_id_cloned(&self) -> Result<String> {
self.inner
.remote_object_id
.lock()
.await
.clone()
.ok_or(ZendriverError::ElementStale)
}
pub(crate) async fn backend_node_id_cloned(&self) -> Result<i64> {
self.inner
.backend_node_id
.lock()
.await
.as_ref()
.copied()
.ok_or(ZendriverError::ElementStale)
}
pub(crate) async fn call_on(&self, function: &str, args: Value) -> Result<Value> {
let object_id = self.remote_object_id_cloned().await?;
let res = self
.inner
.tab
.call(
"Runtime.callFunctionOn",
json!({
"objectId": object_id,
"functionDeclaration": function,
"arguments": args,
"returnByValue": true,
"awaitPromise": true,
}),
)
.await?;
if let Some(details) = res.get("exceptionDetails") {
let msg = details
.get("exception")
.and_then(|e| e.get("description"))
.and_then(|d| d.as_str())
.unwrap_or("unknown")
.to_string();
return Err(ZendriverError::JsException(msg));
}
Ok(res["result"].clone())
}
#[allow(dead_code)] pub(crate) async fn call_on_main(&self, function: &str, args: Value) -> Result<Value> {
let object_id = self.remote_object_id_cloned().await?;
let mut full_args = vec![json!({ "objectId": object_id })];
if let Some(extra) = args.as_array() {
full_args.extend(extra.iter().cloned());
}
self.call_on(function, Value::Array(full_args)).await
}
pub async fn evaluate_main<T: DeserializeOwned>(&self, js: impl AsRef<str>) -> Result<T> {
let function = format!("function(el){{ return ({}) }}", js.as_ref());
let result = self.call_on_main(&function, json!([])).await?;
let value = result.get("value").cloned().unwrap_or(Value::Null);
serde_json::from_value(value).map_err(ZendriverError::Serde)
}
}
impl crate::traits::Queryable for Element {
fn find(&self) -> crate::query::FindBuilder<'_> {
Element::find(self)
}
fn find_all(&self) -> crate::query::FindAllBuilder<'_> {
Element::find_all(self)
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
use zendriver_transport::SessionHandle;
use zendriver_transport::testing::MockConnection;
#[tokio::test]
async fn from_jsret_yields_evaluation_origin() {
let (_mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let tab = Tab::new_for_test(sess);
let el = Element::from_jsret(tab, 7, "R7".to_string());
assert!(matches!(el.inner.origin, ElementOrigin::Evaluation));
conn.shutdown();
}
#[tokio::test]
async fn remote_object_id_cloned_errors_after_clear() {
let (_mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let tab = Tab::new_for_test(sess);
let el = Element::from_jsret(tab, 1, "R1".to_string());
assert_eq!(el.remote_object_id_cloned().await.unwrap(), "R1");
*el.inner.remote_object_id.lock().await = None;
let err = el.remote_object_id_cloned().await.unwrap_err();
assert!(matches!(err, ZendriverError::ElementStale));
conn.shutdown();
}
}