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 `Ok(Some(JsHandle))` when the result is a truthy object (DOM elements, objects, arrays).
72    /// Returns `Ok(None)` when the result is a truthy primitive (booleans, numbers, strings).
73    /// Primitive values have no object handle in the JavaScript runtime.
74    ///
75    /// # Example
76    ///
77    /// ```no_run
78    /// # async fn example(page: &viewpoint_core::Page) -> Result<(), viewpoint_core::CoreError> {
79    /// // Wait for a condition - primitives return None
80    /// let _handle = page.wait_for_function("() => document.body.innerText.includes('loaded')")
81    ///     .wait()
82    ///     .await?;
83    /// // handle is None because `.includes()` returns a boolean
84    ///
85    /// // Wait for an element - objects return Some(JsHandle)
86    /// let handle = page.wait_for_function("() => document.querySelector('.ready')")
87    ///     .wait()
88    ///     .await?;
89    /// // handle is Some(JsHandle) referencing the element
90    /// # Ok(())
91    /// # }
92    /// ```
93    #[instrument(level = "debug", skip(self), fields(expression = %self.expression, timeout_ms = self.timeout.as_millis()))]
94    pub async fn wait(self) -> Result<Option<JsHandle>, PageError> {
95        if self.page.closed {
96            return Err(PageError::Closed);
97        }
98
99        let start = std::time::Instant::now();
100
101        debug!("Starting wait_for_function polling with {:?}", self.polling);
102
103        loop {
104            if start.elapsed() >= self.timeout {
105                return Err(PageError::EvaluationFailed(format!(
106                    "Timeout {}ms exceeded waiting for function",
107                    self.timeout.as_millis()
108                )));
109            }
110
111            // Try to evaluate the expression
112            let result = self.try_evaluate().await?;
113
114            if result.is_truthy {
115                debug!(
116                    "Function returned truthy value (handle: {})",
117                    if result.handle.is_some() {
118                        "present"
119                    } else {
120                        "none (primitive)"
121                    }
122                );
123                return Ok(result.handle);
124            }
125
126            // Wait according to polling mode
127            match self.polling {
128                Polling::Raf => {
129                    // Approximate RAF timing
130                    tokio::time::sleep(Duration::from_millis(16)).await;
131                }
132                Polling::Interval(duration) => {
133                    tokio::time::sleep(duration).await;
134                }
135            }
136        }
137    }
138
139    /// Try to evaluate the function and check if result is truthy.
140    async fn try_evaluate(&self) -> Result<TryResult, PageError> {
141        let expression = if let Some(ref arg) = self.arg {
142            let arg_json = serde_json::to_string(arg)
143                .map_err(|e| PageError::EvaluationFailed(e.to_string()))?;
144            format!("({})({})", self.expression, arg_json)
145        } else {
146            wrap_expression(&self.expression)
147        };
148
149        let params = EvaluateParams {
150            expression,
151            object_group: Some("viewpoint-wait".to_string()),
152            include_command_line_api: None,
153            silent: Some(false),
154            context_id: None,
155            return_by_value: Some(false),
156            await_promise: Some(true),
157        };
158
159        let result: EvaluateResult = self
160            .page
161            .connection
162            .send_command(
163                "Runtime.evaluate",
164                Some(params),
165                Some(&self.page.session_id),
166            )
167            .await?;
168
169        if let Some(exception) = result.exception_details {
170            return Err(PageError::EvaluationFailed(exception.text));
171        }
172
173        // Check if the result is truthy
174        let is_truthy = is_truthy_result(&result.result);
175
176        let handle = if is_truthy {
177            result.result.object_id.map(|id| {
178                JsHandle::new(
179                    id,
180                    self.page.session_id.clone(),
181                    self.page.connection.clone(),
182                )
183            })
184        } else {
185            // Release non-truthy object references
186            if let Some(object_id) = result.result.object_id {
187                let _ = self
188                    .page
189                    .connection
190                    .send_command::<_, serde_json::Value>(
191                        "Runtime.releaseObject",
192                        Some(ReleaseObjectParams { object_id }),
193                        Some(&self.page.session_id),
194                    )
195                    .await;
196            }
197            None
198        };
199
200        Ok(TryResult { is_truthy, handle })
201    }
202}
203
204struct TryResult {
205    is_truthy: bool,
206    handle: Option<JsHandle>,
207}
208
209/// Check if a `RemoteObject` represents a truthy value.
210fn is_truthy_result(result: &viewpoint_cdp::protocol::runtime::RemoteObject) -> bool {
211    // Check by type
212    match result.object_type.as_str() {
213        "undefined" => false,
214        "object" => {
215            // Null is falsy, other objects are truthy
216            result.subtype.as_deref() != Some("null")
217        }
218        "boolean" => result
219            .value
220            .as_ref()
221            .and_then(serde_json::Value::as_bool)
222            .unwrap_or(false),
223        "number" => result
224            .value
225            .as_ref()
226            .and_then(serde_json::Value::as_f64)
227            .is_some_and(|n| n != 0.0 && !n.is_nan()),
228        "string" => result
229            .value
230            .as_ref()
231            .and_then(|v| v.as_str())
232            .is_some_and(|s| !s.is_empty()),
233        _ => {
234            // Functions, symbols, bigints are truthy
235            true
236        }
237    }
238}
239
240impl Page {
241    /// Wait for a JavaScript function to return a truthy value.
242    ///
243    /// # Example
244    ///
245    /// ```no_run
246    /// use std::time::Duration;
247    /// use viewpoint_core::page::Polling;
248    /// use viewpoint_js::js;
249    ///
250    /// # async fn example(page: viewpoint_core::Page) -> Result<(), viewpoint_core::CoreError> {
251    /// // Wait for an element to appear
252    /// let selector = ".loaded";
253    /// page.wait_for_function(js!{ () => document.querySelector(#{selector}) })
254    ///     .wait()
255    ///     .await?;
256    ///
257    /// // Wait with custom timeout
258    /// page.wait_for_function(js!{ () => window.ready })
259    ///     .timeout(Duration::from_secs(10))
260    ///     .wait()
261    ///     .await?;
262    ///
263    /// // Wait with interval polling
264    /// page.wait_for_function(js!{ () => window.ready })
265    ///     .polling(Polling::Interval(Duration::from_millis(100)))
266    ///     .wait()
267    ///     .await?;
268    /// # Ok(())
269    /// # }
270    /// ```
271    pub fn wait_for_function(&self, expression: impl Into<String>) -> WaitForFunctionBuilder<'_> {
272        WaitForFunctionBuilder::new(self, expression.into())
273    }
274
275    /// Wait for a JavaScript function with an argument to return a truthy value.
276    pub fn wait_for_function_with_arg<A: Serialize>(
277        &self,
278        expression: impl Into<String>,
279        arg: A,
280    ) -> WaitForFunctionBuilder<'_> {
281        WaitForFunctionBuilder::new(self, expression.into()).arg(arg)
282    }
283}