viewpoint_core/page/locator/evaluation/
evaluate.rs

1//! Single element evaluation methods.
2
3use serde::Deserialize;
4use tracing::{debug, instrument};
5use viewpoint_cdp::protocol::dom::{BackendNodeId, ResolveNodeParams, ResolveNodeResult};
6use viewpoint_js::js;
7
8use super::super::Locator;
9use super::super::Selector;
10use crate::error::LocatorError;
11
12impl Locator<'_> {
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    /// ```no_run
29    /// use viewpoint_core::Page;
30    ///
31    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
32    /// // Get the element's computed style
33    /// let color = page.locator("button")
34    ///     .evaluate::<String>("getComputedStyle(element).color")
35    ///     .await?;
36    ///
37    /// // Get element dimensions
38    /// let rect = page.locator("button")
39    ///     .evaluate::<serde_json::Value>("element.getBoundingClientRect()")
40    ///     .await?;
41    ///
42    /// // Modify element state
43    /// page.locator("input")
44    ///     .evaluate::<()>("element.value = 'Hello'")
45    ///     .await?;
46    /// # Ok(())
47    /// # }
48    /// ```
49    ///
50    /// # Errors
51    ///
52    /// Returns an error if:
53    /// - The element is not found
54    /// - The JavaScript expression fails
55    /// - The result cannot be deserialized to type `T`
56    #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
57    pub async fn evaluate<T: serde::de::DeserializeOwned>(
58        &self,
59        expression: &str,
60    ) -> Result<T, LocatorError> {
61        self.wait_for_actionable().await?;
62
63        debug!(expression, "Evaluating expression on element");
64
65        // Handle Ref selector - lookup in ref map and resolve via CDP
66        if let Selector::Ref(ref_str) = &self.selector {
67            let backend_node_id = self.page.get_backend_node_id_for_ref(ref_str)?;
68            return self
69                .evaluate_by_backend_id(backend_node_id, expression)
70                .await;
71        }
72
73        // Handle BackendNodeId selector
74        if let Selector::BackendNodeId(backend_node_id) = &self.selector {
75            return self
76                .evaluate_by_backend_id(*backend_node_id, expression)
77                .await;
78        }
79
80        let selector_expr = self.selector.to_js_expression();
81        let js = js! {
82            (function() {
83                const elements = @{selector_expr};
84                if (elements.length === 0) return { __viewpoint_error: "Element not found" };
85
86                const element = elements[0];
87                try {
88                    const result = (function(element) { return @{expression}; })(element);
89                    return { __viewpoint_result: result };
90                } catch (e) {
91                    return { __viewpoint_error: e.toString() };
92                }
93            })()
94        };
95
96        let result = self.evaluate_js(&js).await?;
97
98        if let Some(error) = result.get("__viewpoint_error").and_then(|v| v.as_str()) {
99            return Err(LocatorError::EvaluationError(error.to_string()));
100        }
101
102        let value = result
103            .get("__viewpoint_result")
104            .cloned()
105            .unwrap_or(serde_json::Value::Null);
106        serde_json::from_value(value).map_err(|e| {
107            LocatorError::EvaluationError(format!("Failed to deserialize result: {e}"))
108        })
109    }
110
111    /// Evaluate a JavaScript expression on an element by backend node ID.
112    pub(super) async fn evaluate_by_backend_id<T: serde::de::DeserializeOwned>(
113        &self,
114        backend_node_id: BackendNodeId,
115        expression: &str,
116    ) -> Result<T, LocatorError> {
117        // Resolve the backend node ID to a RemoteObject
118        let result: ResolveNodeResult = self
119            .page
120            .connection()
121            .send_command(
122                "DOM.resolveNode",
123                Some(ResolveNodeParams {
124                    node_id: None,
125                    backend_node_id: Some(backend_node_id),
126                    object_group: Some("viewpoint-evaluate".to_string()),
127                    execution_context_id: None,
128                }),
129                Some(self.page.session_id()),
130            )
131            .await
132            .map_err(|_| {
133                LocatorError::NotFound(format!(
134                    "Could not resolve backend node ID {backend_node_id}: element may no longer exist"
135                ))
136            })?;
137
138        let object_id = result.object.object_id.ok_or_else(|| {
139            LocatorError::NotFound(format!(
140                "No object ID for backend node ID {backend_node_id}"
141            ))
142        })?;
143
144        // Call the function on the resolved element
145        #[derive(Debug, Deserialize)]
146        struct CallResult {
147            result: viewpoint_cdp::protocol::runtime::RemoteObject,
148            #[serde(rename = "exceptionDetails")]
149            exception_details: Option<viewpoint_cdp::protocol::runtime::ExceptionDetails>,
150        }
151
152        // Build function declaration for CDP callFunctionOn
153        // Wrapping in parens makes it a valid expression for js! macro parsing
154        let js_fn = js! {
155            (function() {
156                const element = this;
157                try {
158                    const result = (function(element) { return @{expression}; })(element);
159                    return { __viewpoint_result: result };
160                } catch (e) {
161                    return { __viewpoint_error: e.toString() };
162                }
163            })
164        };
165        // Strip outer parentheses for CDP (it expects function declaration syntax)
166        let js_fn = js_fn.trim_start_matches('(').trim_end_matches(')');
167
168        let call_result: CallResult = self
169            .page
170            .connection()
171            .send_command(
172                "Runtime.callFunctionOn",
173                Some(serde_json::json!({
174                    "objectId": object_id,
175                    "functionDeclaration": js_fn,
176                    "returnByValue": true
177                })),
178                Some(self.page.session_id()),
179            )
180            .await?;
181
182        // Release the object
183        let _ = self
184            .page
185            .connection()
186            .send_command::<_, serde_json::Value>(
187                "Runtime.releaseObject",
188                Some(serde_json::json!({ "objectId": object_id })),
189                Some(self.page.session_id()),
190            )
191            .await;
192
193        if let Some(exception) = call_result.exception_details {
194            return Err(LocatorError::EvaluationError(exception.text));
195        }
196
197        let value = call_result
198            .result
199            .value
200            .ok_or_else(|| LocatorError::EvaluationError("No result from evaluate".to_string()))?;
201
202        if let Some(error) = value.get("__viewpoint_error").and_then(|v| v.as_str()) {
203            return Err(LocatorError::EvaluationError(error.to_string()));
204        }
205
206        let result_value = value
207            .get("__viewpoint_result")
208            .cloned()
209            .unwrap_or(serde_json::Value::Null);
210        serde_json::from_value(result_value).map_err(|e| {
211            LocatorError::EvaluationError(format!("Failed to deserialize result: {e}"))
212        })
213    }
214}