use crate::cdp::CdpError;
use crate::dom::{
get_backend_node_id, get_node_id_from_backend, get_outer_html, get_iframe_content_document_node_id,
query_selector, resolve_backend_to_object_id, resolve_node_to_object_id,
};
use crate::frame::Frame;
use serde_json::{json, Value};
use std::cell::RefCell;
use std::sync::Arc;
use crate::cdp::CdpClient;
use std::fmt;
pub struct Element {
pub(crate) client: Arc<CdpClient>,
pub(crate) session_id: String,
pub(crate) node_id: RefCell<i64>,
pub(crate) initial_object_id: RefCell<Option<String>>,
pub(crate) backend_node_id: Option<i64>,
}
impl fmt::Debug for Element {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Element")
.field("node_id", &*self.node_id.borrow())
.finish_non_exhaustive()
}
}
fn is_no_node_error(e: &CdpError) -> bool {
match e {
CdpError::Protocol { message, .. } => {
message.contains("No node with given id")
|| message.contains("Could not find node with given id")
}
_ => false,
}
}
fn describe_node_by_object_id(
client: &Arc<CdpClient>,
session_id: &str,
object_id: &str,
) -> Result<(Option<i64>, Option<i64>), CdpError> {
let params = json!({ "objectId": object_id });
let res = client.send_with_session("DOM.describeNode", Some(params), Some(session_id))?;
let node = res.get("node");
let node_id = node.and_then(|n| n.get("nodeId")).and_then(Value::as_i64);
let backend_node_id = node.and_then(|n| n.get("backendNodeId")).and_then(Value::as_i64);
Ok((node_id, backend_node_id))
}
fn is_object_invalid_error(e: &CdpError) -> bool {
match e {
CdpError::Protocol { message, .. } => {
message.contains("No node with given id")
|| message.contains("Could not find object")
|| message.contains("Object has been collected")
|| message.contains("given id")
}
_ => false,
}
}
impl Element {
pub(crate) fn new(client: Arc<CdpClient>, session_id: String, node_id: i64) -> Self {
let backend_node_id = get_backend_node_id(&client, &session_id, node_id).ok();
Self {
client,
session_id,
node_id: RefCell::new(node_id),
initial_object_id: RefCell::new(None),
backend_node_id,
}
}
pub(crate) fn new_with_backend(
client: Arc<CdpClient>,
session_id: String,
node_id: i64,
backend_node_id: Option<i64>,
) -> Self {
Self {
client,
session_id,
node_id: RefCell::new(node_id),
initial_object_id: RefCell::new(None),
backend_node_id,
}
}
pub(crate) fn new_with_object_id(
client: Arc<CdpClient>,
session_id: String,
node_id: i64,
object_id: Option<String>,
backend_node_id: Option<i64>,
) -> Self {
Self {
client,
session_id,
node_id: RefCell::new(node_id),
initial_object_id: RefCell::new(object_id),
backend_node_id,
}
}
fn with_valid_node_id<T, F1, F2>(&self, first: F1, retry: F2) -> Result<T, CdpError>
where
F1: FnOnce() -> Result<T, CdpError>,
F2: FnOnce(i64) -> Result<T, CdpError>,
{
match first() {
Ok(t) => Ok(t),
Err(e) => {
if is_no_node_error(&e) {
if let Some(backend_id) = self.backend_node_id {
let fresh = get_node_id_from_backend(
&self.client,
&self.session_id,
backend_id,
)?;
self.node_id.replace(fresh);
return retry(fresh);
}
}
Err(e)
}
}
}
fn object_id(&self) -> Result<String, CdpError> {
if let Some(oid) = self.initial_object_id.borrow().as_ref() {
return Ok(oid.clone());
}
match resolve_node_to_object_id(&self.client, &self.session_id, *self.node_id.borrow()) {
Ok(oid) => Ok(oid),
Err(e) => {
if is_no_node_error(&e) {
if let Some(bid) = self.backend_node_id {
if let Ok(oid) =
resolve_backend_to_object_id(&self.client, &self.session_id, bid)
{
return Ok(oid);
}
if let Ok(fresh_nid) =
get_node_id_from_backend(&self.client, &self.session_id, bid)
{
self.node_id.replace(fresh_nid);
return resolve_node_to_object_id(
&self.client,
&self.session_id,
fresh_nid,
);
}
}
}
Err(e)
}
}
}
pub(crate) fn call_on(&self, fn_decl: &str, args: Option<Value>) -> Result<Value, CdpError> {
self.call_on_inner(fn_decl, args, false)
}
pub(crate) fn call_on_value(&self, fn_decl: &str, args: Option<Value>) -> Result<Value, CdpError> {
self.call_on_inner(fn_decl, args, true)
}
fn call_on_inner(&self, fn_decl: &str, args: Option<Value>, return_by_value: bool) -> Result<Value, CdpError> {
let object_id = self.object_id()?;
let mut params = json!({
"functionDeclaration": fn_decl,
"objectId": object_id,
"returnByValue": return_by_value
});
if let Some(ref a) = args {
params["arguments"] = a.clone();
}
let result = self.client.send_with_session(
"Runtime.callFunctionOn",
Some(params),
Some(self.session_id.as_str()),
);
match result {
Ok(r) => Ok(r.get("result").cloned().unwrap_or(Value::Null)),
Err(e) => {
if is_object_invalid_error(&e) {
if self.initial_object_id.borrow().is_some() {
self.initial_object_id.replace(None);
return self.call_on_inner(fn_decl, args.clone(), return_by_value);
}
if let Some(bid) = self.backend_node_id {
if let Ok(fresh_nid) =
get_node_id_from_backend(&self.client, &self.session_id, bid)
{
self.node_id.replace(fresh_nid);
return self.call_on_inner(fn_decl, args.clone(), return_by_value);
}
}
}
Err(e)
}
}
}
pub fn click(&self) -> Result<(), CdpError> {
self.call_on("function(){ this.click(); }", None)?;
Ok(())
}
pub fn tag(&self) -> Result<String, CdpError> {
let result = self.call_on("function(){ return (this.localName || this.tagName || '').toLowerCase(); }", None)?;
Ok(result
.get("value")
.and_then(Value::as_str)
.unwrap_or("")
.to_string())
}
pub fn is_frame(&self) -> Result<bool, CdpError> {
let t = self.tag()?;
Ok(t == "iframe" || t == "frame")
}
pub fn into_frame(self) -> Result<Option<Frame>, CdpError> {
if !self.is_frame()? {
return Ok(None);
}
let content_document_node_id = self.with_valid_node_id(
|| {
get_iframe_content_document_node_id(
&self.client,
&self.session_id,
*self.node_id.borrow(),
)
},
|nid| get_iframe_content_document_node_id(&self.client, &self.session_id, nid),
)?;
let Some(content_document_node_id) = content_document_node_id else {
return Ok(None); };
Ok(Some(Frame::new(
self.client,
self.session_id,
*self.node_id.borrow(),
content_document_node_id,
)))
}
pub fn inner_html(&self) -> Result<String, CdpError> {
let result = self.call_on("function(){ return this.innerHTML || ''; }", None)?;
Ok(result
.get("value")
.and_then(Value::as_str)
.unwrap_or("")
.to_string())
}
pub fn attrs(&self) -> Result<std::collections::HashMap<String, String>, CdpError> {
let result = self.call_on(
"function(){ var m=this.attributes; var o={}; for(var i=0;i<m.length;i++){ o[m[i].name]=m[i].value; } return o; }",
None,
)?;
let obj = result.get("value").and_then(Value::as_object).ok_or_else(|| CdpError::Protocol {
id: None,
code: -1,
message: "The attrs result was not an object".into(),
})?;
let mut map = std::collections::HashMap::new();
for (k, v) in obj {
if let Some(s) = v.as_str() {
map.insert(k.clone(), s.to_string());
}
}
Ok(map)
}
pub fn text(&self) -> Result<String, CdpError> {
let result = self.call_on("function(){ return this.innerText || ''; }", None)?;
Ok(result
.get("value")
.and_then(Value::as_str)
.unwrap_or("")
.to_string())
}
pub fn text_content(&self) -> Result<String, CdpError> {
let result = self.call_on("function(){ return this.textContent || ''; }", None)?;
Ok(result
.get("value")
.and_then(Value::as_str)
.unwrap_or("")
.to_string())
}
pub fn html(&self) -> Result<String, CdpError> {
self.with_valid_node_id(
|| get_outer_html(&self.client, &self.session_id, *self.node_id.borrow()),
|nid| get_outer_html(&self.client, &self.session_id, nid),
)
}
pub fn property(&self, name: &str) -> Result<Value, CdpError> {
let name_json = serde_json::to_string(name).map_err(CdpError::Json)?;
let result = self.call_on(
&format!("function(){{ return this[{}]; }}", name_json),
None,
)?;
Ok(result.get("value").cloned().unwrap_or(Value::Null))
}
pub fn run_js(&self, script: &str) -> Result<Value, CdpError> {
self.call_on(
"function(scriptBody){ try { return (new Function(scriptBody)).call(this); } catch(e) { throw e; } }",
Some(json!([{ "value": script }])),
)
}
pub fn attr(&self, name: &str) -> Result<String, CdpError> {
let name_json = serde_json::to_string(name).map_err(CdpError::Json)?;
let result = self.call_on(
&format!("function(){{ var v = this.getAttribute({}); return v !== null ? v : ''; }}", name_json),
None,
)?;
Ok(result
.get("value")
.and_then(Value::as_str)
.unwrap_or("")
.to_string())
}
pub fn input(&self, text: &str) -> Result<(), CdpError> {
let text_escaped = serde_json::to_string(text).map_err(CdpError::Json)?;
self.call_on(
&format!(
"function(){{ this.focus(); this.value = {}; this.dispatchEvent(new Event('input', {{ bubbles: true }})); this.dispatchEvent(new Event('change', {{ bubbles: true }})); }}",
text_escaped
),
None,
)?;
Ok(())
}
pub fn clear(&self) -> Result<(), CdpError> {
self.call_on("function(){ this.focus(); this.value = ''; this.dispatchEvent(new Event('input', { bubbles: true })); }", None)?;
Ok(())
}
pub fn focus(&self) -> Result<(), CdpError> {
self.call_on("function(){ this.focus(); }", None)?;
Ok(())
}
pub fn hover(&self) -> Result<(), CdpError> {
let result = self.call_on(
"function(){ var r = this.getBoundingClientRect(); return { x: r.left + r.width/2, y: r.top + r.height/2 }; }",
None,
)?;
let x = result.get("value").and_then(|v| v.get("x")).and_then(Value::as_f64).unwrap_or(0.0);
let y = result.get("value").and_then(|v| v.get("y")).and_then(Value::as_f64).unwrap_or(0.0);
self.client.send_with_session(
"Input.enable",
None,
Some(self.session_id.as_str()),
).ok();
let params = json!({
"type": "mouseMoved",
"x": x,
"y": y
});
self.client.send_with_session("Input.dispatchMouseEvent", Some(params), Some(self.session_id.as_str()))?;
Ok(())
}
pub fn is_displayed(&self) -> Result<bool, CdpError> {
let result = self.call_on(
"function(){ var r = this.getBoundingClientRect(); return r.width > 0 && r.height > 0 && window.getComputedStyle(this).visibility !== 'hidden' && window.getComputedStyle(this).display !== 'none'; }",
None,
)?;
Ok(result.get("value").and_then(Value::as_bool).unwrap_or(false))
}
pub fn is_enabled(&self) -> Result<bool, CdpError> {
let result = self.call_on("function(){ return !this.disabled; }", None)?;
Ok(result.get("value").and_then(Value::as_bool).unwrap_or(true))
}
pub fn screenshot(&self, path: &str) -> Result<(), CdpError> {
let result = self.call_on_value(
"function(){ var r = this.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }",
None,
)?;
let v = result.get("value").ok_or_else(|| CdpError::Protocol {
id: None,
code: -1,
message: "Could not read the element bounding rectangle".into(),
})?;
let x = v.get("x").and_then(Value::as_f64).unwrap_or(0.0);
let y = v.get("y").and_then(Value::as_f64).unwrap_or(0.0);
let width = v.get("width").and_then(Value::as_f64).unwrap_or(0.0);
let height = v.get("height").and_then(Value::as_f64).unwrap_or(0.0);
let params = json!({
"format": "png",
"clip": { "x": x, "y": y, "width": width, "height": height, "scale": 1.0 }
});
let result = self.client.send_with_session(
"Page.captureScreenshot",
Some(params),
Some(self.session_id.as_str()),
)?;
let data_b64 = result
.get("data")
.and_then(Value::as_str)
.ok_or_else(|| CdpError::Protocol {
id: None,
code: -1,
message: "Page.captureScreenshot did not return image data".into(),
})?;
let data = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
data_b64,
).map_err(|e| CdpError::Protocol {
id: None,
code: -1,
message: format!("Failed to decode base64 data: {}", e),
})?;
std::fs::write(path, data).map_err(|e| CdpError::Protocol {
id: None,
code: -1,
message: format!("Failed to write the file: {}", e),
})?;
Ok(())
}
pub fn element_text(&self, locator: &str) -> Result<Option<String>, CdpError> {
self._element_text_inner(locator).map_err(|e| e.with_context(locator))
}
fn _element_text_inner(&self, locator: &str) -> Result<Option<String>, CdpError> {
let loc = crate::locator::Locator::parse(locator).map_err(|_| CdpError::Protocol {
id: None,
code: -1,
message: format!("Invalid locator: {}. Please check the locator syntax.", locator),
})?;
let Some(selector) = loc.to_css_selector() else {
return Ok(None);
};
let result = self.call_on(
"function(sel){ try { const el = this.querySelector(sel); return el ? el.textContent : null; } catch(e) { return null; } }",
Some(json!([{ "value": selector }])),
)?;
let value = result.get("value");
if value.map(Value::is_null) == Some(true) {
return Ok(None);
}
Ok(value.and_then(Value::as_str).map(|s| s.to_string()))
}
pub fn element_exists(&self, locator: &str) -> Result<bool, CdpError> {
self._element_exists_inner(locator).map_err(|e| e.with_context(locator))
}
fn _element_exists_inner(&self, locator: &str) -> Result<bool, CdpError> {
let loc = crate::locator::Locator::parse(locator).map_err(|_| CdpError::Protocol {
id: None,
code: -1,
message: format!("Invalid locator: {}. Please check the locator syntax.", locator),
})?;
let Some(selector) = loc.to_css_selector() else {
return Ok(false);
};
let result = self.call_on(
"function(sel){ try { return this.querySelector(sel) !== null; } catch(e) { return false; } }",
Some(json!([{ "value": selector }])),
)?;
Ok(result.get("value").and_then(Value::as_bool).unwrap_or(false))
}
pub fn element_attr(&self, locator: &str, attr: &str) -> Result<Option<String>, CdpError> {
self._element_attr_inner(locator, attr).map_err(|e| e.with_context(locator))
}
fn _element_attr_inner(&self, locator: &str, attr: &str) -> Result<Option<String>, CdpError> {
let loc = crate::locator::Locator::parse(locator).map_err(|_| CdpError::Protocol {
id: None,
code: -1,
message: format!("Invalid locator: {}. Please check the locator syntax.", locator),
})?;
let Some(selector) = loc.to_css_selector() else {
return Ok(None);
};
let result = self.call_on(
"function(sel, attr){ try { var el = this.querySelector(sel); return el ? el.getAttribute(attr) : null; } catch(e) { return null; } }",
Some(json!([{ "value": selector }, { "value": attr }])),
)?;
let value = result.get("value");
if value.map(Value::is_null) == Some(true) {
return Ok(None);
}
Ok(value.and_then(Value::as_str).map(|s| s.to_string()))
}
pub fn element_texts(&self, locator: &str) -> Result<Vec<String>, CdpError> {
self._element_texts_inner(locator).map_err(|e| e.with_context(locator))
}
fn _element_texts_inner(&self, locator: &str) -> Result<Vec<String>, CdpError> {
let loc = crate::locator::Locator::parse(locator).map_err(|_| CdpError::Protocol {
id: None,
code: -1,
message: format!("Invalid locator: {}. Please check the locator syntax.", locator),
})?;
let Some(selector) = loc.to_css_selector() else {
return Ok(Vec::new());
};
let result = self.call_on(
"function(sel){ try { var nodes = this.querySelectorAll(sel); var arr = []; for(var i=0;i<nodes.length;i++) { var t = (nodes[i].textContent||'').trim(); if(t) arr.push(t); } return JSON.stringify(arr); } catch(e) { return '[]'; } }",
Some(json!([{ "value": selector }])),
)?;
let s = result
.get("value")
.and_then(Value::as_str)
.unwrap_or("[]");
let arr: Vec<String> = serde_json::from_str(s).unwrap_or_else(|_| Vec::new());
Ok(arr)
}
pub fn element(&self, locator: &str) -> Result<Option<Element>, CdpError> {
self._element_inner(locator).map_err(|e| e.with_context(locator))
}
fn _element_inner(&self, locator: &str) -> Result<Option<Element>, CdpError> {
let loc = crate::locator::Locator::parse(locator).map_err(|_| CdpError::Protocol {
id: None,
code: -1,
message: format!("Invalid locator: {}. Please check the locator syntax.", locator),
})?;
if let Some(selector) = loc.to_css_selector() {
let result = self.call_on(
"function(sel){ try { return this.querySelector(sel); } catch(e) { return null; } }",
Some(json!([{ "value": selector }])),
)?;
if let Some(oid) = result.get("objectId").and_then(Value::as_str) {
let params = json!({ "objectId": oid });
let node_res = self.client.send_with_session(
"DOM.requestNode",
Some(params),
Some(self.session_id.as_str()),
)?;
if let Some(nid) = node_res.get("nodeId").and_then(Value::as_i64) {
let backend_node_id = get_backend_node_id(&self.client, &self.session_id, nid).ok();
return Ok(Some(Element::new_with_object_id(
Arc::clone(&self.client),
self.session_id.clone(),
nid,
Some(oid.to_string()),
backend_node_id,
)));
}
}
let node_id = self.with_valid_node_id(
|| {
query_selector(
&self.client,
&self.session_id,
*self.node_id.borrow(),
&selector,
)
},
|nid| query_selector(&self.client, &self.session_id, nid, &selector),
)?;
Ok(node_id.map(|id| {
let backend_node_id = get_backend_node_id(&self.client, &self.session_id, id).ok();
Element::new_with_backend(
Arc::clone(&self.client),
self.session_id.clone(),
id,
backend_node_id,
)
}))
} else if let Some(xpath) = loc.to_xpath_expression() {
let result = self.call_on(
&format!(
"function(){{ var r = document.evaluate({}, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return r.singleNodeValue; }}",
serde_json::to_string(&xpath).map_err(CdpError::Json)?
),
None,
)?;
let obj_id = result.get("objectId").and_then(Value::as_str);
if let Some(oid) = obj_id {
let params = json!({ "objectId": oid });
let res = self.client.send_with_session("DOM.requestNode", Some(params), Some(self.session_id.as_str()))?;
if let Some(nid) = res.get("nodeId").and_then(Value::as_i64) {
return Ok(Some(Element::new(Arc::clone(&self.client), self.session_id.clone(), nid)));
}
}
Ok(None)
} else {
Ok(None)
}
}
pub fn elements(&self, locator: &str) -> Result<Vec<Element>, CdpError> {
self._elements_inner(locator).map_err(|e| e.with_context(locator))
}
fn _elements_inner(&self, locator: &str) -> Result<Vec<Element>, CdpError> {
let loc = crate::locator::Locator::parse(locator).map_err(|_| CdpError::Protocol {
id: None,
code: -1,
message: format!("Invalid locator: {}. Please check the locator syntax.", locator),
})?;
if let Some(selector) = loc.to_css_selector() {
let sel = serde_json::to_string(&selector).map_err(CdpError::Json)?;
let result = self.call_on(
&format!("function(){{ return Array.from(this.querySelectorAll({})); }}", sel),
None,
)?;
let obj_id = result.get("objectId").and_then(Value::as_str);
let mut out = Vec::new();
if let Some(oid) = obj_id {
let params = json!({ "functionDeclaration": "function(){ return this.length; }", "objectId": oid });
let res = self.client.send_with_session("Runtime.callFunctionOn", Some(params), Some(self.session_id.as_str()))?;
let len = res.get("result").and_then(|r| r.get("value")).and_then(Value::as_u64).unwrap_or(0) as usize;
for i in 0..len {
let params = json!({ "functionDeclaration": "function(i){ return this[i]; }", "objectId": oid, "arguments": [{"value": i}] });
let res = self.client.send_with_session("Runtime.callFunctionOn", Some(params), Some(self.session_id.as_str()))?;
let eid = res.get("result").and_then(|r| r.get("objectId")).and_then(Value::as_str);
if let Some(eid) = eid {
let params = json!({ "objectId": eid });
let node_res = self.client.send_with_session("DOM.requestNode", Some(params), Some(self.session_id.as_str()))?;
if let Some(nid) = node_res.get("nodeId").and_then(Value::as_i64) {
let backend_node_id =
get_backend_node_id(&self.client, &self.session_id, nid).ok();
out.push(Element::new_with_object_id(
Arc::clone(&self.client),
self.session_id.clone(),
nid,
Some(eid.to_string()),
backend_node_id,
));
}
}
}
}
Ok(out)
} else if let Some(xpath) = loc.to_xpath_expression() {
let result = self.call_on(
&format!(
"function(){{ var r = document.evaluate({}, this, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); var a = []; for(var i=0;i<r.snapshotLength;i++) a.push(r.snapshotItem(i)); return a; }}",
serde_json::to_string(&xpath).map_err(CdpError::Json)?
),
None,
)?;
let obj_id = result.get("objectId").and_then(Value::as_str);
let mut elements = Vec::new();
if let Some(oid) = obj_id {
let params = json!({ "functionDeclaration": "function(){ return this.length; }", "objectId": oid });
let res = self.client.send_with_session("Runtime.callFunctionOn", Some(params), Some(self.session_id.as_str()))?;
let len = res.get("result").and_then(|r| r.get("value")).and_then(Value::as_u64).unwrap_or(0) as usize;
for i in 0..len {
let params = json!({ "functionDeclaration": "function(i){ return this[i]; }", "objectId": oid, "arguments": [{"value": i}] });
let res = self.client.send_with_session("Runtime.callFunctionOn", Some(params), Some(self.session_id.as_str()))?;
let eid = res.get("result").and_then(|r| r.get("objectId")).and_then(Value::as_str);
if let Some(eid) = eid {
let params = json!({ "objectId": eid });
let node_res = self.client.send_with_session("DOM.requestNode", Some(params), Some(self.session_id.as_str()))?;
if let Some(nid) = node_res.get("nodeId").and_then(Value::as_i64) {
elements.push(Element::new(Arc::clone(&self.client), self.session_id.clone(), nid));
}
}
}
}
Ok(elements)
} else {
Ok(Vec::new())
}
}
pub fn parent(&self, level: u32) -> Result<Option<Element>, CdpError> {
let script = format!(
"function(){{ var el = this; for(var i=0;i<{};i++){{ if(el.parentElement){{el=el.parentElement;}}else{{return null;}} }} return el; }}",
level
);
let result = self.call_on(&script, None)?;
let obj_id = result.get("objectId").and_then(Value::as_str);
if let Some(oid) = obj_id {
let res = describe_node_by_object_id(&self.client, &self.session_id, &oid)?;
if let (Some(nid), bid) = res {
return Ok(Some(Element::new_with_backend(
Arc::clone(&self.client),
self.session_id.clone(),
nid,
bid,
)));
}
}
Ok(None)
}
pub fn child(&self, locator: &str) -> Result<Option<Element>, CdpError> {
self.element(locator)
}
pub fn prev(&self) -> Result<Option<Element>, CdpError> {
let result = self.call_on("function(){ return this.previousElementSibling; }", None)?;
let obj_id = result.get("objectId").and_then(Value::as_str);
if let Some(oid) = obj_id {
let res = describe_node_by_object_id(&self.client, &self.session_id, &oid)?;
if let (Some(nid), bid) = res {
return Ok(Some(Element::new_with_backend(
Arc::clone(&self.client),
self.session_id.clone(),
nid,
bid,
)));
}
}
Ok(None)
}
pub fn next(&self) -> Result<Option<Element>, CdpError> {
let result = self.call_on("function(){ return this.nextElementSibling; }", None)?;
let obj_id = result.get("objectId").and_then(Value::as_str);
if let Some(oid) = obj_id {
let res = describe_node_by_object_id(&self.client, &self.session_id, &oid)?;
if let (Some(nid), bid) = res {
return Ok(Some(Element::new_with_backend(
Arc::clone(&self.client),
self.session_id.clone(),
nid,
bid,
)));
}
}
Ok(None)
}
pub fn children(&self) -> Result<Vec<Element>, CdpError> {
let result = self.call_on(
"function(){ return Array.from(this.children); }",
None,
)?;
let obj_id = result.get("objectId").and_then(Value::as_str);
let mut elements = Vec::new();
if let Some(oid) = obj_id {
let params = json!({ "functionDeclaration": "function(){ return this.length; }", "objectId": oid });
let res = self.client.send_with_session("Runtime.callFunctionOn", Some(params), Some(self.session_id.as_str()))?;
let len = res.get("result").and_then(|r| r.get("value")).and_then(Value::as_u64).unwrap_or(0) as usize;
for i in 0..len {
let params = json!({ "functionDeclaration": "function(i){ return this[i]; }", "objectId": oid, "arguments": [{"value": i}] });
let res = self.client.send_with_session("Runtime.callFunctionOn", Some(params), Some(self.session_id.as_str()))?;
let eid = res.get("result").and_then(|r| r.get("objectId")).and_then(Value::as_str);
if let Some(eid) = eid {
let desc = describe_node_by_object_id(&self.client, &self.session_id, &eid)?;
if let (Some(nid), bid) = desc {
elements.push(Element::new_with_backend(
Arc::clone(&self.client),
self.session_id.clone(),
nid,
bid,
));
}
}
}
}
Ok(elements)
}
pub fn rect(&self) -> Result<Value, CdpError> {
let result = self.call_on_value(
"function(){ var r = this.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height, top: r.top, bottom: r.bottom, left: r.left, right: r.right }; }",
None,
)?;
Ok(result.get("value").cloned().unwrap_or(Value::Null))
}
pub fn check(&self, uncheck: bool) -> Result<(), CdpError> {
if uncheck {
let checked = self.property("checked")?;
if checked.as_bool().unwrap_or(false) {
self.click()?;
}
} else {
let checked = self.property("checked")?;
if !checked.as_bool().unwrap_or(false) {
self.click()?;
}
}
Ok(())
}
pub fn select(&self, text_or_value: &str, by_text: bool) -> Result<(), CdpError> {
let tag = self.tag()?;
if tag != "select" {
return Err(CdpError::Protocol {
id: None,
code: -1,
message: format!("select() can only be used on <select> elements, but the current element is <{}>", tag),
});
}
let escaped = serde_json::to_string(text_or_value).map_err(CdpError::Json)?;
let script = if by_text {
format!(
"function(){{ var opts = this.options; for(var i=0;i<opts.length;i++){{ if(opts[i].text === {}){{ this.selectedIndex = i; this.dispatchEvent(new Event('change', {{bubbles:true}})); return true; }} }} return false; }}",
escaped
)
} else {
format!(
"function(){{ var opts = this.options; for(var i=0;i<opts.length;i++){{ if(opts[i].value === {}){{ this.selectedIndex = i; this.dispatchEvent(new Event('change', {{bubbles:true}})); return true; }} }} return false; }}",
escaped
)
};
self.call_on(&script, None)?;
Ok(())
}
pub fn value(&self) -> Result<String, CdpError> {
let result = self.call_on("function(){ return this.value || ''; }", None)?;
Ok(result.get("value").and_then(Value::as_str).unwrap_or("").to_string())
}
pub fn drag(&self, offset_x: i64, offset_y: i64, duration: u64) -> Result<(), CdpError> {
let result = self.call_on_value(
"function(){ var r = this.getBoundingClientRect(); return { x: r.left + r.width/2, y: r.top + r.height/2 }; }",
None,
)?;
let start_x = result.get("value").and_then(|v| v.get("x")).and_then(Value::as_f64).unwrap_or(0.0);
let start_y = result.get("value").and_then(|v| v.get("y")).and_then(Value::as_f64).unwrap_or(0.0);
let end_x = start_x + offset_x as f64;
let end_y = start_y + offset_y as f64;
self.drag_by_coords(start_x, start_y, end_x, end_y, duration)
}
pub fn drag_to(&self, target: &Element, duration: u64) -> Result<(), CdpError> {
let start_result = self.call_on_value(
"function(){ var r = this.getBoundingClientRect(); return { x: r.left + r.width/2, y: r.top + r.height/2 }; }",
None,
)?;
let start_x = start_result.get("value").and_then(|v| v.get("x")).and_then(Value::as_f64).unwrap_or(0.0);
let start_y = start_result.get("value").and_then(|v| v.get("y")).and_then(Value::as_f64).unwrap_or(0.0);
let end_result = target.call_on_value(
"function(){ var r = this.getBoundingClientRect(); return { x: r.left + r.width/2, y: r.top + r.height/2 }; }",
None,
)?;
let end_x = end_result.get("value").and_then(|v| v.get("x")).and_then(Value::as_f64).unwrap_or(0.0);
let end_y = end_result.get("value").and_then(|v| v.get("y")).and_then(Value::as_f64).unwrap_or(0.0);
self.drag_by_coords(start_x, start_y, end_x, end_y, duration)
}
fn drag_by_coords(&self, start_x: f64, start_y: f64, end_x: f64, end_y: f64, duration: u64) -> Result<(), CdpError> {
let _ = self.client.send_with_session("Input.enable", None, Some(self.session_id.as_str()));
let params_press = json!({
"type": "mousePressed",
"x": start_x,
"y": start_y,
"button": "left",
"clickCount": 1
});
self.client.send_with_session("Input.dispatchMouseEvent", Some(params_press), Some(self.session_id.as_str()))?;
let steps = (duration as f64 / 16.0).ceil() as u32; for i in 1..=steps {
let t = i as f64 / steps as f64;
let x = start_x + (end_x - start_x) * t;
let y = start_y + (end_y - start_y) * t;
let params_move = json!({
"type": "mouseMoved",
"x": x,
"y": y,
"button": "left",
"clickCount": 0
});
self.client.send_with_session("Input.dispatchMouseEvent", Some(params_move), Some(self.session_id.as_str()))?;
std::thread::sleep(std::time::Duration::from_millis(16));
}
let params_release = json!({
"type": "mouseReleased",
"x": end_x,
"y": end_y,
"button": "left",
"clickCount": 1
});
self.client.send_with_session("Input.dispatchMouseEvent", Some(params_release), Some(self.session_id.as_str()))?;
Ok(())
}
pub fn hover_at(&self, offset_x: Option<f64>, offset_y: Option<f64>) -> Result<(), CdpError> {
let result = self.call_on_value(
"function(){ var r = this.getBoundingClientRect(); return { x: r.left + r.width/2, y: r.top + r.height/2 }; }",
None,
)?;
let base_x = result.get("value").and_then(|v| v.get("x")).and_then(Value::as_f64).unwrap_or(0.0);
let base_y = result.get("value").and_then(|v| v.get("y")).and_then(Value::as_f64).unwrap_or(0.0);
let x = base_x + offset_x.unwrap_or(0.0);
let y = base_y + offset_y.unwrap_or(0.0);
self.client.send_with_session(
"Input.enable",
None,
Some(self.session_id.as_str()),
).ok();
let params = json!({
"type": "mouseMoved",
"x": x,
"y": y
});
self.client.send_with_session("Input.dispatchMouseEvent", Some(params), Some(self.session_id.as_str()))?;
Ok(())
}
pub fn scroll_into_view(&self) -> Result<(), CdpError> {
self.call_on("function(){ this.scrollIntoView({ block: 'center', inline: 'center' }); }", None)?;
Ok(())
}
pub fn scroll(&self, x: i64, y: i64) -> Result<(), CdpError> {
let script = format!("function(){{ this.scrollTo({}, {}); }}", x, y);
self.call_on(&script, None)?;
Ok(())
}
pub fn remove_attr(&self, name: &str) -> Result<(), CdpError> {
let name_json = serde_json::to_string(name).map_err(CdpError::Json)?;
self.call_on(&format!("function(){{ this.removeAttribute({}); }}", name_json), None)?;
Ok(())
}
pub fn style(&self) -> Result<String, CdpError> {
let result = self.call_on("function(){ return this.getAttribute('style') || ''; }", None)?;
Ok(result.get("value").and_then(Value::as_str).unwrap_or("").to_string())
}
pub fn remove(&self) -> Result<(), CdpError> {
self.call_on("function(){ this.remove(); }", None)?;
Ok(())
}
}