use std::fmt;
use std::sync::Arc;
use serde_json::Value;
use tracing::debug;
use crate::error::{Error, Result};
use crate::identifiers::{ElementId, FrameId, SessionId, TabId};
use crate::protocol::{Command, ElementCommand, InputCommand, Request, Response, ScriptCommand};
use super::Window;
use super::keyboard::Key;
use super::selector::By;
pub(crate) struct ElementInner {
pub id: ElementId,
pub tab_id: TabId,
pub frame_id: FrameId,
pub session_id: SessionId,
pub window: Option<Window>,
}
#[derive(Clone)]
pub struct Element {
pub(crate) inner: Arc<ElementInner>,
}
impl fmt::Debug for Element {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Element")
.field("id", &self.inner.id)
.field("tab_id", &self.inner.tab_id)
.field("frame_id", &self.inner.frame_id)
.finish_non_exhaustive()
}
}
impl Element {
pub(crate) fn new(
id: ElementId,
tab_id: TabId,
frame_id: FrameId,
session_id: SessionId,
window: Option<Window>,
) -> Self {
Self {
inner: Arc::new(ElementInner {
id,
tab_id,
frame_id,
session_id,
window,
}),
}
}
}
impl Element {
#[inline]
#[must_use]
pub fn id(&self) -> &ElementId {
&self.inner.id
}
#[inline]
#[must_use]
pub fn tab_id(&self) -> TabId {
self.inner.tab_id
}
#[inline]
#[must_use]
pub fn frame_id(&self) -> FrameId {
self.inner.frame_id
}
}
impl Element {
pub async fn click(&self) -> Result<()> {
debug!(element_id = %self.inner.id, "Clicking element");
self.call_method("click", vec![]).await?;
Ok(())
}
pub async fn focus(&self) -> Result<()> {
debug!(element_id = %self.inner.id, "Focusing element");
self.call_method("focus", vec![]).await?;
Ok(())
}
pub async fn blur(&self) -> Result<()> {
debug!(element_id = %self.inner.id, "Blurring element");
self.call_method("blur", vec![]).await?;
Ok(())
}
pub async fn clear(&self) -> Result<()> {
debug!(element_id = %self.inner.id, "Clearing element");
self.set_property("value", Value::String(String::new()))
.await
}
}
impl Element {
pub async fn get_text(&self) -> Result<String> {
let value = self.get_property("textContent").await?;
Ok(value.as_str().unwrap_or("").to_string())
}
pub async fn get_inner_html(&self) -> Result<String> {
let value = self.get_property("innerHTML").await?;
Ok(value.as_str().unwrap_or("").to_string())
}
pub async fn get_value(&self) -> Result<String> {
let value = self.get_property("value").await?;
Ok(value.as_str().unwrap_or("").to_string())
}
pub async fn set_value(&self, value: &str) -> Result<()> {
self.set_property("value", Value::String(value.to_string()))
.await
}
pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
let result = self
.call_method("getAttribute", vec![Value::String(name.to_string())])
.await?;
Ok(result.as_str().map(|s| s.to_string()))
}
pub async fn is_displayed(&self) -> Result<bool> {
let script = r#"
const el = arguments[0];
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
"#;
let command = Command::Script(ScriptCommand::Evaluate {
script: script.to_string(),
args: vec![serde_json::json!({"elementId": self.inner.id.as_str()})],
});
let response = self.send_command(command).await?;
let displayed = response
.result
.as_ref()
.and_then(|v| v.get("value"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
Ok(displayed)
}
pub async fn is_enabled(&self) -> Result<bool> {
let disabled = self.get_property("disabled").await?;
Ok(!disabled.as_bool().unwrap_or(false))
}
}
impl Element {
pub async fn press(&self, key: Key) -> Result<()> {
let (key_str, code, key_code, printable) = key.properties();
self.type_key(
key_str, code, key_code, printable, false, false, false, false,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn type_key(
&self,
key: &str,
code: &str,
key_code: u32,
printable: bool,
ctrl: bool,
shift: bool,
alt: bool,
meta: bool,
) -> Result<()> {
let command = Command::Input(InputCommand::TypeKey {
element_id: self.inner.id.clone(),
key: key.to_string(),
code: code.to_string(),
key_code,
printable,
ctrl,
shift,
alt,
meta,
});
self.send_command(command).await?;
Ok(())
}
pub async fn type_char(&self, c: char) -> Result<()> {
self.type_text(&c.to_string()).await
}
pub async fn type_text(&self, text: &str) -> Result<()> {
debug!(element_id = %self.inner.id, text_len = text.len(), "Typing text");
let command = Command::Input(InputCommand::TypeText {
element_id: self.inner.id.clone(),
text: text.to_string(),
});
self.send_command(command).await?;
Ok(())
}
}
impl Element {
pub async fn mouse_click(&self, button: u8) -> Result<()> {
debug!(element_id = %self.inner.id, button = button, "Mouse clicking element");
let command = Command::Input(InputCommand::MouseClick {
element_id: Some(self.inner.id.clone()),
x: None,
y: None,
button,
});
self.send_command(command).await?;
Ok(())
}
pub async fn double_click(&self) -> Result<()> {
debug!(element_id = %self.inner.id, "Double clicking element");
self.call_method(
"dispatchEvent",
vec![serde_json::json!({"type": "dblclick", "bubbles": true, "cancelable": true})],
)
.await?;
Ok(())
}
pub async fn context_click(&self) -> Result<()> {
debug!(element_id = %self.inner.id, "Context clicking element");
self.mouse_click(2).await
}
pub async fn hover(&self) -> Result<()> {
debug!(element_id = %self.inner.id, "Hovering over element");
self.mouse_move().await
}
pub async fn mouse_move(&self) -> Result<()> {
debug!(element_id = %self.inner.id, "Moving mouse to element");
let command = Command::Input(InputCommand::MouseMove {
element_id: Some(self.inner.id.clone()),
x: None,
y: None,
});
self.send_command(command).await?;
Ok(())
}
pub async fn mouse_down(&self, button: u8) -> Result<()> {
debug!(element_id = %self.inner.id, button = button, "Mouse down on element");
let command = Command::Input(InputCommand::MouseDown {
element_id: Some(self.inner.id.clone()),
x: None,
y: None,
button,
});
self.send_command(command).await?;
Ok(())
}
pub async fn mouse_up(&self, button: u8) -> Result<()> {
debug!(element_id = %self.inner.id, button = button, "Mouse up on element");
let command = Command::Input(InputCommand::MouseUp {
element_id: Some(self.inner.id.clone()),
x: None,
y: None,
button,
});
self.send_command(command).await?;
Ok(())
}
}
impl Element {
pub async fn scroll_into_view(&self) -> Result<()> {
debug!(element_id = %self.inner.id, "Scrolling element into view");
self.call_method(
"scrollIntoView",
vec![serde_json::json!({"behavior": "smooth", "block": "center"})],
)
.await?;
Ok(())
}
pub async fn scroll_into_view_instant(&self) -> Result<()> {
debug!(element_id = %self.inner.id, "Scrolling element into view (instant)");
self.call_method(
"scrollIntoView",
vec![serde_json::json!({"behavior": "instant", "block": "center"})],
)
.await?;
Ok(())
}
pub async fn get_bounding_rect(&self) -> Result<(f64, f64, f64, f64)> {
let result = self.call_method("getBoundingClientRect", vec![]).await?;
let x = result.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
let y = result.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
let width = result.get("width").and_then(|v| v.as_f64()).unwrap_or(0.0);
let height = result.get("height").and_then(|v| v.as_f64()).unwrap_or(0.0);
debug!(element_id = %self.inner.id, x = x, y = y, width = width, height = height, "Got bounding rect");
Ok((x, y, width, height))
}
}
impl Element {
pub async fn is_checked(&self) -> Result<bool> {
let value = self.get_property("checked").await?;
Ok(value.as_bool().unwrap_or(false))
}
pub async fn check(&self) -> Result<()> {
if !self.is_checked().await? {
self.click().await?;
}
Ok(())
}
pub async fn uncheck(&self) -> Result<()> {
if self.is_checked().await? {
self.click().await?;
}
Ok(())
}
pub async fn toggle(&self) -> Result<()> {
self.click().await
}
pub async fn set_checked(&self, checked: bool) -> Result<()> {
if checked {
self.check().await
} else {
self.uncheck().await
}
}
}
impl Element {
pub async fn select_by_text(&self, text: &str) -> Result<()> {
let escaped = serde_json::to_string(text).unwrap_or_else(|_| format!("\"{}\"", text));
let script = format!(
r#"const select = arguments[0];
for (const opt of select.options) {{
if (opt.textContent.trim() === {escaped}) {{
opt.selected = true;
select.dispatchEvent(new Event('change', {{bubbles: true}}));
return true;
}}
}}
return false;"#
);
let command = Command::Script(ScriptCommand::Evaluate {
script,
args: vec![serde_json::json!({"elementId": self.inner.id.as_str()})],
});
let response = self.send_command(command).await?;
let found = response
.result
.as_ref()
.and_then(|v| v.get("value"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !found {
return Err(Error::invalid_argument(format!(
"Option with text '{}' not found",
text
)));
}
Ok(())
}
pub async fn select_by_value(&self, value: &str) -> Result<()> {
self.set_property("value", Value::String(value.to_string()))
.await?;
self.call_method(
"dispatchEvent",
vec![serde_json::json!({"type": "change", "bubbles": true})],
)
.await?;
Ok(())
}
pub async fn select_by_index(&self, index: usize) -> Result<()> {
self.set_property("selectedIndex", Value::Number(index.into()))
.await?;
self.call_method(
"dispatchEvent",
vec![serde_json::json!({"type": "change", "bubbles": true})],
)
.await?;
Ok(())
}
pub async fn get_selected_value(&self) -> Result<Option<String>> {
let value = self.get_property("value").await?;
Ok(value.as_str().map(|s| s.to_string()))
}
pub async fn get_selected_index(&self) -> Result<i64> {
let value = self.get_property("selectedIndex").await?;
Ok(value.as_i64().unwrap_or(-1))
}
pub async fn get_selected_text(&self) -> Result<Option<String>> {
let options = self.find_elements(By::css("option:checked")).await?;
if let Some(option) = options.first() {
let text = option.get_text().await?;
return Ok(Some(text));
}
Ok(None)
}
pub async fn is_multiple(&self) -> Result<bool> {
let value = self.get_property("multiple").await?;
Ok(value.as_bool().unwrap_or(false))
}
}
impl Element {
pub async fn find_element(&self, by: By) -> Result<Element> {
let command = Command::Element(ElementCommand::Find {
strategy: by.strategy().to_string(),
value: by.value().to_string(),
parent_id: Some(self.inner.id.clone()),
});
let response = self.send_command(command).await?;
let element_id = response
.result
.as_ref()
.and_then(|v| v.get("elementId"))
.and_then(|v| v.as_str())
.ok_or_else(|| {
Error::element_not_found(
format!("{}:{}", by.strategy(), by.value()),
self.inner.tab_id,
self.inner.frame_id,
)
})?;
Ok(Element::new(
ElementId::new(element_id),
self.inner.tab_id,
self.inner.frame_id,
self.inner.session_id,
self.inner.window.clone(),
))
}
pub async fn find_elements(&self, by: By) -> Result<Vec<Element>> {
let command = Command::Element(ElementCommand::FindAll {
strategy: by.strategy().to_string(),
value: by.value().to_string(),
parent_id: Some(self.inner.id.clone()),
});
let response = self.send_command(command).await?;
let element_ids = response
.result
.as_ref()
.and_then(|v| v.get("elementIds"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(|id| {
Element::new(
ElementId::new(id),
self.inner.tab_id,
self.inner.frame_id,
self.inner.session_id,
self.inner.window.clone(),
)
})
.collect()
})
.unwrap_or_default();
Ok(element_ids)
}
}
impl Element {
pub async fn get_property(&self, name: &str) -> Result<Value> {
let command = Command::Element(ElementCommand::GetProperty {
element_id: self.inner.id.clone(),
name: name.to_string(),
});
let response = self.send_command(command).await?;
Ok(response
.result
.and_then(|v| v.get("value").cloned())
.unwrap_or(Value::Null))
}
pub async fn set_property(&self, name: &str, value: Value) -> Result<()> {
let command = Command::Element(ElementCommand::SetProperty {
element_id: self.inner.id.clone(),
name: name.to_string(),
value,
});
self.send_command(command).await?;
Ok(())
}
pub async fn call_method(&self, name: &str, args: Vec<Value>) -> Result<Value> {
let command = Command::Element(ElementCommand::CallMethod {
element_id: self.inner.id.clone(),
name: name.to_string(),
args,
});
let response = self.send_command(command).await?;
Ok(response
.result
.and_then(|v| v.get("value").cloned())
.unwrap_or(Value::Null))
}
}
impl Element {
pub async fn screenshot(&self) -> Result<String> {
self.screenshot_with_format("png", None).await
}
pub async fn screenshot_jpeg(&self, quality: u8) -> Result<String> {
self.screenshot_with_format("jpeg", Some(quality.min(100)))
.await
}
async fn screenshot_with_format(&self, format: &str, quality: Option<u8>) -> Result<String> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD as Base64Standard;
use image::GenericImageView;
let command = Command::Element(ElementCommand::CaptureScreenshot {
element_id: self.inner.id.clone(),
format: format.to_string(),
quality,
});
let response = self.send_command(command).await?;
tracing::debug!(response = ?response, "Element screenshot response");
let result = response.result.as_ref().ok_or_else(|| {
let error_str = response.error.as_deref().unwrap_or("none");
let msg_str = response.message.as_deref().unwrap_or("none");
Error::script_error(format!(
"Element screenshot failed. error={}, message={}",
error_str, msg_str
))
})?;
let data = result
.get("data")
.and_then(|v| v.as_str())
.ok_or_else(|| Error::script_error("Screenshot response missing data field"))?;
if let Some(clip) = result.get("clip") {
let x = clip.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
let y = clip.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
let width = clip.get("width").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
let height = clip.get("height").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
let scale = clip.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0);
let x = (x as f64 * scale) as u32;
let y = (y as f64 * scale) as u32;
let width = (width as f64 * scale) as u32;
let height = (height as f64 * scale) as u32;
if width == 0 || height == 0 {
return Err(Error::script_error("Element has zero dimensions"));
}
let image_bytes = Base64Standard
.decode(data)
.map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))?;
let img = image::load_from_memory(&image_bytes)
.map_err(|e| Error::script_error(format!("Failed to load image: {}", e)))?;
let (img_width, img_height) = img.dimensions();
let x = x.min(img_width.saturating_sub(1));
let y = y.min(img_height.saturating_sub(1));
let width = width.min(img_width.saturating_sub(x));
let height = height.min(img_height.saturating_sub(y));
let cropped = img.crop_imm(x, y, width, height);
let mut output = std::io::Cursor::new(Vec::new());
match format {
"jpeg" => {
use image::codecs::jpeg::JpegEncoder;
let q = quality.unwrap_or(85);
let encoder = JpegEncoder::new_with_quality(&mut output, q);
cropped.write_with_encoder(encoder).map_err(|e| {
Error::script_error(format!("Failed to encode JPEG: {}", e))
})?;
}
_ => {
cropped
.write_to(&mut output, image::ImageFormat::Png)
.map_err(|e| Error::script_error(format!("Failed to encode PNG: {}", e)))?;
}
}
Ok(Base64Standard.encode(output.into_inner()))
} else {
Ok(data.to_string())
}
}
pub async fn screenshot_bytes(&self) -> Result<Vec<u8>> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD as Base64Standard;
let base64_data = self.screenshot().await?;
Base64Standard
.decode(&base64_data)
.map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))
}
pub async fn save_screenshot(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
use base64::Engine;
use base64::engine::general_purpose::STANDARD as Base64Standard;
let path = path.as_ref();
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("png")
.to_lowercase();
let base64_data = match ext.as_str() {
"jpg" | "jpeg" => self.screenshot_jpeg(85).await?,
_ => self.screenshot().await?,
};
let bytes = Base64Standard
.decode(&base64_data)
.map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))?;
tokio::fs::write(path, bytes).await.map_err(Error::Io)?;
Ok(())
}
}
impl Element {
async fn send_command(&self, command: Command) -> Result<Response> {
let window = self
.inner
.window
.as_ref()
.ok_or_else(|| Error::protocol("Element has no associated window"))?;
let request = Request::new(self.inner.tab_id, self.inner.frame_id, command);
window
.inner
.pool
.send(window.inner.session_id, request)
.await
}
}
#[cfg(test)]
mod tests {
use super::Element;
#[test]
fn test_element_is_clone() {
fn assert_clone<T: Clone>() {}
assert_clone::<Element>();
}
#[test]
fn test_element_is_debug() {
fn assert_debug<T: std::fmt::Debug>() {}
assert_debug::<Element>();
}
}