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::Page;
7use crate::error::LocatorError;
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
61            .page
62            .connection()
63            .send_command(
64                "DOM.getBoxModel",
65                Some(serde_json::json!({
66                    "objectId": self.object_id
67                })),
68                Some(self.page.session_id()),
69            )
70            .await?;
71
72        Ok(result.model)
73    }
74
75    /// Check if the element is still attached to the DOM.
76    ///
77    /// Returns `true` if the element still exists in the document.
78    pub async fn is_attached(&self) -> Result<bool, LocatorError> {
79        #[derive(Debug, serde::Deserialize)]
80        struct CallResult {
81            result: viewpoint_cdp::protocol::runtime::RemoteObject,
82        }
83
84        let result: CallResult = self
85            .page
86            .connection()
87            .send_command(
88                "Runtime.callFunctionOn",
89                Some(serde_json::json!({
90                    "objectId": self.object_id,
91                    "functionDeclaration": "function() { return this.isConnected; }",
92                    "returnByValue": true
93                })),
94                Some(self.page.session_id()),
95            )
96            .await?;
97
98        Ok(result
99            .result
100            .value
101            .and_then(|v| v.as_bool())
102            .unwrap_or(false))
103    }
104
105    /// Evaluate a JavaScript expression with this element as `this`.
106    ///
107    /// # Example
108    ///
109    /// ```no_run
110    /// use viewpoint_core::Page;
111    /// use viewpoint_js::js;
112    ///
113    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
114    /// let handle = page.locator("button").element_handle().await?;
115    /// let text: String = handle.evaluate(js!{ this.textContent }).await?;
116    /// # Ok(())
117    /// # }
118    /// ```
119    pub async fn evaluate<T: serde::de::DeserializeOwned>(
120        &self,
121        expression: &str,
122    ) -> Result<T, LocatorError> {
123        let function = format!("function() {{ return {expression}; }}");
124
125        #[derive(Debug, serde::Deserialize)]
126        struct CallResult {
127            result: viewpoint_cdp::protocol::runtime::RemoteObject,
128            #[serde(rename = "exceptionDetails")]
129            exception_details: Option<viewpoint_cdp::protocol::runtime::ExceptionDetails>,
130        }
131
132        let result: CallResult = self
133            .page
134            .connection()
135            .send_command(
136                "Runtime.callFunctionOn",
137                Some(serde_json::json!({
138                    "objectId": self.object_id,
139                    "functionDeclaration": function,
140                    "returnByValue": true
141                })),
142                Some(self.page.session_id()),
143            )
144            .await?;
145
146        if let Some(exception) = result.exception_details {
147            return Err(LocatorError::EvaluationError(exception.text));
148        }
149
150        let value = result.result.value.unwrap_or(serde_json::Value::Null);
151        serde_json::from_value(value)
152            .map_err(|e| LocatorError::EvaluationError(format!("Failed to deserialize: {e}")))
153    }
154}
155
156/// Box model information for an element.
157#[derive(Debug, Clone, serde::Deserialize)]
158pub struct BoxModel {
159    /// Content box coordinates.
160    pub content: Vec<f64>,
161    /// Padding box coordinates.
162    pub padding: Vec<f64>,
163    /// Border box coordinates.
164    pub border: Vec<f64>,
165    /// Margin box coordinates.
166    pub margin: Vec<f64>,
167    /// Width of the element.
168    pub width: i32,
169    /// Height of the element.
170    pub height: i32,
171}