use std::future::Future;
use std::pin::Pin;
use serde_json::{Value, json};
use crate::element::{Element, ElementOrigin, ScopeKind, TraversalKind};
use crate::error::{Result, ZendriverError};
use crate::query::selectors::{QueryScope, RemoteRef, extract_node_ref};
use crate::tab::Tab;
impl Element {
pub async fn refresh(&self) -> Result<()> {
let r = resolve_origin(&self.inner.origin, &self.inner.tab).await?;
*self.inner.backend_node_id.lock().await = Some(r.backend_node_id);
*self.inner.remote_object_id.lock().await = Some(r.remote_object_id);
Ok(())
}
pub(crate) async fn with_refresh<T, F, Fut>(&self, op: F) -> Result<T>
where
F: Fn() -> Fut + Send,
Fut: Future<Output = Result<T>> + Send,
{
match op().await {
Ok(v) => Ok(v),
Err(e) if is_stale_node_error(&e) => {
self.refresh().await?;
op().await
}
Err(e) => Err(e),
}
}
}
fn resolve_origin<'a>(
origin: &'a ElementOrigin,
tab: &'a Tab,
) -> Pin<Box<dyn Future<Output = Result<RemoteRef>> + Send + 'a>> {
Box::pin(async move {
match origin {
ElementOrigin::Query {
scope_kind: ScopeKind::TabMain,
selector,
nth,
} => {
let scope = QueryScope::Tab(tab);
let candidates = selector.resolve_many(&scope).await?;
candidates
.into_iter()
.nth(*nth)
.ok_or_else(|| ZendriverError::ElementNotFound {
selector: format!("{selector:?}"),
})
}
ElementOrigin::Query {
scope_kind: ScopeKind::ElementSubtree,
..
}
| ElementOrigin::Evaluation => Err(ZendriverError::NotRefreshable),
ElementOrigin::Traversal { parent, kind } => {
let parent_ref = resolve_origin(parent, tab).await?;
let parent_el = Element::from_jsret(
tab.clone(),
parent_ref.backend_node_id,
parent_ref.remote_object_id,
);
let (function_decl, missing_selector) = match kind {
TraversalKind::Parent => (
"function(){ return this.parentElement; }".to_string(),
"parent".to_string(),
),
TraversalKind::NthChild(idx) => (
format!("function(){{ return this.children[{idx}]; }}"),
format!("nth_child({idx})"),
),
};
let object_id = parent_el.remote_object_id_cloned().await?;
let raw = tab
.call(
"Runtime.callFunctionOn",
json!({
"objectId": object_id,
"functionDeclaration": function_decl,
"arguments": [],
"returnByValue": false,
"awaitPromise": true,
}),
)
.await?;
extract_remote_ref(&raw["result"], tab).await?.ok_or(
ZendriverError::ElementNotFound {
selector: missing_selector,
},
)
}
}
})
}
async fn extract_remote_ref(value: &Value, tab: &Tab) -> Result<Option<RemoteRef>> {
extract_node_ref(tab.session(), value).await
}
pub(crate) fn is_stale_node_error(e: &ZendriverError) -> bool {
match e {
ZendriverError::ElementStale => true,
ZendriverError::Navigation(m) => {
m.contains("No node with given id") || m.contains("Cannot find context")
}
ZendriverError::Cdp { message, .. } => {
message.contains("No node with given id") || message.contains("Cannot find context")
}
_ => false,
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use zendriver_transport::SessionHandle;
use zendriver_transport::testing::MockConnection;
use crate::element::ElementInner;
use crate::tab::Tab;
fn make_traversal_element(
tab: Tab,
backend: i64,
remote: &str,
parent_origin: ElementOrigin,
kind: TraversalKind,
) -> Element {
Element {
inner: Arc::new(ElementInner {
tab,
backend_node_id: Mutex::new(Some(backend)),
remote_object_id: Mutex::new(Some(remote.to_string())),
origin: ElementOrigin::Traversal {
parent: Box::new(parent_origin),
kind,
},
}),
}
}
#[tokio::test]
async fn traversal_parent_refresh_reresolves_parent_then_retraverses() {
use crate::element::ScopeKind;
use crate::query::selectors::SelectorKind;
let (mut mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let tab = Tab::new_for_test(sess);
let parent_origin = ElementOrigin::Query {
scope_kind: ScopeKind::TabMain,
selector: SelectorKind::Css("#root".into()),
nth: 0,
};
let el = make_traversal_element(
tab.clone(),
10, "R_STALE",
parent_origin,
TraversalKind::Parent,
);
let fut = tokio::spawn({
let e = el.clone();
async move { e.refresh().await }
});
let id_eval = mock.expect_cmd("Runtime.evaluate").await;
let sent = mock.last_sent()["params"]["expression"]
.as_str()
.unwrap()
.to_string();
assert!(
sent.contains("document.querySelectorAll") && sent.contains("#root"),
"parent re-resolve must call querySelectorAll('#root'); got: {sent}"
);
mock.reply(
id_eval,
json!({ "result": { "objectId": "RArr", "type": "object", "subtype": "array" } }),
)
.await;
let id_props = mock.expect_cmd("Runtime.getProperties").await;
assert_eq!(mock.last_sent()["params"]["objectId"], "RArr");
mock.reply(
id_props,
json!({
"result": [
{ "name": "0", "value": { "objectId": "R_PARENT", "type": "object", "subtype": "node" } },
{ "name": "length", "value": { "value": 1, "type": "number" } }
]
}),
)
.await;
let id_d1 = mock.expect_cmd("DOM.describeNode").await;
assert_eq!(mock.last_sent()["params"]["objectId"], "R_PARENT");
mock.reply(id_d1, json!({ "node": { "backendNodeId": 200 } }))
.await;
let id_call = mock.expect_cmd("Runtime.callFunctionOn").await;
let sent = mock.last_sent();
assert_eq!(
sent["params"]["objectId"], "R_PARENT",
"re-traversal must dispatch against the fresh parent objectId"
);
let decl = sent["params"]["functionDeclaration"].as_str().unwrap();
assert!(
decl.contains("this.parentElement"),
"Parent traversal must call `this.parentElement`; got: {decl}"
);
assert_eq!(sent["params"]["returnByValue"], false);
mock.reply(
id_call,
json!({ "result": { "objectId": "R_FRESH", "type": "object", "subtype": "node" } }),
)
.await;
let id_d2 = mock.expect_cmd("DOM.describeNode").await;
assert_eq!(mock.last_sent()["params"]["objectId"], "R_FRESH");
mock.reply(id_d2, json!({ "node": { "backendNodeId": 314 } }))
.await;
fut.await.unwrap().unwrap();
assert_eq!(el.remote_object_id_cloned().await.unwrap(), "R_FRESH");
assert_eq!(el.backend_node_id_cloned().await.unwrap(), 314);
conn.shutdown();
}
#[tokio::test]
async fn traversal_chain_two_level_refresh_recurses() {
use crate::element::ScopeKind;
use crate::query::selectors::SelectorKind;
let (mut mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let tab = Tab::new_for_test(sess);
let grandparent_origin = ElementOrigin::Query {
scope_kind: ScopeKind::TabMain,
selector: SelectorKind::Css(".list".into()),
nth: 0,
};
let parent_origin = ElementOrigin::Traversal {
parent: Box::new(grandparent_origin),
kind: TraversalKind::NthChild(2),
};
let el = make_traversal_element(
tab.clone(),
5,
"R_STALE",
parent_origin,
TraversalKind::Parent,
);
let fut = tokio::spawn({
let e = el.clone();
async move { e.refresh().await }
});
let id_eval = mock.expect_cmd("Runtime.evaluate").await;
assert!(
mock.last_sent()["params"]["expression"]
.as_str()
.unwrap()
.contains(".list")
);
mock.reply(
id_eval,
json!({ "result": { "objectId": "GP_ARR", "type": "object", "subtype": "array" } }),
)
.await;
let id_props = mock.expect_cmd("Runtime.getProperties").await;
mock.reply(
id_props,
json!({
"result": [
{ "name": "0", "value": { "objectId": "GP", "type": "object", "subtype": "node" } },
{ "name": "length", "value": { "value": 1, "type": "number" } }
]
}),
)
.await;
let id_d = mock.expect_cmd("DOM.describeNode").await;
assert_eq!(mock.last_sent()["params"]["objectId"], "GP");
mock.reply(id_d, json!({ "node": { "backendNodeId": 100 } }))
.await;
let id_call = mock.expect_cmd("Runtime.callFunctionOn").await;
let sent = mock.last_sent();
assert_eq!(sent["params"]["objectId"], "GP");
let decl = sent["params"]["functionDeclaration"].as_str().unwrap();
assert!(
decl.contains("this.children[2]"),
"NthChild(2) must call `this.children[2]`; got: {decl}"
);
mock.reply(
id_call,
json!({ "result": { "objectId": "P", "type": "object", "subtype": "node" } }),
)
.await;
let id_d = mock.expect_cmd("DOM.describeNode").await;
assert_eq!(mock.last_sent()["params"]["objectId"], "P");
mock.reply(id_d, json!({ "node": { "backendNodeId": 200 } }))
.await;
let id_call = mock.expect_cmd("Runtime.callFunctionOn").await;
let sent = mock.last_sent();
assert_eq!(sent["params"]["objectId"], "P");
assert!(
sent["params"]["functionDeclaration"]
.as_str()
.unwrap()
.contains("this.parentElement")
);
mock.reply(
id_call,
json!({ "result": { "objectId": "SELF", "type": "object", "subtype": "node" } }),
)
.await;
let id_d = mock.expect_cmd("DOM.describeNode").await;
assert_eq!(mock.last_sent()["params"]["objectId"], "SELF");
mock.reply(id_d, json!({ "node": { "backendNodeId": 300 } }))
.await;
fut.await.unwrap().unwrap();
assert_eq!(el.remote_object_id_cloned().await.unwrap(), "SELF");
assert_eq!(el.backend_node_id_cloned().await.unwrap(), 300);
conn.shutdown();
}
#[test]
fn is_stale_node_error_matches_expected_shapes() {
assert!(is_stale_node_error(&ZendriverError::ElementStale));
assert!(is_stale_node_error(&ZendriverError::Navigation(
"Cannot find context with specified id".into(),
)));
assert!(is_stale_node_error(&ZendriverError::Cdp {
code: -32000,
message: "No node with given id".into(),
data: None,
}));
assert!(!is_stale_node_error(&ZendriverError::Timeout(
Duration::from_secs(1)
)));
}
}