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
            })
        })
    }
}