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}