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() {
118 "present"
119 } else {
120 "none (primitive)"
121 }
122 );
123 return Ok(result.handle);
124 }
125
126 match self.polling {
128 Polling::Raf => {
129 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 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 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 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
209fn is_truthy_result(result: &viewpoint_cdp::protocol::runtime::RemoteObject) -> bool {
211 match result.object_type.as_str() {
213 "undefined" => false,
214 "object" => {
215 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 true
236 }
237 }
238}
239
240impl Page {
241 pub fn wait_for_function(&self, expression: impl Into<String>) -> WaitForFunctionBuilder<'_> {
272 WaitForFunctionBuilder::new(self, expression.into())
273 }
274
275 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}