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    /// ```ignore
106    /// use viewpoint_js::js;
107    /// let handle = page.locator("button").element_handle().await?;
108    /// let text: String = handle.evaluate(js!{ this.textContent }).await?;
109    /// ```
110    pub async fn evaluate<T: serde::de::DeserializeOwned>(
111        &self,
112        expression: &str,
113    ) -> Result<T, LocatorError> {
114        let function = format!("function() {{ return {expression}; }}");
115
116        #[derive(Debug, serde::Deserialize)]
117        struct CallResult {
118            result: viewpoint_cdp::protocol::runtime::RemoteObject,
119            #[serde(rename = "exceptionDetails")]
120            exception_details: Option<viewpoint_cdp::protocol::runtime::ExceptionDetails>,
121        }
122
123        let result: CallResult = self.page
124            .connection()
125            .send_command(
126                "Runtime.callFunctionOn",
127                Some(serde_json::json!({
128                    "objectId": self.object_id,
129                    "functionDeclaration": function,
130                    "returnByValue": true
131                })),
132                Some(self.page.session_id()),
133            )
134            .await?;
135
136        if let Some(exception) = result.exception_details {
137            return Err(LocatorError::EvaluationError(exception.text));
138        }
139
140        let value = result.result.value.unwrap_or(serde_json::Value::Null);
141        serde_json::from_value(value)
142            .map_err(|e| LocatorError::EvaluationError(format!("Failed to deserialize: {e}")))
143    }
144}
145
146/// Box model information for an element.
147#[derive(Debug, Clone, serde::Deserialize)]
148pub struct BoxModel {
149    /// Content box coordinates.
150    pub content: Vec<f64>,
151    /// Padding box coordinates.
152    pub padding: Vec<f64>,
153    /// Border box coordinates.
154    pub border: Vec<f64>,
155    /// Margin box coordinates.
156    pub margin: Vec<f64>,
157    /// Width of the element.
158    pub width: i32,
159    /// Height of the element.
160    pub height: i32,
161}