viewpoint_core/page/evaluate/wait/
mod.rs

1//! Wait for function functionality.
2//!
3//! This module provides waiting for JavaScript conditions to become truthy.
4
5use std::time::Duration;
6
7use serde::Serialize;
8use tracing::{debug, instrument};
9use viewpoint_cdp::protocol::runtime::{EvaluateParams, EvaluateResult, ReleaseObjectParams};
10
11use crate::error::PageError;
12use crate::page::Page;
13
14use super::{DEFAULT_TIMEOUT, JsHandle, wrap_expression};
15
16/// Polling mode for `wait_for_function`.
17#[derive(Debug, Clone, Copy, Default)]
18pub enum Polling {
19    /// Poll on requestAnimationFrame (default).
20    #[default]
21    Raf,
22    /// Poll at a fixed interval.
23    Interval(Duration),
24}
25
26/// Builder for `wait_for_function`.
27#[derive(Debug)]
28pub struct WaitForFunctionBuilder<'a> {
29    page: &'a Page,
30    expression: String,
31    arg: Option<serde_json::Value>,
32    timeout: Duration,
33    polling: Polling,
34}
35
36impl<'a> WaitForFunctionBuilder<'a> {
37    /// Create a new builder.
38    pub(crate) fn new(page: &'a Page, expression: String) -> Self {
39        Self {
40            page,
41            expression,
42            arg: None,
43            timeout: DEFAULT_TIMEOUT,
44            polling: Polling::default(),
45        }
46    }
47
48    /// Set an argument to pass to the function.
49    #[must_use]
50    pub fn arg<A: Serialize>(mut self, arg: A) -> Self {
51        self.arg = serde_json::to_value(arg).ok();
52        self
53    }
54
55    /// Set the timeout.
56    #[must_use]
57    pub fn timeout(mut self, timeout: Duration) -> Self {
58        self.timeout = timeout;
59        self
60    }
61
62    /// Set the polling mode.
63    #[must_use]
64    pub fn polling(mut self, polling: Polling) -> Self {
65        self.polling = polling;
66        self
67    }
68
69    /// Wait for the function to return a truthy value.
70    ///
71    /// Returns a handle to the truthy result.
72    #[instrument(level = "debug", skip(self), fields(expression = %self.expression, timeout_ms = self.timeout.as_millis()))]
73    pub async fn wait(self) -> Result<JsHandle, PageError> {
74        if self.page.closed {
75            return Err(PageError::Closed);
76        }
77
78        let start = std::time::Instant::now();
79
80        debug!("Starting wait_for_function polling with {:?}", self.polling);
81
82        loop {
83            if start.elapsed() >= self.timeout {
84                return Err(PageError::EvaluationFailed(format!(
85                    "Timeout {}ms exceeded waiting for function",
86                    self.timeout.as_millis()
87                )));
88            }
89
90            // Try to evaluate the expression
91            let result = self.try_evaluate().await?;
92
93            if result.is_truthy {
94                debug!("Function returned truthy value");
95                return Ok(result.handle.expect("truthy result has handle"));
96            }
97
98            // Wait according to polling mode
99            match self.polling {
100                Polling::Raf => {
101                    // Approximate RAF timing
102                    tokio::time::sleep(Duration::from_millis(16)).await;
103                }
104                Polling::Interval(duration) => {
105                    tokio::time::sleep(duration).await;
106                }
107            }
108        }
109    }
110
111    /// Try to evaluate the function and check if result is truthy.
112    async fn try_evaluate(&self) -> Result<TryResult, PageError> {
113        let expression = if let Some(ref arg) = self.arg {
114            let arg_json = serde_json::to_string(arg)
115                .map_err(|e| PageError::EvaluationFailed(e.to_string()))?;
116            format!("({})({})", self.expression, arg_json)
117        } else {
118            wrap_expression(&self.expression)
119        };
120
121        let params = EvaluateParams {
122            expression,
123            object_group: Some("viewpoint-wait".to_string()),
124            include_command_line_api: None,
125            silent: Some(false),
126            context_id: None,
127            return_by_value: Some(false),
128            await_promise: Some(true),
129        };
130
131        let result: EvaluateResult = self
132            .page
133            .connection
134            .send_command(
135                "Runtime.evaluate",
136                Some(params),
137                Some(&self.page.session_id),
138            )
139            .await?;
140
141        if let Some(exception) = result.exception_details {
142            return Err(PageError::EvaluationFailed(exception.text));
143        }
144
145        // Check if the result is truthy
146        let is_truthy = is_truthy_result(&result.result);
147
148        let handle = if is_truthy {
149            result.result.object_id.map(|id| {
150                JsHandle::new(
151                    id,
152                    self.page.session_id.clone(),
153                    self.page.connection.clone(),
154                )
155            })
156        } else {
157            // Release non-truthy object references
158            if let Some(object_id) = result.result.object_id {
159                let _ = self
160                    .page
161                    .connection
162                    .send_command::<_, serde_json::Value>(
163                        "Runtime.releaseObject",
164                        Some(ReleaseObjectParams { object_id }),
165                        Some(&self.page.session_id),
166                    )
167                    .await;
168            }
169            None
170        };
171
172        Ok(TryResult { is_truthy, handle })
173    }
174}
175
176struct TryResult {
177    is_truthy: bool,
178    handle: Option<JsHandle>,
179}
180
181/// Check if a `RemoteObject` represents a truthy value.
182fn is_truthy_result(result: &viewpoint_cdp::protocol::runtime::RemoteObject) -> bool {
183    // Check by type
184    match result.object_type.as_str() {
185        "undefined" => false,
186        "object" => {
187            // Null is falsy, other objects are truthy
188            result.subtype.as_deref() != Some("null")
189        }
190        "boolean" => result
191            .value
192            .as_ref()
193            .and_then(serde_json::Value::as_bool)
194            .unwrap_or(false),
195        "number" => result
196            .value
197            .as_ref()
198            .and_then(serde_json::Value::as_f64)
199            .is_some_and(|n| n != 0.0 && !n.is_nan()),
200        "string" => result
201            .value
202            .as_ref()
203            .and_then(|v| v.as_str())
204            .is_some_and(|s| !s.is_empty()),
205        _ => {
206            // Functions, symbols, bigints are truthy
207            true
208        }
209    }
210}
211
212impl Page {
213    /// Wait for a JavaScript function to return a truthy value.
214    ///
215    /// # Example
216    ///
217    /// ```no_run
218    /// use std::time::Duration;
219    /// use viewpoint_core::page::Polling;
220    /// use viewpoint_js::js;
221    ///
222    /// # async fn example(page: viewpoint_core::Page) -> Result<(), viewpoint_core::CoreError> {
223    /// // Wait for an element to appear
224    /// let selector = ".loaded";
225    /// page.wait_for_function(js!{ () => document.querySelector(#{selector}) })
226    ///     .wait()
227    ///     .await?;
228    ///
229    /// // Wait with custom timeout
230    /// page.wait_for_function(js!{ () => window.ready })
231    ///     .timeout(Duration::from_secs(10))
232    ///     .wait()
233    ///     .await?;
234    ///
235    /// // Wait with interval polling
236    /// page.wait_for_function(js!{ () => window.ready })
237    ///     .polling(Polling::Interval(Duration::from_millis(100)))
238    ///     .wait()
239    ///     .await?;
240    /// # Ok(())
241    /// # }
242    /// ```
243    pub fn wait_for_function(&self, expression: impl Into<String>) -> WaitForFunctionBuilder<'_> {
244        WaitForFunctionBuilder::new(self, expression.into())
245    }
246
247    /// Wait for a JavaScript function with an argument to return a truthy value.
248    pub fn wait_for_function_with_arg<A: Serialize>(
249        &self,
250        expression: impl Into<String>,
251        arg: A,
252    ) -> WaitForFunctionBuilder<'_> {
253        WaitForFunctionBuilder::new(self, expression.into()).arg(arg)
254    }
255}