fantoccini/wait.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
//! Allow to wait for conditions.
//!
//! Sometimes it is necessary to wait for a browser to achieve a certain state. For example,
//! navigating to a page may be take bit of time. And the time may vary between different
//! environments and test runs. Static delays can work around this issue, but also prolong the
//! test runs unnecessarily. Longer delays have less flaky tests, but even more unnecessary wait
//! time.
//!
//! To wait as optimal as possible, you can use asynchronous wait operations, which periodically
//! check for the expected state, re-try if necessary, but also fail after a certain time and still
//! allow you to fail the test. Allow for longer grace periods, and only spending the time waiting
//! when necessary.
//!
//! # Basic usage
//!
//! By default all wait operations will time-out after 30 seconds and will re-check every
//! 250 milliseconds. You can configure this using the [`Wait::at_most`] and [`Wait::every`]
//! methods or use [`Wait::forever`] to wait indefinitely.
//!
//! Once configured, you can start waiting on some condition by using the `Wait::for_*` methods.
//! For example:
//!
//! ```no_run
//! # use fantoccini::{ClientBuilder, Locator};
//! # #[tokio::main]
//! # async fn main() -> Result<(), fantoccini::error::CmdError> {
//! # #[cfg(all(feature = "native-tls", not(feature = "rustls-tls")))]
//! # let client = ClientBuilder::native().connect("http://localhost:4444").await.expect("failed to connect to WebDriver");
//! # #[cfg(feature = "rustls-tls")]
//! # let client = ClientBuilder::rustls().connect("http://localhost:4444").await.expect("failed to connect to WebDriver");
//! # #[cfg(all(not(feature = "native-tls"), not(feature = "rustls-tls")))]
//! # let client: fantoccini::Client = unreachable!("no tls provider available");
//! // -- snip wrapper code --
//! let button = client.wait().for_element(Locator::Css(
//! r#"a.button-download[href="/learn/get-started"]"#,
//! )).await?;
//! // -- snip wrapper code --
//! # client.close().await
//! # }
//! ```
//!
//! # Error handling
//!
//! When a wait operation times out, it will return a [`CmdError::WaitTimeout`]. When a wait
//! condition check returns an error, the wait operation will be aborted, and the error returned.
use crate::elements::Element;
use crate::error::CmdError;
use crate::wd::Locator;
use crate::Client;
use std::time::{Duration, Instant};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_PERIOD: Duration = Duration::from_millis(250);
/// Used for setting up a wait operation on the client.
#[derive(Debug)]
pub struct Wait<'c> {
client: &'c Client,
timeout: Option<Duration>,
period: Duration,
}
macro_rules! wait_on {
($self:ident, $ready:expr) => {{
let start = Instant::now();
loop {
match $self.timeout {
Some(timeout) if start.elapsed() > timeout => break Err(CmdError::WaitTimeout),
_ => {}
}
match $ready? {
Some(result) => break Ok(result),
None => {
tokio::time::sleep($self.period).await;
}
};
}
}};
}
impl<'c> Wait<'c> {
/// Create a new wait operation from a client.
///
/// This only starts the process of building a new wait operation. Waiting, and checking, will
/// only begin once one of the consuming methods has been called.
///
/// ```no_run
/// # use fantoccini::{ClientBuilder, Locator};
/// # #[tokio::main]
/// # async fn main() -> Result<(), fantoccini::error::CmdError> {
/// # #[cfg(all(feature = "native-tls", not(feature = "rustls-tls")))]
/// # let client = ClientBuilder::native().connect("http://localhost:4444").await.expect("failed to connect to WebDriver");
/// # #[cfg(feature = "rustls-tls")]
/// # let client = ClientBuilder::rustls().connect("http://localhost:4444").await.expect("failed to connect to WebDriver");
/// # #[cfg(all(not(feature = "native-tls"), not(feature = "rustls-tls")))]
/// # let client: fantoccini::Client = unreachable!("no tls provider available");
/// // -- snip wrapper code --
/// let button = client.wait().for_element(Locator::Css(
/// r#"a.button-download[href="/learn/get-started"]"#,
/// )).await?;
/// // -- snip wrapper code --
/// # client.close().await
/// # }
/// ```
pub fn new(client: &'c Client) -> Self {
Self {
client,
timeout: Some(DEFAULT_TIMEOUT),
period: DEFAULT_PERIOD,
}
}
/// Set the timeout until the operation should wait.
#[must_use]
pub fn at_most(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
/// Wait forever.
#[must_use]
pub fn forever(mut self) -> Self {
self.timeout = None;
self
}
/// Sets the period to delay checks.
#[must_use]
pub fn every(mut self, period: Duration) -> Self {
self.period = period;
self
}
/// Wait until a particular element can be found.
pub async fn for_element(self, search: Locator<'_>) -> Result<Element, CmdError> {
wait_on!(self, {
match self.client.by(search.into_parameters()).await {
Ok(element) => Ok(Some(element)),
Err(CmdError::NoSuchElement(_)) => Ok(None),
Err(err) => Err(err),
}
})
}
/// Wait until a given URL is reached.
pub async fn for_url(self, url: url::Url) -> Result<(), CmdError> {
wait_on!(self, {
Ok::<_, CmdError>(if self.client.current_url().await? == url {
Some(())
} else {
None
})
})
}
}