viewpoint_core/page/locator/evaluation/mod.rs
1//! JavaScript evaluation methods for locators.
2//!
3//! Methods for evaluating JavaScript expressions on elements.
4
5use viewpoint_cdp::protocol::runtime::EvaluateParams;
6use tracing::{debug, instrument};
7
8use super::element::{BoundingBox, ElementHandle};
9use super::Locator;
10use crate::error::LocatorError;
11
12impl<'a> Locator<'a> {
13 /// Evaluate a JavaScript expression with the element as the first argument.
14 ///
15 /// The element is passed as `element` to the expression. The expression
16 /// should be a function body or expression that uses `element`.
17 ///
18 /// # Arguments
19 ///
20 /// * `expression` - JavaScript expression. The element is available as `element`.
21 ///
22 /// # Returns
23 ///
24 /// The result of the JavaScript expression, or an error if evaluation fails.
25 ///
26 /// # Example
27 ///
28 /// ```ignore
29 /// // Get the element's computed style
30 /// let color = page.locator("button")
31 /// .evaluate::<String>("getComputedStyle(element).color")
32 /// .await?;
33 ///
34 /// // Get element dimensions
35 /// let rect = page.locator("button")
36 /// .evaluate::<serde_json::Value>("element.getBoundingClientRect()")
37 /// .await?;
38 ///
39 /// // Modify element state
40 /// page.locator("input")
41 /// .evaluate::<()>("element.value = 'Hello'")
42 /// .await?;
43 /// ```
44 ///
45 /// # Errors
46 ///
47 /// Returns an error if:
48 /// - The element is not found
49 /// - The JavaScript expression fails
50 /// - The result cannot be deserialized to type `T`
51 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
52 pub async fn evaluate<T: serde::de::DeserializeOwned>(
53 &self,
54 expression: &str,
55 ) -> Result<T, LocatorError> {
56 self.wait_for_actionable().await?;
57
58 debug!(expression, "Evaluating expression on element");
59
60 let js = format!(
61 r"(function() {{
62 const elements = {selector};
63 if (elements.length === 0) return {{ __viewpoint_error: 'Element not found' }};
64
65 const element = elements[0];
66 try {{
67 const result = (function(element) {{ return {expression}; }})(element);
68 return {{ __viewpoint_result: result }};
69 }} catch (e) {{
70 return {{ __viewpoint_error: e.toString() }};
71 }}
72 }})()",
73 selector = self.selector.to_js_expression(),
74 expression = expression
75 );
76
77 let result = self.evaluate_js(&js).await?;
78
79 if let Some(error) = result.get("__viewpoint_error").and_then(|v| v.as_str()) {
80 return Err(LocatorError::EvaluationError(error.to_string()));
81 }
82
83 let value = result.get("__viewpoint_result").cloned().unwrap_or(serde_json::Value::Null);
84 serde_json::from_value(value)
85 .map_err(|e| LocatorError::EvaluationError(format!("Failed to deserialize result: {e}")))
86 }
87
88 /// Evaluate a JavaScript expression on all matching elements.
89 ///
90 /// The elements are passed as `elements` (an array) to the expression.
91 ///
92 /// # Arguments
93 ///
94 /// * `expression` - JavaScript expression. The elements are available as `elements`.
95 ///
96 /// # Returns
97 ///
98 /// The result of the JavaScript expression, or an error if evaluation fails.
99 ///
100 /// # Example
101 ///
102 /// ```ignore
103 /// // Get all element IDs
104 /// let ids = page.locator("button")
105 /// .evaluate_all::<Vec<String>>("elements.map(e => e.id)")
106 /// .await?;
107 ///
108 /// // Count visible elements
109 /// let count = page.locator(".item")
110 /// .evaluate_all::<usize>("elements.filter(e => e.offsetParent !== null).length")
111 /// .await?;
112 ///
113 /// // Get custom data attributes
114 /// let data = page.locator("[data-test]")
115 /// .evaluate_all::<Vec<String>>("elements.map(e => e.dataset.test)")
116 /// .await?;
117 /// ```
118 ///
119 /// # Errors
120 ///
121 /// Returns an error if:
122 /// - The JavaScript expression fails
123 /// - The result cannot be deserialized to type `T`
124 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
125 pub async fn evaluate_all<T: serde::de::DeserializeOwned>(
126 &self,
127 expression: &str,
128 ) -> Result<T, LocatorError> {
129 debug!(expression, "Evaluating expression on all elements");
130
131 let js = format!(
132 r"(function() {{
133 const elements = Array.from({selector});
134 try {{
135 const result = (function(elements) {{ return {expression}; }})(elements);
136 return {{ __viewpoint_result: result }};
137 }} catch (e) {{
138 return {{ __viewpoint_error: e.toString() }};
139 }}
140 }})()",
141 selector = self.selector.to_js_expression(),
142 expression = expression
143 );
144
145 let result = self.evaluate_js(&js).await?;
146
147 if let Some(error) = result.get("__viewpoint_error").and_then(|v| v.as_str()) {
148 return Err(LocatorError::EvaluationError(error.to_string()));
149 }
150
151 let value = result.get("__viewpoint_result").cloned().unwrap_or(serde_json::Value::Null);
152 serde_json::from_value(value)
153 .map_err(|e| LocatorError::EvaluationError(format!("Failed to deserialize result: {e}")))
154 }
155
156 /// Get a raw element handle for the first matching element.
157 ///
158 /// The returned [`ElementHandle`] provides lower-level access to the DOM element
159 /// and can be used for advanced operations that aren't covered by the Locator API.
160 ///
161 /// **Note:** Unlike locators, element handles are bound to the specific element
162 /// at the time of creation. If the element is removed from the DOM, the handle
163 /// becomes stale.
164 ///
165 /// # Example
166 ///
167 /// ```ignore
168 /// let handle = page.locator("button").element_handle().await?;
169 /// let box_model = handle.box_model().await?;
170 /// println!("Element at: {:?}", box_model);
171 /// ```
172 ///
173 /// # Errors
174 ///
175 /// Returns an error if the element cannot be found.
176 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
177 pub async fn element_handle(&self) -> Result<ElementHandle<'a>, LocatorError> {
178 self.wait_for_actionable().await?;
179
180 debug!("Getting element handle");
181
182 // Use Runtime.evaluate to get the element object ID
183 let js = format!(
184 r"(function() {{
185 const elements = {selector};
186 if (elements.length === 0) return null;
187 return elements[0];
188 }})()",
189 selector = self.selector.to_js_expression()
190 );
191
192 let params = EvaluateParams {
193 expression: js,
194 object_group: Some("viewpoint-element-handle".to_string()),
195 include_command_line_api: None,
196 silent: Some(true),
197 context_id: None,
198 return_by_value: Some(false),
199 await_promise: Some(false),
200 };
201
202 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
203 .page
204 .connection()
205 .send_command("Runtime.evaluate", Some(params), Some(self.page.session_id()))
206 .await?;
207
208 if let Some(exception) = result.exception_details {
209 return Err(LocatorError::EvaluationError(exception.text));
210 }
211
212 let object_id = result.result.object_id
213 .ok_or_else(|| LocatorError::NotFound(format!("{:?}", self.selector)))?;
214
215 Ok(ElementHandle {
216 object_id,
217 page: self.page,
218 })
219 }
220
221 /// Scroll the element into view if needed.
222 ///
223 /// This scrolls the element's parent container(s) to make the element visible.
224 ///
225 /// # Example
226 ///
227 /// ```ignore
228 /// page.locator(".footer").scroll_into_view_if_needed().await?;
229 /// ```
230 ///
231 /// # Errors
232 ///
233 /// Returns an error if the element cannot be found.
234 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
235 pub async fn scroll_into_view_if_needed(&self) -> Result<(), LocatorError> {
236 let _info = self.wait_for_actionable().await?;
237
238 debug!("Scrolling element into view");
239
240 let js = format!(
241 r"(function() {{
242 const elements = {selector};
243 if (elements.length === 0) return {{ found: false }};
244
245 const el = elements[0];
246 el.scrollIntoView({{ behavior: 'instant', block: 'center', inline: 'center' }});
247 return {{ found: true }};
248 }})()",
249 selector = self.selector.to_js_expression()
250 );
251
252 let result = self.evaluate_js(&js).await?;
253 let found = result.get("found").and_then(serde_json::Value::as_bool).unwrap_or(false);
254 if !found {
255 return Err(LocatorError::NotFound(format!("{:?}", self.selector)));
256 }
257
258 Ok(())
259 }
260
261 /// Get the bounding box of the element.
262 ///
263 /// Returns the element's position and dimensions relative to the viewport.
264 ///
265 /// # Example
266 ///
267 /// ```ignore
268 /// let bbox = page.locator("button").bounding_box().await?;
269 /// if let Some(box_) = bbox {
270 /// println!("Element at ({}, {}), size {}x{}",
271 /// box_.x, box_.y, box_.width, box_.height);
272 /// }
273 /// ```
274 ///
275 /// # Returns
276 ///
277 /// - `Some(BoundingBox)` if the element exists and is visible
278 /// - `None` if the element exists but has no visible bounding box
279 ///
280 /// # Errors
281 ///
282 /// Returns an error if the element cannot be found.
283 pub async fn bounding_box(&self) -> Result<Option<BoundingBox>, LocatorError> {
284 let info = self.query_element_info().await?;
285
286 if !info.found {
287 return Err(LocatorError::NotFound(format!("{:?}", self.selector)));
288 }
289
290 match (info.x, info.y, info.width, info.height) {
291 (Some(x), Some(y), Some(width), Some(height)) if width > 0.0 && height > 0.0 => {
292 Ok(Some(BoundingBox { x, y, width, height }))
293 }
294 _ => Ok(None),
295 }
296 }
297}