viewpoint_core/page/evaluate/wait/
mod.rs1use 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#[derive(Debug, Clone, Copy, Default)]
18pub enum Polling {
19 #[default]
21 Raf,
22 Interval(Duration),
24}
25
26#[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 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 #[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 #[must_use]
57 pub fn timeout(mut self, timeout: Duration) -> Self {
58 self.timeout = timeout;
59 self
60 }
61
62 #[must_use]
64 pub fn polling(mut self, polling: Polling) -> Self {
65 self.polling = polling;
66 self
67 }
68
69 #[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 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() { "present" } else { "none (primitive)" }
118 );
119 return Ok(result.handle);
120 }
121
122 match self.polling {
124 Polling::Raf => {
125 tokio::time::sleep(Duration::from_millis(16)).await;
127 }
128 Polling::Interval(duration) => {
129 tokio::time::sleep(duration).await;
130 }
131 }
132 }
133 }
134
135 async fn try_evaluate(&self) -> Result<TryResult, PageError> {
137 let expression = if let Some(ref arg) = self.arg {
138 let arg_json = serde_json::to_string(arg)
139 .map_err(|e| PageError::EvaluationFailed(e.to_string()))?;
140 format!("({})({})", self.expression, arg_json)
141 } else {
142 wrap_expression(&self.expression)
143 };
144
145 let params = EvaluateParams {
146 expression,
147 object_group: Some("viewpoint-wait".to_string()),
148 include_command_line_api: None,
149 silent: Some(false),
150 context_id: None,
151 return_by_value: Some(false),
152 await_promise: Some(true),
153 };
154
155 let result: EvaluateResult = self
156 .page
157 .connection
158 .send_command(
159 "Runtime.evaluate",
160 Some(params),
161 Some(&self.page.session_id),
162 )
163 .await?;
164
165 if let Some(exception) = result.exception_details {
166 return Err(PageError::EvaluationFailed(exception.text));
167 }
168
169 let is_truthy = is_truthy_result(&result.result);
171
172 let handle = if is_truthy {
173 result.result.object_id.map(|id| {
174 JsHandle::new(
175 id,
176 self.page.session_id.clone(),
177 self.page.connection.clone(),
178 )
179 })
180 } else {
181 if let Some(object_id) = result.result.object_id {
183 let _ = self
184 .page
185 .connection
186 .send_command::<_, serde_json::Value>(
187 "Runtime.releaseObject",
188 Some(ReleaseObjectParams { object_id }),
189 Some(&self.page.session_id),
190 )
191 .await;
192 }
193 None
194 };
195
196 Ok(TryResult { is_truthy, handle })
197 }
198}
199
200struct TryResult {
201 is_truthy: bool,
202 handle: Option<JsHandle>,
203}
204
205fn is_truthy_result(result: &viewpoint_cdp::protocol::runtime::RemoteObject) -> bool {
207 match result.object_type.as_str() {
209 "undefined" => false,
210 "object" => {
211 result.subtype.as_deref() != Some("null")
213 }
214 "boolean" => result
215 .value
216 .as_ref()
217 .and_then(serde_json::Value::as_bool)
218 .unwrap_or(false),
219 "number" => result
220 .value
221 .as_ref()
222 .and_then(serde_json::Value::as_f64)
223 .is_some_and(|n| n != 0.0 && !n.is_nan()),
224 "string" => result
225 .value
226 .as_ref()
227 .and_then(|v| v.as_str())
228 .is_some_and(|s| !s.is_empty()),
229 _ => {
230 true
232 }
233 }
234}
235
236impl Page {
237 pub fn wait_for_function(&self, expression: impl Into<String>) -> WaitForFunctionBuilder<'_> {
268 WaitForFunctionBuilder::new(self, expression.into())
269 }
270
271 pub fn wait_for_function_with_arg<A: Serialize>(
273 &self,
274 expression: impl Into<String>,
275 arg: A,
276 ) -> WaitForFunctionBuilder<'_> {
277 WaitForFunctionBuilder::new(self, expression.into()).arg(arg)
278 }
279}