diff --git a/src/error.rs b/src/error.rs index deb38634..d056bf22 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,7 @@ use hyper::error as herror; use std::error::Error; use std::fmt; use std::io::Error as IOError; +use std::time::Duration; use url::ParseError; use webdriver::error as wderror; @@ -120,6 +121,10 @@ pub enum CmdError { /// Could not decode a base64 image ImageDecodeError(::base64::DecodeError), + + /// The user specified timeout is reached + /// Contains expired duration + Timeout(Duration), } impl CmdError { @@ -158,6 +163,7 @@ impl Error for CmdError { CmdError::NotW3C(..) => "webdriver returned non-conforming response", CmdError::InvalidArgument(..) => "invalid argument provided", CmdError::ImageDecodeError(..) => "error decoding image", + CmdError::Timeout(..) => "timeout occured", } } @@ -171,7 +177,10 @@ impl Error for CmdError { CmdError::Lost(ref e) => Some(e), CmdError::Json(ref e) => Some(e), CmdError::ImageDecodeError(ref e) => Some(e), - CmdError::NotJson(_) | CmdError::NotW3C(_) | CmdError::InvalidArgument(..) => None, + CmdError::NotJson(_) + | CmdError::NotW3C(_) + | CmdError::InvalidArgument(..) + | CmdError::Timeout(..) => None, } } } @@ -191,6 +200,7 @@ impl fmt::Display for CmdError { CmdError::Json(ref e) => write!(f, "{}", e), CmdError::NotW3C(ref e) => write!(f, "{:?}", e), CmdError::ImageDecodeError(ref e) => write!(f, "{:?}", e), + CmdError::Timeout(timeout) => write!(f, "timeout was exceeded on {} ms", timeout.as_millis()), CmdError::InvalidArgument(ref arg, ref msg) => { write!(f, "Invalid argument `{}`: {}", arg, msg) } diff --git a/src/lib.rs b/src/lib.rs index f5868d9b..a86a05c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,6 +146,9 @@ pub use hyper::Method; /// Error types. pub mod error; +/// Wait logic +pub mod wait; + /// The long-running session future we spawn for multiplexing onto a running WebDriver instance. mod session; use crate::session::{Cmd, Session}; @@ -711,7 +714,7 @@ impl Client { /// While this currently just spins and yields, it may be more efficient than this in the /// future. In particular, in time, it may only run `is_ready` again when an event occurs on /// the page. - pub async fn wait_for(&mut self, mut is_ready: F) -> Result<(), error::CmdError> + pub async fn wait_for<'a, F, FF>(&mut self, mut is_ready: F) -> Result<(), error::CmdError> where F: FnMut(&mut Client) -> FF, FF: Future>, @@ -727,20 +730,31 @@ impl Client { /// future. In particular, in time, it may only run `is_ready` again when an event occurs on /// the page. pub async fn wait_for_find(&mut self, search: Locator<'_>) -> Result { - let s: webdriver::command::LocatorParameters = search.into(); - loop { - match self - .by(webdriver::command::LocatorParameters { - using: s.using, - value: s.value.clone(), - }) - .await - { - Ok(v) => break Ok(v), - Err(error::CmdError::NoSuchElement(_)) => {} - Err(e) => break Err(e), + fn closure( + s: Locator<'_>, + mut client: Client, // TODO: handle lifetime issues with &mut Client type + ) -> impl Future>> { + let s: webdriver::command::LocatorParameters = s.into(); + async move { + match client + .by(webdriver::command::LocatorParameters { + using: s.using, + value: s.value.clone(), + }) + .await + { + Ok(v) => Some(Ok(v)), + Err(error::CmdError::NoSuchElement(_)) => None, + Err(e) => Some(Err(e)), + } } } + + const DEFAULT_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500); + let mut wait = wait::Wait::new(self, DEFAULT_TIMEOUT); + wait.until(move |client| closure(search, client.clone())) + .await + .map_err(|timeout| error::CmdError::Timeout(timeout))? } /// Wait for the page to navigate to a new URL before proceeding. diff --git a/src/wait.rs b/src/wait.rs new file mode 100644 index 00000000..5e274554 --- /dev/null +++ b/src/wait.rs @@ -0,0 +1,70 @@ +/// The module contains a wait primitive +use crate::Client; +use std::future::Future; +use std::time::Duration; + +const DEFAULT_POOLING_INTERVAL: Duration = Duration::from_millis(500); + +/// +#[derive(Debug)] +pub struct Wait<'a> { + waiter: DefaultWait<&'a mut Client>, +} + +impl<'a> Wait<'a> { + /// + pub fn new(client: &'a mut Client, timeout: Duration) -> Self { + Self { + waiter: DefaultWait::new(client, timeout, DEFAULT_POOLING_INTERVAL), + } + } + + pub(crate) async fn until(&mut self, condition: F) -> Result + where + F: FnMut(&mut &mut Client) -> FF, + FF: Future>, + { + self.waiter.until(condition).await + } +} + +/// +#[derive(Debug)] +pub(crate) struct DefaultWait { + input: T, + timeout: Duration, + pooling_interval: Duration, +} + +impl DefaultWait { + /// + pub(crate) fn new(input: T, timeout: Duration, pooling_interval: Duration) -> Self { + Self { + input, + timeout, + pooling_interval, + } + } + + /// + pub(crate) async fn until(&mut self, mut condition: F) -> Result + where + F: FnMut(&mut T) -> FF, + FF: Future>, + { + let now = std::time::Instant::now(); + + loop { + match condition(&mut self.input).await { + Some(result) => return Ok(result), + None => (), + } + + if now.elapsed() > self.timeout { + return Err(now.elapsed() - self.timeout)?; + } + + tokio::time::delay_for(self.pooling_interval).await; + } + } +}