use super::{is_element_cdp_error, sleep_ms, Element, Page};
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>(timeout_ms: u64, error_msg: String, check: F) -> Result<T>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<Option<T>>>,
{
let timeout = std::time::Duration::from_millis(timeout_ms);
tokio::time::timeout(timeout, async {
loop {
if let Some(val) = check().await? {
return Ok(val);
}
sleep_ms(POLL_INTERVAL_MS).await;
}
})
.await
.map_err(|_| Error::Timeout(error_msg))?
}
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> {
tokio::time::timeout(std::time::Duration::from_millis(timeout_ms), async {
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
})
.await
.map_err(|_| Error::Timeout("URL did not change before the deadline".into()))?
}
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<()> {
self.session
.wait_for_network_idle(idle_time_ms, timeout_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 @ Error::Action { .. }) => return Err(e),
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::*;
#[tokio::test(start_paused = true)]
async fn slow_command_cannot_overrun_poll_deadline() {
let start = tokio::time::Instant::now();
let result: Result<()> = Page::poll_until(100, "bounded".into(), || async {
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
Ok(Some(()))
})
.await;
assert!(matches!(result, Err(Error::Timeout(_))));
assert_eq!(start.elapsed(), std::time::Duration::from_millis(100));
}
#[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 { .. })
));
}
}