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}