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}