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}