1use super::*;
7
8impl BrowserSession {
9 pub async fn wait(
16 &self,
17 condition: WaitCondition,
18 deadline: Duration,
19 ) -> BrowserResult<WaitOutcome> {
20 self.cdp
21 .with_current_route(async {
22 validate_wait_deadline(deadline)?;
23 condition.validate()?;
24 if let WaitCondition::NetworkQuiet(quiet) = condition {
25 return tokio::time::timeout(
26 deadline,
27 self.wait_for_network_quiet(quiet, deadline),
28 )
29 .await
30 .map_err(|_| {
31 wait_timeout("network_quiet", deadline, "network_check_pending")
32 })?;
33 }
34 let mut events = self.cdp.subscribe_events();
35 self.wait_loop(condition, deadline, deadline, &mut events, false)
36 .await
37 })
38 .await
39 }
40
41 pub(crate) async fn wait_loop(
42 &self,
43 condition: WaitCondition,
44 deadline: Duration,
45 reported_deadline: Duration,
46 events: &mut tokio::sync::broadcast::Receiver<crate::browser::cdp::CdpEvent>,
47 require_load_event: bool,
48 ) -> BrowserResult<WaitOutcome> {
49 let started = tokio::time::Instant::now();
50 let expires = started + deadline;
51 let mut previous_geometry = None;
52 let description = condition.description();
53 let mut load_event_seen = !require_load_event;
54 let mut last_state = "not_checked".to_string();
55 loop {
56 let now = tokio::time::Instant::now();
57 if now >= expires {
58 return Err(wait_timeout(&description, reported_deadline, &last_state).into());
59 }
60 let remaining = expires - now;
61 let (matched, state, geometry) = tokio::time::timeout(
62 remaining,
63 self.check_wait_condition(&condition, previous_geometry.as_deref()),
64 )
65 .await
66 .map_err(|_| wait_timeout(&description, reported_deadline, &last_state))??;
67 last_state = bounded_wait_state(&state);
68 previous_geometry = geometry;
69 if matched && load_event_seen {
70 let (target_id, frame_id) = self.ensured_route_identity().await?;
71 return Ok(WaitOutcome {
72 condition: description,
73 elapsed_ms: started.elapsed().as_millis() as u64,
74 last_state,
75 target_id,
76 frame_id,
77 });
78 }
79 let now = tokio::time::Instant::now();
80 let remaining = expires - now;
81 tokio::select! {
82 _ = tokio::time::sleep(WAIT_POLL_INTERVAL.min(remaining)) => {}
83 event = events.recv() => match event {
84 Ok(event) => { load_event_seen |= event.method == "Page.loadEventFired"; }
85 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
86 Err(_) => return Err("CDP event stream closed during wait".into()),
87 }
88 }
89 }
90 }
91
92 async fn check_wait_condition(
93 &self,
94 condition: &WaitCondition,
95 previous_geometry: Option<&str>,
96 ) -> BrowserResult<(bool, String, Option<String>)> {
97 match condition {
98 WaitCondition::Lifecycle(expected) => {
99 let page = self.page_info().await?;
100 Ok((page.ready_state == *expected, page.ready_state, None))
101 }
102 WaitCondition::UrlExact(expected) => {
103 let page = self.page_info().await?;
104 Ok((page.url == *expected, page.url, None))
105 }
106 WaitCondition::UrlPrefix(prefix) => {
107 let page = self.page_info().await?;
108 Ok((page.url.starts_with(prefix), page.url, None))
109 }
110 WaitCondition::Text(expected) => {
111 let expression = visible_text_contains_expression(expected)?;
112 let value = self.evaluate_value(&expression).await?;
113 let matched = value.as_bool().unwrap_or(false);
114 Ok((matched, format!("present={matched}"), None))
115 }
116 WaitCondition::JavaScript(expression) => {
117 let value = self.evaluate_value(expression).await?;
118 let matched = value
119 .as_bool()
120 .ok_or("wait JavaScript predicate must return a boolean")?;
121 Ok((matched, matched.to_string(), None))
122 }
123 WaitCondition::TargetAttached(target)
124 | WaitCondition::TargetVisible(target)
125 | WaitCondition::TargetHidden(target)
126 | WaitCondition::TargetEnabled(target)
127 | WaitCondition::TargetStable(target) => {
128 self.check_target_wait(condition, target, previous_geometry)
129 .await
130 }
131 WaitCondition::NetworkQuiet(_) => unreachable!("handled by wait"),
132 }
133 }
134
135 async fn check_target_wait(
136 &self,
137 condition: &WaitCondition,
138 target: &str,
139 previous_geometry: Option<&str>,
140 ) -> BrowserResult<(bool, String, Option<String>)> {
141 let element = match self.resolve_element(target).await {
142 Ok(element) => element,
143 Err(error)
144 if error
145 .downcast_ref::<TargetError>()
146 .is_some_and(|error| error.kind == TargetErrorKind::NotFound) =>
147 {
148 let matched = matches!(condition, WaitCondition::TargetHidden(_));
149 return Ok((matched, "detached".to_string(), None));
150 }
151 Err(error) => return Err(error),
152 };
153 if matches!(condition, WaitCondition::TargetAttached(_)) {
154 return Ok((true, "attached".to_string(), None));
155 }
156 let object_id = self
157 .cdp
158 .resolve_node_object(element.node_id, element.backend_dom_node_id)
159 .await?;
160 let remote = RemoteObjectGuard::new(self.cdp.clone(), object_id);
161 let raw = self
162 .cdp
163 .call_on_object(&remote.object_id, WAIT_TARGET_STATE_FUNCTION)
164 .await;
165 let value = runtime_value(&raw?)?;
166 let visible = value["visible"].as_bool().unwrap_or(false);
167 let enabled = value["enabled"].as_bool().unwrap_or(false);
168 let geometry = value["geometry"].as_str().map(str::to_string);
169 let matched = match condition {
170 WaitCondition::TargetVisible(_) => visible,
171 WaitCondition::TargetHidden(_) => !visible,
172 WaitCondition::TargetEnabled(_) => visible && enabled,
173 WaitCondition::TargetStable(_) => {
174 visible
175 && geometry
176 .as_deref()
177 .is_some_and(|geometry| previous_geometry == Some(geometry))
178 }
179 _ => unreachable!(),
180 };
181 Ok((matched, value.to_string(), geometry))
182 }
183
184 async fn wait_for_network_quiet(
185 &self,
186 quiet: Duration,
187 deadline: Duration,
188 ) -> BrowserResult<WaitOutcome> {
189 if quiet.is_zero() {
190 return Err("network quiet duration must be positive".into());
191 }
192 let mut events = self.cdp.subscribe_events_with_params();
193 let mut guard =
194 NetworkDomainGuard::acquire(self.cdp.clone(), Arc::clone(&self.network_wait_leases))
195 .await?;
196 let started = tokio::time::Instant::now();
197 let expires = started + deadline;
198 let mut empty_since = started;
199 let mut in_flight = HashSet::new();
200 let mut overflowed = false;
201 loop {
202 let now = tokio::time::Instant::now();
203 if in_flight.is_empty() && !overflowed && now.duration_since(empty_since) >= quiet {
204 guard.disable().await?;
205 let (target_id, frame_id) = self.route_identity().await?;
206 return Ok(WaitOutcome {
207 condition: "network_quiet".to_string(),
208 elapsed_ms: started.elapsed().as_millis() as u64,
209 last_state: "in_flight=0".to_string(),
210 target_id,
211 frame_id,
212 });
213 }
214 if now >= expires {
215 return Err(WaitTimeout {
216 condition: "network_quiet".to_string(),
217 deadline_ms: deadline.as_millis() as u64,
218 last_state: if overflowed {
219 "in_flight=overflow".to_string()
220 } else {
221 format!("in_flight={}", in_flight.len())
222 },
223 reason: "deadline_exceeded",
224 }
225 .into());
226 }
227 tokio::select! {
228 _ = tokio::time::sleep((expires - now).min(WAIT_POLL_INTERVAL)) => {}
229 event = events.recv() => match event {
230 Ok(event) => {
231 let request_id = event.params["requestId"].as_str();
232 match event.method.as_str() {
233 "Network.requestWillBeSent" => {
234 if let Some(id) = request_id {
235 if in_flight.len() < NETWORK_IN_FLIGHT_LIMIT {
236 in_flight.insert(id.to_string());
237 } else {
238 overflowed = true;
239 }
240 }
241 }
242 "Network.loadingFinished" | "Network.loadingFailed" => {
243 if let Some(id) = request_id { in_flight.remove(id); }
244 if in_flight.is_empty() && !overflowed { empty_since = tokio::time::Instant::now(); }
245 }
246 _ => {}
247 }
248 }
249 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => return Err("network wait event stream lagged".into()),
250 Err(tokio::sync::broadcast::error::RecvError::Closed) => return Err("network wait event stream closed".into()),
251 }
252 }
253 }
254 }
255}