use super::{is_element_cdp_error, sleep_ms, Element, Page, INTERACTION_DELAY_MS};
use crate::error::{Error, Result};
const POLL_INTERVAL_MS: u64 = 100;
fn retry_if_not_found<T>(result: Result<T>) -> Result<Option<T>> {
match result {
Ok(value) => Ok(Some(value)),
Err(Error::ElementNotFound(_)) => Ok(None),
Err(error) => Err(error),
}
}
impl Page {
async fn poll_until<T, F, Fut>(&self, timeout_ms: u64, error_msg: String, check: F) -> Result<T>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<Option<T>>>,
{
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_millis(timeout_ms);
loop {
if let Some(val) = check().await? {
return Ok(val);
}
if start.elapsed() > timeout {
return Err(Error::Timeout(error_msg));
}
sleep_ms(POLL_INTERVAL_MS).await;
}
}
pub async fn wait_for(&self, selector: &str, timeout_ms: u64) -> Result<Element> {
self.poll_until(
timeout_ms,
format!("Element '{}' not found within {}ms", selector, timeout_ms),
|| async { retry_if_not_found(self.find(selector).await) },
)
.await
}
pub async fn wait_for_visible(&self, selector: &str, timeout_ms: u64) -> Result<Element> {
self.poll_until(
timeout_ms,
format!("Element '{}' not visible within {}ms", selector, timeout_ms),
|| async {
let Some(element) = retry_if_not_found(self.find(selector).await)? else {
return Ok(None);
};
match element.center().await {
Ok(_) => Ok(Some(element)),
Err(error) if is_element_cdp_error(&error) => Ok(None),
Err(error) => Err(error),
}
},
)
.await
}
pub async fn wait_for_hidden(&self, selector: &str, timeout_ms: u64) -> Result<()> {
self.poll_until(
timeout_ms,
format!(
"Element '{}' still visible after {}ms",
selector, timeout_ms
),
|| async {
match self.find(selector).await {
Err(Error::ElementNotFound(_)) => Ok(Some(())),
Ok(el) => match el.is_visible().await {
Ok(false) => Ok(Some(())),
Ok(true) => Ok(None),
Err(error) if is_element_cdp_error(&error) => Ok(Some(())),
Err(error) => Err(error),
},
Err(error) => Err(error),
}
},
)
.await
}
pub async fn wait(&self, ms: u64) {
sleep_ms(ms).await;
}
pub async fn wait_for_text(&self, text: &str, timeout_ms: u64) -> Result<Element> {
self.poll_until(
timeout_ms,
format!(
"Element with text '{}' not found within {}ms",
text, timeout_ms
),
|| async { retry_if_not_found(self.find_by_text(text).await) },
)
.await
}
pub async fn wait_for_url_contains(&self, pattern: &str, timeout_ms: u64) -> Result<()> {
self.poll_until(
timeout_ms,
format!("URL did not contain '{}' within {}ms", pattern, timeout_ms),
|| async {
let url = self.url().await?;
Ok(url.contains(pattern).then_some(()))
},
)
.await
}
pub async fn wait_for_url_change(&self, timeout_ms: u64) -> Result<String> {
let original_url = self.url().await?;
self.poll_until(
timeout_ms,
format!(
"URL did not change from '{}' within {}ms",
original_url, timeout_ms
),
|| async {
let url = self.url().await?;
Ok((url != original_url).then_some(url))
},
)
.await
}
pub async fn wait_for_any(&self, selectors: &[&str], timeout_ms: u64) -> Result<Element> {
self.poll_until(
timeout_ms,
format!(
"None of selectors found within {}ms: {:?}",
timeout_ms, selectors
),
|| async { retry_if_not_found(self.find_any(selectors).await) },
)
.await
}
pub async fn wait_for_network_idle(&self, idle_time_ms: u64, timeout_ms: u64) -> Result<()> {
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_millis(timeout_ms);
let idle_duration = std::time::Duration::from_millis(idle_time_ms);
let key = &self.net_idle_key;
let check_idle_js = format!(
r#"
(() => {{
var K = '{key}';
if (window[K] === undefined) {{
Object.defineProperty(window, K, {{
value: {{ n: 0 }}, writable: true, enumerable: false, configurable: true
}});
var st = window[K];
const of = window.fetch;
const wf = function fetch(...a) {{
st.n++;
return of.apply(this, a).finally(() => {{ st.n--; }});
}};
try {{ wf.toString = () => 'function fetch() {{ [native code] }}'; }} catch(e) {{}}
window.fetch = wf;
const oo = XMLHttpRequest.prototype.open;
const os = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(...a) {{ this[K] = true; return oo.apply(this, a); }};
XMLHttpRequest.prototype.send = function(...a) {{
if (this[K]) {{
st.n++;
this.addEventListener('loadend', () => {{ st.n--; }});
}}
return os.apply(this, a);
}};
}}
var pend = window[K].n;
return (document.readyState === 'complete') ? pend : -1;
}})()
"#
);
let mut idle_start: Option<std::time::Instant> = None;
loop {
let pending: i32 = self.evaluate_sync(&check_idle_js).await?;
if pending == 0 {
match idle_start {
Some(start) if start.elapsed() >= idle_duration => {
return Ok(());
}
None => {
idle_start = Some(std::time::Instant::now());
}
_ => {}
}
} else {
idle_start = None;
}
if start.elapsed() > timeout {
tracing::warn!(
"wait_for_network_idle timed out after {}ms with {} pending request(s)",
timeout_ms,
pending
);
return Err(Error::Timeout(format!(
"Network not idle after {}ms ({} pending requests)",
timeout_ms, pending
)));
}
sleep_ms(INTERACTION_DELAY_MS).await;
}
}
pub async fn with_retry<F, Fut, T>(
&self,
attempts: u32,
delay_ms: u64,
operation: F,
) -> Result<T>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let attempts = attempts.max(1);
let mut last_error = String::new();
for attempt in 1..=attempts {
match operation().await {
Ok(result) => return Ok(result),
Err(e) => {
last_error = e.to_string();
if attempt < attempts {
sleep_ms(delay_ms).await;
}
}
}
}
Err(Error::RetryExhausted {
attempts,
last_error,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retry_if_not_found_only_retries_absence() {
assert!(matches!(retry_if_not_found::<()>(Ok(())), Ok(Some(()))));
assert!(matches!(
retry_if_not_found::<()>(Err(Error::ElementNotFound("missing".into()))),
Ok(None)
));
assert!(matches!(
retry_if_not_found::<()>(Err(Error::transport("disconnected"))),
Err(Error::Transport { .. })
));
}
}