use crate::error::{Error, Result};
use crate::page::{escape_js_string, sleep_ms, Page, INTERACTION_DELAY_MS};
#[derive(Debug, Clone, Copy)]
pub struct BoundingBox {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
impl BoundingBox {
pub fn center(&self) -> (f64, f64) {
(self.x + self.width / 2.0, self.y + self.height / 2.0)
}
}
pub struct Element {
pub(crate) page: Page,
pub(crate) node_id: i32,
}
impl Element {
pub async fn center(&self) -> Result<(f64, f64)> {
let model = self.page.session.get_box_model(self.node_id).await?;
model.try_center().ok_or_else(|| Error::ElementNotVisible {
selector: format!("node {}", self.node_id),
})
}
pub async fn click(&self) -> Result<()> {
let (x, y) = self.center().await?;
self.page.click_at(x, y).await
}
pub async fn human_click(&self) -> Result<()> {
let (x, y) = self.center().await?;
self.page.human().move_and_click(x, y).await
}
pub async fn human_drag_by(&self, dx: f64) -> Result<()> {
let (x, y) = self.center().await?;
self.page.human().drag_by(x, y, dx).await
}
pub async fn outer_html(&self) -> Result<String> {
self.page.session.get_outer_html(self.node_id).await
}
pub async fn text(&self) -> Result<String> {
self.eval_str("this.textContent || ''").await
}
async fn eval_on_element(&self, js_body: &str) -> Result<serde_json::Value> {
let object_id = self.page.session.resolve_node(self.node_id).await?;
let func = format!("function() {{ return {}; }}", js_body);
let result = self
.page
.session
.call_function_on(&object_id, &func)
.await?;
Ok(result.result.value.unwrap_or(serde_json::Value::Null))
}
async fn eval_str(&self, js_body: &str) -> Result<String> {
let value = self.eval_on_element(js_body).await?;
Ok(value.as_str().unwrap_or("").to_string())
}
async fn eval_bool(&self, js_body: &str, default: bool) -> Result<bool> {
let value = self.eval_on_element(js_body).await?;
Ok(value.as_bool().unwrap_or(default))
}
pub async fn type_text(&self, text: &str) -> Result<()> {
self.click().await?;
sleep_ms(INTERACTION_DELAY_MS).await;
self.page.session.insert_text(text).await
}
pub async fn focus(&self) -> Result<()> {
self.page.session.focus(self.node_id).await
}
pub async fn is_visible(&self) -> Result<bool> {
match self.page.session.get_box_model(self.node_id).await {
Ok(_) => Ok(true),
Err(Error::Cdp { message, .. }) if message.contains("box model") => Ok(false),
Err(e) => Err(e),
}
}
pub async fn bounding_box(&self) -> Option<BoundingBox> {
match self.page.session.get_box_model(self.node_id).await {
Ok(model) => {
let content = &model.content;
if content.len() >= 8 {
let xs = [content[0], content[2], content[4], content[6]];
let ys = [content[1], content[3], content[5], content[7]];
let min_x = xs.iter().copied().fold(f64::INFINITY, f64::min);
let max_x = xs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let min_y = ys.iter().copied().fold(f64::INFINITY, f64::min);
let max_y = ys.iter().copied().fold(f64::NEG_INFINITY, f64::max);
Some(BoundingBox {
x: min_x,
y: min_y,
width: max_x - min_x,
height: max_y - min_y,
})
} else {
None
}
}
Err(_) => None,
}
}
pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
let escaped_name = escape_js_string(name);
let value = self
.eval_on_element(&format!("this.getAttribute('{}')", escaped_name))
.await?;
if value.is_null() {
return Ok(None);
}
if let Some(s) = value.as_str() {
return Ok(Some(s.to_string()));
}
Ok(None)
}
pub async fn tag_name(&self) -> Result<String> {
self.eval_str("this.tagName.toLowerCase()").await
}
pub async fn is_enabled(&self) -> Result<bool> {
self.eval_bool("!this.disabled", true).await
}
pub async fn is_checked(&self) -> Result<bool> {
self.eval_bool("this.checked === true", false).await
}
pub async fn value(&self) -> Result<String> {
self.eval_str("this.value || ''").await
}
pub(crate) async fn input_type(&self) -> Result<Option<String>> {
let value = self
.eval_on_element("this instanceof HTMLInputElement ? this.type : null")
.await?;
Ok(value.as_str().map(str::to_owned))
}
pub(crate) async fn set_range_value(&self, value: &str) -> Result<Option<String>> {
let object_id = self.page.session.resolve_node(self.node_id).await?;
let requested = serde_json::to_string(value)?;
let function = format!(
r#"function() {{
const requested = {requested};
const requestedNumber = Number(requested);
if (!Number.isFinite(requestedNumber)) return null;
const probe = this.cloneNode(false);
probe.value = requested;
const normalized = probe.value;
if (!probe.validity.valid || Number(normalized) !== requestedNumber) return null;
this.value = requested;
// Capture before listeners run: fill must reject handlers that
// overwrite the requested range value during input/change.
const assigned = this.value;
this.dispatchEvent(new Event('input', {{ bubbles: true }}));
this.dispatchEvent(new Event('change', {{ bubbles: true }}));
return assigned;
}}"#
);
let result = self
.page
.session
.call_function_on(&object_id, &function)
.await?;
if let Some(exception) = result.exception_details {
return Err(Error::cdp_msg(format!(
"JavaScript error: {} at {}:{}",
exception.text, exception.line_number, exception.column_number
)));
}
Ok(result
.result
.value
.and_then(|value| value.as_str().map(str::to_owned)))
}
pub async fn css(&self, property: &str) -> Result<String> {
let escaped = escape_js_string(property);
self.eval_str(&format!(
"getComputedStyle(this).getPropertyValue('{}')",
escaped
))
.await
}
pub async fn scroll_into_view(&self) -> Result<()> {
let object_id = self.page.session.resolve_node(self.node_id).await?;
self.page
.session
.call_function_on(
&object_id,
"function() { this.scrollIntoView({ behavior: 'smooth', block: 'center' }); }",
)
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::Element;
#[test]
fn element_is_send_and_static() {
fn assert_send_static<T: Send + 'static>() {}
assert_send_static::<Element>();
}
}