Skip to main content

browser_test/
wait.rs

1use std::time::Duration;
2
3use thirtyfour::extensions::query::{ElementPollerWithTimeout, IntoElementPoller};
4use typed_builder::TypedBuilder;
5
6/// Wait configuration used by thirtyfour element queries and element waits.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TypedBuilder)]
8pub struct ElementQueryWaitConfig {
9    /// Maximum time an element query or element wait keeps polling before failing.
10    ///
11    /// This controls `thirtyfour`'s explicit element-query polling, such as queries created via
12    /// `driver.query(...)` and waits that use the configured driver poller. It is not a
13    /// `WebDriver` protocol timeout, and it does not control page navigation, script execution, or
14    /// ordinary Rust futures in the test body.
15    ///
16    /// In browser tests this is the timeout that usually determines how long the test waits for a
17    /// dynamic DOM condition, such as an element appearing after hydration, a button becoming
18    /// clickable, or content being inserted after an application request completes.
19    #[builder(setter(into))]
20    timeout: Duration,
21
22    /// Delay between element-query poll attempts during the timeout window.
23    ///
24    /// Smaller intervals can make tests react faster once the expected element state appears, but
25    /// they also issue `WebDriver` commands more frequently. Larger intervals reduce browser-driver
26    /// traffic, but can make successful waits complete later than necessary.
27    ///
28    /// Avoid `Duration::ZERO` for values from configuration, environment variables, or other
29    /// dynamic input. A zero interval is accepted by [`Self::new`] and the builder for trusted
30    /// construction, but can create an immediate retry loop in the underlying element poller. Use
31    /// [`Self::try_new`] when the interval is not a hard-coded trusted value.
32    #[builder(setter(into))]
33    interval: Duration,
34}
35
36/// Invalid element query wait configuration.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
38pub enum ElementQueryWaitConfigError {
39    /// The poll interval must be non-zero.
40    #[error("Element query wait poll interval must be non-zero.")]
41    ZeroInterval,
42}
43
44impl ElementQueryWaitConfig {
45    /// Create a new element query wait configuration without validation.
46    ///
47    /// Use [`Self::try_new`] for values from user input or environment configuration. A zero
48    /// interval is accepted here for const construction, but can cause immediate retry loops in the
49    /// underlying `WebDriver` element poller.
50    #[must_use]
51    pub const fn new(timeout: Duration, interval: Duration) -> Self {
52        Self { timeout, interval }
53    }
54
55    /// Create a new element query wait configuration.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`ElementQueryWaitConfigError::ZeroInterval`] if `interval` is
60    /// [`Duration::ZERO`].
61    pub fn try_new(
62        timeout: Duration,
63        interval: Duration,
64    ) -> Result<Self, ElementQueryWaitConfigError> {
65        if interval.is_zero() {
66            return Err(ElementQueryWaitConfigError::ZeroInterval);
67        }
68
69        Ok(Self { timeout, interval })
70    }
71
72    /// Maximum time an element query or element wait keeps polling before failing.
73    #[must_use]
74    pub const fn timeout(self) -> Duration {
75        self.timeout
76    }
77
78    /// Delay between element-query poll attempts during the timeout window.
79    #[must_use]
80    pub const fn interval(self) -> Duration {
81        self.interval
82    }
83
84    pub(crate) fn into_thirtyfour_poller(self) -> impl IntoElementPoller + Send + Sync {
85        ElementPollerWithTimeout::new(self.timeout, self.interval)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use assertr::prelude::*;
93
94    #[test]
95    fn builder_preserves_timeout_and_interval() {
96        let wait = ElementQueryWaitConfig::builder()
97            .timeout(Duration::from_secs(10))
98            .interval(Duration::from_millis(250))
99            .build();
100
101        assert_that!(wait.timeout()).is_equal_to(Duration::from_secs(10));
102        assert_that!(wait.interval()).is_equal_to(Duration::from_millis(250));
103    }
104
105    #[test]
106    fn builder_preserves_zero_interval_for_trusted_callers() {
107        let wait = ElementQueryWaitConfig::builder()
108            .timeout(Duration::from_secs(10))
109            .interval(Duration::ZERO)
110            .build();
111
112        assert_that!(wait.interval()).is_equal_to(Duration::ZERO);
113    }
114
115    #[test]
116    fn try_new_accepts_non_zero_interval() {
117        let wait =
118            ElementQueryWaitConfig::try_new(Duration::from_secs(10), Duration::from_millis(250))
119                .expect("non-zero interval should be accepted");
120
121        assert_that!(wait.timeout()).is_equal_to(Duration::from_secs(10));
122        assert_that!(wait.interval()).is_equal_to(Duration::from_millis(250));
123    }
124
125    #[test]
126    fn try_new_rejects_zero_interval() {
127        let err = ElementQueryWaitConfig::try_new(Duration::from_secs(10), Duration::ZERO)
128            .expect_err("zero interval should be rejected");
129
130        assert_that!(err).is_equal_to(ElementQueryWaitConfigError::ZeroInterval);
131    }
132
133    #[test]
134    fn new_preserves_zero_interval_for_const_trusted_callers() {
135        const WAIT: ElementQueryWaitConfig =
136            ElementQueryWaitConfig::new(Duration::from_secs(10), Duration::ZERO);
137
138        assert_that!(WAIT.interval()).is_equal_to(Duration::ZERO);
139    }
140}