viewpoint_core/page/evaluate/
mod.rs1use std::time::Duration;
6
7use serde::{de::DeserializeOwned, Serialize};
8use tracing::{debug, instrument, trace};
9use viewpoint_cdp::protocol::runtime::{
10 CallFunctionOnParams, EvaluateParams, EvaluateResult, ReleaseObjectParams,
11};
12
13use crate::error::PageError;
14
15use super::Page;
16
17mod wait;
18
19pub use wait::{Polling, WaitForFunctionBuilder};
20
21pub(super) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
23
24#[derive(Debug)]
29pub struct JsHandle {
30 object_id: String,
32 page_session_id: String,
34 connection: std::sync::Arc<viewpoint_cdp::CdpConnection>,
36}
37
38impl JsHandle {
39 pub(crate) fn new(
41 object_id: String,
42 page_session_id: String,
43 connection: std::sync::Arc<viewpoint_cdp::CdpConnection>,
44 ) -> Self {
45 Self {
46 object_id,
47 page_session_id,
48 connection,
49 }
50 }
51
52 pub fn object_id(&self) -> &str {
54 &self.object_id
55 }
56
57 pub async fn json_value<T: DeserializeOwned>(&self) -> Result<T, PageError> {
63 let params = CallFunctionOnParams {
64 function_declaration: "function() { return this; }".to_string(),
65 object_id: Some(self.object_id.clone()),
66 arguments: None,
67 silent: Some(false),
68 return_by_value: Some(true),
69 generate_preview: None,
70 user_gesture: None,
71 await_promise: Some(true),
72 execution_context_id: None,
73 object_group: None,
74 throw_on_side_effect: None,
75 unique_context_id: None,
76 serialization_options: None,
77 };
78
79 let result: viewpoint_cdp::protocol::runtime::CallFunctionOnResult = self
80 .connection
81 .send_command(
82 "Runtime.callFunctionOn",
83 Some(params),
84 Some(&self.page_session_id),
85 )
86 .await?;
87
88 if let Some(exception) = result.exception_details {
89 return Err(PageError::EvaluationFailed(exception.text));
90 }
91
92 let value = result.result.value.unwrap_or(serde_json::Value::Null);
94
95 serde_json::from_value(value)
96 .map_err(|e| PageError::EvaluationFailed(format!("Failed to deserialize: {e}")))
97 }
98
99 pub async fn dispose(self) -> Result<(), PageError> {
105 self.connection
106 .send_command::<_, serde_json::Value>(
107 "Runtime.releaseObject",
108 Some(ReleaseObjectParams {
109 object_id: self.object_id,
110 }),
111 Some(&self.page_session_id),
112 )
113 .await?;
114 Ok(())
115 }
116}
117
118impl Page {
119 #[instrument(level = "debug", skip(self), fields(expression = %expression))]
158 pub async fn evaluate<T: DeserializeOwned>(&self, expression: &str) -> Result<T, PageError> {
159 self.evaluate_internal(expression, None, DEFAULT_TIMEOUT)
160 .await
161 }
162
163 #[instrument(level = "debug", skip(self, arg), fields(expression = %expression))]
181 pub async fn evaluate_with_arg<T: DeserializeOwned, A: Serialize>(
182 &self,
183 expression: &str,
184 arg: A,
185 ) -> Result<T, PageError> {
186 let arg_json = serde_json::to_value(arg).map_err(|e| {
187 PageError::EvaluationFailed(format!("Failed to serialize argument: {e}"))
188 })?;
189
190 self.evaluate_internal(expression, Some(arg_json), DEFAULT_TIMEOUT)
191 .await
192 }
193
194 #[instrument(level = "debug", skip(self), fields(expression = %expression))]
215 pub async fn evaluate_handle(&self, expression: &str) -> Result<JsHandle, PageError> {
216 if self.closed {
217 return Err(PageError::Closed);
218 }
219
220 debug!("Evaluating expression for handle");
221
222 let wrapped = wrap_expression(expression);
224
225 let params = EvaluateParams {
226 expression: wrapped,
227 object_group: Some("viewpoint".to_string()),
228 include_command_line_api: None,
229 silent: Some(false),
230 context_id: None,
231 return_by_value: Some(false), await_promise: Some(true),
233 };
234
235 let result: EvaluateResult = self
236 .connection
237 .send_command("Runtime.evaluate", Some(params), Some(&self.session_id))
238 .await?;
239
240 if let Some(exception) = result.exception_details {
241 return Err(PageError::EvaluationFailed(exception.text));
242 }
243
244 let object_id = result
245 .result
246 .object_id
247 .ok_or_else(|| PageError::EvaluationFailed("Result is not an object".to_string()))?;
248
249 Ok(JsHandle::new(
250 object_id,
251 self.session_id.clone(),
252 self.connection.clone(),
253 ))
254 }
255
256 async fn evaluate_internal<T: DeserializeOwned>(
258 &self,
259 expression: &str,
260 arg: Option<serde_json::Value>,
261 _timeout: Duration,
262 ) -> Result<T, PageError> {
263 if self.closed {
264 return Err(PageError::Closed);
265 }
266
267 trace!(expression = expression, "Evaluating JavaScript");
268
269 let final_expression = if let Some(arg_value) = arg {
271 let arg_json = serde_json::to_string(&arg_value)
273 .map_err(|e| PageError::EvaluationFailed(format!("Failed to serialize: {e}")))?;
274
275 format!("({expression})({arg_json})")
276 } else {
277 wrap_expression(expression)
278 };
279
280 let params = EvaluateParams {
281 expression: final_expression,
282 object_group: None,
283 include_command_line_api: None,
284 silent: Some(false),
285 context_id: None,
286 return_by_value: Some(true),
287 await_promise: Some(true),
288 };
289
290 let result: EvaluateResult = self
291 .connection
292 .send_command("Runtime.evaluate", Some(params), Some(&self.session_id))
293 .await?;
294
295 if let Some(exception) = result.exception_details {
296 return Err(PageError::EvaluationFailed(exception.text));
297 }
298
299 let value = result.result.value.unwrap_or(serde_json::Value::Null);
302
303 serde_json::from_value(value)
304 .map_err(|e| PageError::EvaluationFailed(format!("Failed to deserialize: {e}")))
305 }
306}
307
308pub(super) fn wrap_expression(expression: &str) -> String {
310 let trimmed = expression.trim();
311
312 if trimmed.starts_with("()")
314 || trimmed.starts_with("async ()")
315 || trimmed.starts_with("async()")
316 || trimmed.starts_with("function")
317 || trimmed.starts_with("async function")
318 {
319 format!("({trimmed})()")
320 } else {
321 trimmed.to_string()
322 }
323}