viewpoint_core/page/locator/element/
mod.rs

1//! Element handle types for low-level DOM operations.
2//!
3//! Unlike [`Locator`], an `ElementHandle` is bound to a specific element instance.
4//! If the element is removed from the DOM, the handle becomes stale.
5
6use crate::error::LocatorError;
7use crate::Page;
8
9/// A bounding box representing an element's position and size.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct BoundingBox {
12    /// X coordinate of the top-left corner.
13    pub x: f64,
14    /// Y coordinate of the top-left corner.
15    pub y: f64,
16    /// Width of the element.
17    pub width: f64,
18    /// Height of the element.
19    pub height: f64,
20}
21
22/// A handle to a DOM element.
23///
24/// Unlike [`Locator`], an `ElementHandle` is bound to a specific element instance.
25/// If the element is removed from the DOM, the handle becomes stale.
26///
27/// Most operations should prefer using [`Locator`] for its auto-waiting and
28/// re-querying capabilities. Use `ElementHandle` only when you need:
29/// - To pass an element reference to JavaScript
30/// - Low-level DOM operations
31/// - Box model information
32#[derive(Debug)]
33pub struct ElementHandle<'a> {
34    pub(crate) object_id: String,
35    pub(crate) page: &'a Page,
36}
37
38impl ElementHandle<'_> {
39    /// Get the object ID of this element handle.
40    ///
41    /// This is the CDP remote object ID that can be used for further CDP calls.
42    pub fn object_id(&self) -> &str {
43        &self.object_id
44    }
45
46    /// Get the box model of the element.
47    ///
48    /// Returns detailed information about the element's box model including
49    /// content, padding, border, and margin boxes.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the element is no longer attached to the DOM.
54    pub async fn box_model(&self) -> Result<Option<BoxModel>, LocatorError> {
55        #[derive(Debug, serde::Deserialize)]
56        struct BoxModelResult {
57            model: Option<BoxModel>,
58        }
59
60        let result: BoxModelResult = self.page
61            .connection()
62            .send_command(
63                "DOM.getBoxModel",
64                Some(serde_json::json!({
65                    "objectId": self.object_id
66                })),
67                Some(self.page.session_id()),
68            )
69            .await?;
70
71        Ok(result.model)
72    }
73
74    /// Check if the element is still attached to the DOM.
75    ///
76    /// Returns `true` if the element still exists in the document.
77    pub async fn is_attached(&self) -> Result<bool, LocatorError> {
78        #[derive(Debug, serde::Deserialize)]
79        struct CallResult {
80            result: viewpoint_cdp::protocol::runtime::RemoteObject,
81        }
82
83        let result: CallResult = self.page
84            .connection()
85            .send_command(
86                "Runtime.callFunctionOn",
87                Some(serde_json::json!({
88                    "objectId": self.object_id,
89                    "functionDeclaration": "function() { return this.isConnected; }",
90                    "returnByValue": true
91                })),
92                Some(self.page.session_id()),
93            )
94            .await?;
95
96        Ok(result.result.value
97            .and_then(|v| v.as_bool())
98            .unwrap_or(false))
99    }
100
101    /// Evaluate a JavaScript expression with this element as `this`.
102    ///
103    /// # Example
104    ///
105    /// ```no_run
106    /// use viewpoint_core::Page;
107    /// use viewpoint_js::js;
108    ///
109    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
110    /// let handle = page.locator("button").element_handle().await?;
111    /// let text: String = handle.evaluate(js!{ this.textContent }).await?;
112    /// # Ok(())
113    /// # }
114    /// ```
115    pub async fn evaluate<T: serde::de::DeserializeOwned>(
116        &self,
117        expression: &str,
118    ) -> Result<T, LocatorError> {
119        let function = format!("function() {{ return {expression}; }}");
120
121        #[derive(Debug, serde::Deserialize)]
122        struct CallResult {
123            result: viewpoint_cdp::protocol::runtime::RemoteObject,
124            #[serde(rename = "exceptionDetails")]
125            exception_details: Option<viewpoint_cdp::protocol::runtime::ExceptionDetails>,
126        }
127
128        let result: CallResult = self.page
129            .connection()
130            .send_command(
131                "Runtime.callFunctionOn",
132                Some(serde_json::json!({
133                    "objectId": self.object_id,
134                    "functionDeclaration": function,
135                    "returnByValue": true
136                })),
137                Some(self.page.session_id()),
138            )
139            .await?;
140
141        if let Some(exception) = result.exception_details {
142            return Err(LocatorError::EvaluationError(exception.text));
143        }
144
145        let value = result.result.value.unwrap_or(serde_json::Value::Null);
146        serde_json::from_value(value)
147            .map_err(|e| LocatorError::EvaluationError(format!("Failed to deserialize: {e}")))
148    }
149}
150
151/// Box model information for an element.
152#[derive(Debug, Clone, serde::Deserialize)]
153pub struct BoxModel {
154    /// Content box coordinates.
155    pub content: Vec<f64>,
156    /// Padding box coordinates.
157    pub padding: Vec<f64>,
158    /// Border box coordinates.
159    pub border: Vec<f64>,
160    /// Margin box coordinates.
161    pub margin: Vec<f64>,
162    /// Width of the element.
163    pub width: i32,
164    /// Height of the element.
165    pub height: i32,
166}