use std::collections::HashMap;
use serde_json::{Value, json};
use crate::element::Element;
use crate::error::{Result, ZendriverError};
use crate::query::actionability;
use crate::query::{BoundingBox, PageBox};
impl Element {
pub async fn attr(&self, name: impl AsRef<str>) -> Result<Option<String>> {
let name = name.as_ref().to_string();
self.with_refresh(|| {
let name = name.clone();
async move {
let res = self
.call_on(
"function(n){ return this.getAttribute(n); }",
json!([{ "value": name }]),
)
.await?;
match res.get("value") {
Some(Value::String(s)) => Ok(Some(s.clone())),
_ => Ok(None),
}
}
})
.await
}
pub async fn attrs(&self) -> Result<HashMap<String, String>> {
self.with_refresh(|| async move {
let js = r"
function() {
const out = {};
for (const a of this.attributes) { out[a.name] = a.value; }
return out;
}
";
let res = self.call_on(js, json!([])).await?;
let value = res.get("value").cloned().unwrap_or(Value::Null);
let obj = value.as_object().cloned().unwrap_or_default();
let mut out = HashMap::with_capacity(obj.len());
for (k, v) in obj {
if let Some(s) = v.as_str() {
out.insert(k, s.to_string());
}
}
Ok(out)
})
.await
}
pub async fn inner_text(&self) -> Result<String> {
self.with_refresh(|| async move {
let res = self
.call_on("function(){ return this.innerText; }", json!([]))
.await?;
Ok(res["value"].as_str().unwrap_or("").to_string())
})
.await
}
pub async fn inner_html(&self) -> Result<String> {
self.with_refresh(|| async move {
let res = self
.call_on("function(){ return this.innerHTML; }", json!([]))
.await?;
Ok(res["value"].as_str().unwrap_or("").to_string())
})
.await
}
pub async fn outer_html(&self) -> Result<String> {
self.with_refresh(|| async move {
let res = self
.call_on("function(){ return this.outerHTML; }", json!([]))
.await?;
Ok(res["value"].as_str().unwrap_or("").to_string())
})
.await
}
pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
self.with_refresh(|| async move {
let backend_node_id = self.backend_node_id_cloned().await?;
let res = self
.inner
.tab
.call(
"DOM.getBoxModel",
json!({ "backendNodeId": backend_node_id }),
)
.await;
let res = match res {
Ok(v) => v,
Err(ZendriverError::Cdp { ref message, .. })
if message.contains("Could not compute box model") =>
{
return Ok(None);
}
Err(e) => return Err(e),
};
let model = match res.get("model") {
Some(m) => m,
None => return Ok(None),
};
let content = match model.get("content").and_then(|v| v.as_array()) {
Some(c) if c.len() >= 2 => c,
_ => return Ok(None),
};
let x = content[0].as_f64().unwrap_or(0.0);
let y = content[1].as_f64().unwrap_or(0.0);
let width = model.get("width").and_then(|v| v.as_f64()).unwrap_or(0.0);
let height = model.get("height").and_then(|v| v.as_f64()).unwrap_or(0.0);
Ok(Some(BoundingBox {
x,
y,
width,
height,
}))
})
.await
}
pub async fn bounding_box_page(&self) -> Result<Option<PageBox>> {
self.with_refresh(|| async move {
let Some(viewport) = self.bounding_box().await? else {
return Ok(None);
};
let res = self
.call_on_main(
"function(){ return { x: window.scrollX, y: window.scrollY }; }",
json!([]),
)
.await?;
let scroll = res.get("value").cloned().unwrap_or(Value::Null);
let scroll_x = scroll.get("x").and_then(Value::as_f64).unwrap_or(0.0);
let scroll_y = scroll.get("y").and_then(Value::as_f64).unwrap_or(0.0);
Ok(Some(PageBox {
viewport,
scroll_x,
scroll_y,
}))
})
.await
}
pub async fn is_visible(&self) -> Result<bool> {
self.with_refresh(|| async move { actionability::check_visible(self).await })
.await
}
pub async fn is_enabled(&self) -> Result<bool> {
self.with_refresh(|| async move { actionability::check_enabled(self).await })
.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 attr_returns_some_when_attribute_present() {
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, 1, "R1".to_string());
let fut = tokio::spawn({
let e = el.clone();
async move { e.attr("href").await }
});
let id = mock.expect_cmd("Runtime.callFunctionOn").await;
let sent = mock.last_sent();
assert_eq!(sent["params"]["objectId"], "R1");
assert!(
sent["params"]["functionDeclaration"]
.as_str()
.unwrap()
.contains("getAttribute")
);
assert_eq!(sent["params"]["arguments"][0]["value"], "href");
mock.reply(
id,
json!({ "result": { "value": "/login", "type": "string" } }),
)
.await;
let got = fut.await.unwrap().unwrap();
assert_eq!(got, Some("/login".to_string()));
conn.shutdown();
}
#[tokio::test]
async fn attrs_returns_hashmap_of_all_attributes() {
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, 1, "R1".to_string());
let fut = tokio::spawn({
let e = el.clone();
async move { e.attrs().await }
});
let id = mock.expect_cmd("Runtime.callFunctionOn").await;
let sent = mock.last_sent();
assert!(
sent["params"]["functionDeclaration"]
.as_str()
.unwrap()
.contains("attributes")
);
mock.reply(
id,
json!({
"result": {
"value": { "id": "btn", "class": "primary", "data-x": "42" },
"type": "object"
}
}),
)
.await;
let map = fut.await.unwrap().unwrap();
assert_eq!(map.get("id").map(String::as_str), Some("btn"));
assert_eq!(map.get("class").map(String::as_str), Some("primary"));
assert_eq!(map.get("data-x").map(String::as_str), Some("42"));
assert_eq!(map.len(), 3);
conn.shutdown();
}
#[tokio::test]
async fn bounding_box_parses_dom_get_box_model_content_quad() {
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, 42, "R1".to_string());
let fut = tokio::spawn({
let e = el.clone();
async move { e.bounding_box().await }
});
let id = mock.expect_cmd("DOM.getBoxModel").await;
let sent = mock.last_sent();
assert_eq!(sent["params"]["backendNodeId"], 42);
mock.reply(
id,
json!({
"model": {
"content": [10.0, 20.0, 110.0, 20.0, 110.0, 70.0, 10.0, 70.0],
"padding": [10.0, 20.0, 110.0, 20.0, 110.0, 70.0, 10.0, 70.0],
"border": [10.0, 20.0, 110.0, 20.0, 110.0, 70.0, 10.0, 70.0],
"margin": [10.0, 20.0, 110.0, 20.0, 110.0, 70.0, 10.0, 70.0],
"width": 100,
"height": 50
}
}),
)
.await;
let bbox = fut.await.unwrap().unwrap().expect("should be Some");
assert!((bbox.x - 10.0).abs() < 1e-9);
assert!((bbox.y - 20.0).abs() < 1e-9);
assert!((bbox.width - 100.0).abs() < 1e-9);
assert!((bbox.height - 50.0).abs() < 1e-9);
conn.shutdown();
}
#[tokio::test]
async fn bounding_box_page_adds_scroll_offsets_to_viewport_box() {
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, 42, "R1".to_string());
let fut = tokio::spawn({
let e = el.clone();
async move { e.bounding_box_page().await }
});
let id = mock.expect_cmd("DOM.getBoxModel").await;
assert_eq!(mock.last_sent()["params"]["backendNodeId"], 42);
mock.reply(
id,
json!({
"model": {
"content": [10.0, 20.0, 110.0, 20.0, 110.0, 70.0, 10.0, 70.0],
"padding": [10.0, 20.0, 110.0, 20.0, 110.0, 70.0, 10.0, 70.0],
"border": [10.0, 20.0, 110.0, 20.0, 110.0, 70.0, 10.0, 70.0],
"margin": [10.0, 20.0, 110.0, 20.0, 110.0, 70.0, 10.0, 70.0],
"width": 100,
"height": 50
}
}),
)
.await;
let id = mock.expect_cmd("Runtime.callFunctionOn").await;
assert!(
mock.last_sent()["params"]["functionDeclaration"]
.as_str()
.unwrap()
.contains("scrollX")
);
mock.reply(
id,
json!({ "result": { "value": { "x": 1000.0, "y": 500.0 }, "type": "object" } }),
)
.await;
let page = fut.await.unwrap().unwrap().expect("should be Some");
assert!((page.viewport.x - 10.0).abs() < 1e-9);
assert!((page.viewport.y - 20.0).abs() < 1e-9);
assert!((page.scroll_x - 1000.0).abs() < 1e-9);
assert!((page.scroll_y - 500.0).abs() < 1e-9);
let (ax, ay) = page.abs_origin();
assert!((ax - 1010.0).abs() < 1e-9);
assert!((ay - 520.0).abs() < 1e-9);
let (cx, cy) = page.abs_center();
assert!((cx - 1060.0).abs() < 1e-9); assert!((cy - 545.0).abs() < 1e-9); conn.shutdown();
}
}