use serde::de::DeserializeOwned;
use serde_json::{Value, json};
use crate::element::Element;
use crate::error::{Result, ZendriverError};
impl Element {
pub async fn evaluate<T: DeserializeOwned>(&self, js: impl AsRef<str>) -> Result<T> {
let js = js.as_ref();
self.with_refresh(|| async move {
let ctx_id = self.inner.tab.ensure_isolated_world().await?;
let backend_node_id = self.backend_node_id_cloned().await?;
let resolved = self
.inner
.tab
.call(
"DOM.resolveNode",
json!({
"backendNodeId": backend_node_id,
"executionContextId": ctx_id,
}),
)
.await?;
let isolated_object_id = resolved["object"]["objectId"]
.as_str()
.ok_or_else(|| {
ZendriverError::Navigation(
"DOM.resolveNode returned no objectId for isolated world".into(),
)
})?
.to_string();
let function = format!("function(el){{ return ({js}) }}");
let result = self
.inner
.tab
.call(
"Runtime.callFunctionOn",
json!({
"objectId": isolated_object_id,
"functionDeclaration": function,
"arguments": [{ "objectId": isolated_object_id }],
"returnByValue": true,
"awaitPromise": true,
}),
)
.await?;
if let Some(details) = result.get("exceptionDetails") {
let msg = details
.get("exception")
.and_then(|e| e.get("description"))
.and_then(|d| d.as_str())
.unwrap_or("unknown")
.to_string();
let _ = self
.inner
.tab
.call(
"Runtime.releaseObject",
json!({ "objectId": isolated_object_id }),
)
.await;
return Err(ZendriverError::JsException(msg));
}
let value = result
.get("result")
.and_then(|r| r.get("value"))
.cloned()
.unwrap_or(Value::Null);
let _ = self
.inner
.tab
.call(
"Runtime.releaseObject",
json!({ "objectId": isolated_object_id }),
)
.await;
serde_json::from_value(value).map_err(ZendriverError::Serde)
})
.await
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
use crate::tab::Tab;
use zendriver_transport::SessionHandle;
use zendriver_transport::testing::MockConnection;
#[tokio::test]
async fn evaluate_dispatches_full_isolated_world_sequence() {
let (mut mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let tab = Tab::new_for_test(sess);
let el = Element::from_jsret(tab, 314, "R_MAIN".to_string());
let fut = tokio::spawn({
let e = el.clone();
async move { e.evaluate::<String>("el.tagName").await }
});
let id_tree = mock.expect_cmd("Page.getFrameTree").await;
mock.reply(
id_tree,
json!({ "frameTree": { "frame": { "id": "FRAME_1" } } }),
)
.await;
let id_world = mock.expect_cmd("Page.createIsolatedWorld").await;
let sent = mock.last_sent();
assert_eq!(sent["params"]["frameId"], "FRAME_1");
assert_eq!(sent["params"]["worldName"], "zendriver-eval");
mock.reply(id_world, json!({ "executionContextId": 42 }))
.await;
let id_resolve = mock.expect_cmd("DOM.resolveNode").await;
let sent = mock.last_sent();
assert_eq!(sent["params"]["backendNodeId"], 314);
assert_eq!(sent["params"]["executionContextId"], 42);
mock.reply(
id_resolve,
json!({ "object": { "objectId": "R_ISO", "type": "object", "subtype": "node" } }),
)
.await;
let id_call = mock.expect_cmd("Runtime.callFunctionOn").await;
let sent = mock.last_sent();
assert_eq!(
sent["params"]["objectId"], "R_ISO",
"callFunctionOn must target the isolated-world handle, not the main-world one",
);
assert_eq!(
sent["params"]["arguments"][0]["objectId"], "R_ISO",
"the bound `el` argument must be the isolated handle",
);
let decl = sent["params"]["functionDeclaration"].as_str().unwrap();
assert!(
decl.contains("function(el)") && decl.contains("el.tagName"),
"function declaration must wrap user JS; got: {decl}",
);
assert_eq!(sent["params"]["returnByValue"], true);
assert_eq!(sent["params"]["awaitPromise"], true);
mock.reply(
id_call,
json!({ "result": { "type": "string", "value": "DIV" } }),
)
.await;
let id_release = mock.expect_cmd("Runtime.releaseObject").await;
assert_eq!(mock.last_sent()["params"]["objectId"], "R_ISO");
mock.reply(id_release, json!({})).await;
let value = fut.await.unwrap().unwrap();
assert_eq!(value, "DIV");
conn.shutdown();
}
}