1use super::*;
7use std::future::Future;
8use std::pin::Pin;
9
10type VerificationCheckFuture<'a> =
11 Pin<Box<dyn Future<Output = BrowserResult<(bool, String)>> + 'a>>;
12
13impl BrowserSession {
14 pub(crate) async fn evaluate_predicate_once(
15 &self,
16 predicate: &VerificationPredicate,
17 ) -> BrowserResult<(bool, String)> {
18 predicate.validate(0)?;
19 self.check_verification_predicate(predicate).await
20 }
21
22 pub async fn verify(
24 &self,
25 predicate: VerificationPredicate,
26 deadline: Duration,
27 ) -> BrowserResult<VerificationOutcome> {
28 validate_wait_deadline(deadline)?;
29 predicate.validate(0)?;
30 let started = tokio::time::Instant::now();
31 let expires = started + deadline;
32 loop {
33 let (matched, observed) = self.check_verification_predicate(&predicate).await?;
34 let state = bounded_diagnostic_text(&observed);
35 if matched {
36 return Ok(VerificationOutcome {
37 status: "satisfied",
38 predicate,
39 elapsed_ms: started.elapsed().as_millis() as u64,
40 state,
41 });
42 }
43 if tokio::time::Instant::now() >= expires {
44 return Err(ActionVerificationError {
45 kind: ActionFailureKind::VerificationFailed,
46 action: ActionKind::Click,
47 phase: ActionFailurePhase::Verification,
48 recovery_strategy: RecoveryStrategy::Report,
49 execution_id: Some(self.next_execution_id()),
50 target: None,
51 revision: self.page_revision.load(Ordering::Relaxed),
52 reason: format!("verification predicate not satisfied: {state}"),
53 }
54 .into());
55 }
56 tokio::time::sleep(WAIT_POLL_INTERVAL).await;
57 }
58 }
59
60 fn check_verification_predicate<'a>(
61 &'a self,
62 predicate: &'a VerificationPredicate,
63 ) -> VerificationCheckFuture<'a> {
64 Box::pin(async move {
65 match predicate {
66 VerificationPredicate::UrlEquals { value } => {
67 let url = self.page_info().await?.url;
68 Ok((url == *value, format!("url={url}")))
69 }
70 VerificationPredicate::TitleContains { value } => {
71 let title = self.page_info().await?.title;
72 Ok((title.contains(value), format!("title={title}")))
73 }
74 VerificationPredicate::Visible { visible } => {
75 let (matched, state, _) = self
76 .check_wait_condition(&WaitCondition::TargetVisible(visible.clone()), None)
77 .await?;
78 Ok((matched, state))
79 }
80 VerificationPredicate::TextContains { value } => {
81 let (matched, state, _) = self
82 .check_wait_condition(&WaitCondition::Text(value.clone()), None)
83 .await?;
84 Ok((matched, state))
85 }
86 VerificationPredicate::PopupOpened { value } => {
87 let topology = self.topology.lock().await;
88 let opened = topology.targets.len() > 1;
89 Ok((opened == *value, format!("popupOpened={opened}")))
90 }
91 VerificationPredicate::DialogOpen { value } => {
92 let topology = self.topology.lock().await;
93 let open = topology.pending_dialog.is_some();
94 Ok((open == *value, format!("dialogOpen={open}")))
95 }
96 VerificationPredicate::DownloadStarted { value } => {
97 let started = self.download_sequence.load(Ordering::Relaxed) > 0;
98 Ok((started == *value, format!("downloadStarted={started}")))
99 }
100 VerificationPredicate::RevisionEquals { value } => {
101 let revision = self.page_revision.load(Ordering::Relaxed);
102 Ok((revision == *value, format!("revision={revision}")))
103 }
104 VerificationPredicate::All { all } => {
105 let mut states = Vec::with_capacity(all.len());
106 let mut matched = true;
107 for child in all {
108 let (child_matched, state) =
109 self.check_verification_predicate(child).await?;
110 matched &= child_matched;
111 states.push(state);
112 }
113 Ok((matched, format!("all=[{}]", states.join(","))))
114 }
115 VerificationPredicate::Any { any } => {
116 let mut states = Vec::with_capacity(any.len());
117 let mut matched = false;
118 for child in any {
119 let (child_matched, state) =
120 self.check_verification_predicate(child).await?;
121 matched |= child_matched;
122 states.push(state);
123 }
124 Ok((matched, format!("any=[{}]", states.join(","))))
125 }
126 VerificationPredicate::Not { not } => {
127 let (matched, state) = self.check_verification_predicate(not).await?;
128 Ok((!matched, format!("not({state})")))
129 }
130 }
131 })
132 }
133
134 pub async fn wait(
141 &self,
142 condition: WaitCondition,
143 deadline: Duration,
144 ) -> BrowserResult<WaitOutcome> {
145 self.cdp
146 .with_current_route(async {
147 validate_wait_deadline(deadline)?;
148 condition.validate()?;
149 if let WaitCondition::NetworkQuiet(quiet) = condition {
150 return tokio::time::timeout(
151 deadline,
152 self.wait_for_network_quiet(quiet, deadline),
153 )
154 .await
155 .map_err(|_| {
156 wait_timeout("network_quiet", deadline, "network_check_pending")
157 })?;
158 }
159 let mut events = self.cdp.subscribe_events();
160 self.wait_loop(condition, deadline, deadline, &mut events, false)
161 .await
162 })
163 .await
164 }
165
166 pub(crate) async fn wait_loop(
167 &self,
168 condition: WaitCondition,
169 deadline: Duration,
170 reported_deadline: Duration,
171 events: &mut tokio::sync::broadcast::Receiver<crate::browser::cdp::CdpEvent>,
172 require_load_event: bool,
173 ) -> BrowserResult<WaitOutcome> {
174 let started = tokio::time::Instant::now();
175 let expires = started + deadline;
176 let mut previous_geometry = None;
177 let description = condition.description();
178 let mut load_event_seen = !require_load_event;
179 let mut last_state = "not_checked".to_string();
180 loop {
181 let now = tokio::time::Instant::now();
182 if now >= expires {
183 return Err(wait_timeout(&description, reported_deadline, &last_state).into());
184 }
185 let remaining = expires - now;
186 let (matched, state, geometry) = tokio::time::timeout(
187 remaining,
188 self.check_wait_condition(&condition, previous_geometry.as_deref()),
189 )
190 .await
191 .map_err(|_| wait_timeout(&description, reported_deadline, &last_state))??;
192 last_state = bounded_wait_state(&state);
193 previous_geometry = geometry;
194 if matched && load_event_seen {
195 let (target_id, frame_id) = self.ensured_route_identity().await?;
196 return Ok(WaitOutcome {
197 condition: description,
198 elapsed_ms: started.elapsed().as_millis() as u64,
199 last_state,
200 target_id,
201 frame_id,
202 });
203 }
204 let now = tokio::time::Instant::now();
205 let remaining = expires - now;
206 tokio::select! {
207 _ = tokio::time::sleep(WAIT_POLL_INTERVAL.min(remaining)) => {}
208 event = events.recv() => match event {
209 Ok(event) => { load_event_seen |= event.method == "Page.loadEventFired"; }
210 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
211 Err(_) => return Err("CDP event stream closed during wait".into()),
212 }
213 }
214 }
215 }
216
217 async fn check_wait_condition(
218 &self,
219 condition: &WaitCondition,
220 previous_geometry: Option<&str>,
221 ) -> BrowserResult<(bool, String, Option<String>)> {
222 match condition {
223 WaitCondition::Lifecycle(expected) => {
224 let page = self.page_info().await?;
225 Ok((page.ready_state == *expected, page.ready_state, None))
226 }
227 WaitCondition::UrlExact(expected) => {
228 let page = self.page_info().await?;
229 Ok((page.url == *expected, page.url, None))
230 }
231 WaitCondition::UrlPrefix(prefix) => {
232 let page = self.page_info().await?;
233 Ok((page.url.starts_with(prefix), page.url, None))
234 }
235 WaitCondition::Text(expected) => {
236 let expression = visible_text_contains_expression(expected)?;
237 let value = self.evaluate_value(&expression).await?;
238 let matched = value.as_bool().unwrap_or(false);
239 Ok((matched, format!("present={matched}"), None))
240 }
241 WaitCondition::JavaScript(expression) => {
242 let value = self.evaluate_value(expression).await?;
243 let matched = value
244 .as_bool()
245 .ok_or("wait JavaScript predicate must return a boolean")?;
246 Ok((matched, matched.to_string(), None))
247 }
248 WaitCondition::TargetAttached(target)
249 | WaitCondition::TargetVisible(target)
250 | WaitCondition::TargetHidden(target)
251 | WaitCondition::TargetEnabled(target)
252 | WaitCondition::TargetStable(target) => {
253 self.check_target_wait(condition, target, previous_geometry)
254 .await
255 }
256 WaitCondition::NetworkQuiet(_) => unreachable!("handled by wait"),
257 }
258 }
259
260 async fn check_target_wait(
261 &self,
262 condition: &WaitCondition,
263 target: &str,
264 previous_geometry: Option<&str>,
265 ) -> BrowserResult<(bool, String, Option<String>)> {
266 let element = match self.resolve_element(target).await {
267 Ok(element) => element,
268 Err(error)
269 if error
270 .downcast_ref::<TargetError>()
271 .is_some_and(|error| error.kind == TargetErrorKind::NotFound) =>
272 {
273 let matched = matches!(condition, WaitCondition::TargetHidden(_));
274 return Ok((matched, "detached".to_string(), None));
275 }
276 Err(error) => return Err(error),
277 };
278 if matches!(condition, WaitCondition::TargetAttached(_)) {
279 return Ok((true, "attached".to_string(), None));
280 }
281 let object_id = self
282 .cdp
283 .resolve_node_object(element.node_id, element.backend_dom_node_id)
284 .await?;
285 let remote = RemoteObjectGuard::new(self.cdp.clone(), object_id);
286 let raw = self
287 .cdp
288 .call_on_object(&remote.object_id, WAIT_TARGET_STATE_FUNCTION)
289 .await;
290 let value = runtime_value(&raw?)?;
291 let visible = value["visible"].as_bool().unwrap_or(false);
292 let enabled = value["enabled"].as_bool().unwrap_or(false);
293 let geometry = value["geometry"].as_str().map(str::to_string);
294 let matched = match condition {
295 WaitCondition::TargetVisible(_) => visible,
296 WaitCondition::TargetHidden(_) => !visible,
297 WaitCondition::TargetEnabled(_) => visible && enabled,
298 WaitCondition::TargetStable(_) => {
299 visible
300 && geometry
301 .as_deref()
302 .is_some_and(|geometry| previous_geometry == Some(geometry))
303 }
304 _ => unreachable!(),
305 };
306 Ok((matched, value.to_string(), geometry))
307 }
308
309 async fn wait_for_network_quiet(
310 &self,
311 quiet: Duration,
312 deadline: Duration,
313 ) -> BrowserResult<WaitOutcome> {
314 if quiet.is_zero() {
315 return Err("network quiet duration must be positive".into());
316 }
317 let mut events = self.cdp.subscribe_events_with_params();
318 let mut guard =
319 NetworkDomainGuard::acquire(self.cdp.clone(), Arc::clone(&self.network_wait_leases))
320 .await?;
321 let started = tokio::time::Instant::now();
322 let expires = started + deadline;
323 let mut empty_since = started;
324 let mut in_flight = HashSet::new();
325 let mut overflowed = false;
326 loop {
327 let now = tokio::time::Instant::now();
328 if in_flight.is_empty() && !overflowed && now.duration_since(empty_since) >= quiet {
329 guard.disable().await?;
330 let (target_id, frame_id) = self.route_identity().await?;
331 return Ok(WaitOutcome {
332 condition: "network_quiet".to_string(),
333 elapsed_ms: started.elapsed().as_millis() as u64,
334 last_state: "in_flight=0".to_string(),
335 target_id,
336 frame_id,
337 });
338 }
339 if now >= expires {
340 return Err(WaitTimeout {
341 condition: "network_quiet".to_string(),
342 deadline_ms: deadline.as_millis() as u64,
343 last_state: if overflowed {
344 "in_flight=overflow".to_string()
345 } else {
346 format!("in_flight={}", in_flight.len())
347 },
348 reason: "deadline_exceeded",
349 }
350 .into());
351 }
352 tokio::select! {
353 _ = tokio::time::sleep((expires - now).min(WAIT_POLL_INTERVAL)) => {}
354 event = events.recv() => match event {
355 Ok(event) => {
356 let request_id = event.params["requestId"].as_str();
357 match event.method.as_str() {
358 "Network.requestWillBeSent" => {
359 if let Some(id) = request_id {
360 if in_flight.len() < NETWORK_IN_FLIGHT_LIMIT {
361 in_flight.insert(id.to_string());
362 } else {
363 overflowed = true;
364 }
365 }
366 }
367 "Network.loadingFinished" | "Network.loadingFailed" => {
368 if let Some(id) = request_id { in_flight.remove(id); }
369 if in_flight.is_empty() && !overflowed { empty_since = tokio::time::Instant::now(); }
370 }
371 _ => {}
372 }
373 }
374 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => return Err("network wait event stream lagged".into()),
375 Err(tokio::sync::broadcast::error::RecvError::Closed) => return Err("network wait event stream closed".into()),
376 }
377 }
378 }
379 }
380}