Skip to main content

browser_test/
wait.rs

1use std::{sync::Arc, time::Duration};
2
3use thirtyfour::{error::WebDriverResult, extensions::query::ElementPollerWithTimeout};
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_webdriver_config(
85        self,
86    ) -> WebDriverResult<thirtyfour::common::config::WebDriverConfig> {
87        thirtyfour::common::config::WebDriverConfig::builder()
88            .poller(Arc::new(ElementPollerWithTimeout::new(
89                self.timeout,
90                self.interval,
91            )))
92            .build()
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use assertr::prelude::*;
100
101    #[test]
102    fn builder_preserves_timeout_and_interval() {
103        let wait = ElementQueryWaitConfig::builder()
104            .timeout(Duration::from_secs(10))
105            .interval(Duration::from_millis(250))
106            .build();
107
108        assert_that!(wait.timeout()).is_equal_to(Duration::from_secs(10));
109        assert_that!(wait.interval()).is_equal_to(Duration::from_millis(250));
110    }
111
112    #[test]
113    fn builder_preserves_zero_interval_for_trusted_callers() {
114        let wait = ElementQueryWaitConfig::builder()
115            .timeout(Duration::from_secs(10))
116            .interval(Duration::ZERO)
117            .build();
118
119        assert_that!(wait.interval()).is_equal_to(Duration::ZERO);
120    }
121
122    #[test]
123    fn try_new_accepts_non_zero_interval() {
124        let wait =
125            ElementQueryWaitConfig::try_new(Duration::from_secs(10), Duration::from_millis(250))
126                .expect("non-zero interval should be accepted");
127
128        assert_that!(wait.timeout()).is_equal_to(Duration::from_secs(10));
129        assert_that!(wait.interval()).is_equal_to(Duration::from_millis(250));
130    }
131
132    #[test]
133    fn try_new_rejects_zero_interval() {
134        let err = ElementQueryWaitConfig::try_new(Duration::from_secs(10), Duration::ZERO)
135            .expect_err("zero interval should be rejected");
136
137        assert_that!(err).is_equal_to(ElementQueryWaitConfigError::ZeroInterval);
138    }
139
140    #[test]
141    fn new_preserves_zero_interval_for_const_trusted_callers() {
142        const WAIT: ElementQueryWaitConfig =
143            ElementQueryWaitConfig::new(Duration::from_secs(10), Duration::ZERO);
144
145        assert_that!(WAIT.interval()).is_equal_to(Duration::ZERO);
146    }
147}