Skip to main content

browser_test/
timeout.rs

1use std::time::Duration;
2
3use typed_builder::TypedBuilder;
4
5/// `WebDriver` timeout configuration applied before running a browser test.
6///
7/// The builder setters accept `Duration` values. Use each setter's `_opt` fallback for
8/// `Option<Duration>` values. Leaving a timeout unset means the runner does not update that
9/// timeout on the `WebDriver` session.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, TypedBuilder)]
11#[allow(clippy::struct_field_names)]
12pub struct BrowserTimeouts {
13    /// Maximum time `WebDriver` waits for asynchronous script execution.
14    ///
15    /// This timeout applies to browser-side scripts that explicitly wait for completion, such as
16    /// `execute_async` / async JavaScript calls. It does not control page navigation, element
17    /// lookup, or ordinary Rust futures in the test body.
18    ///
19    /// In browser tests this usually matters when helpers inject JavaScript that calls back later,
20    /// waits for browser APIs, or bridges to application state from inside the page. If the script
21    /// does not finish before this duration, the script command fails even if the page itself is
22    /// otherwise healthy.
23    ///
24    /// Use the builders `script_timeout(...)` to update the timeout. Use `script_timeout_opt(None)`
25    /// to leave the session's current script timeout unchanged.
26    #[builder(default, setter(strip_option(fallback_suffix = "_opt")))]
27    script_timeout: Option<Duration>,
28
29    /// Maximum time `WebDriver` waits for page navigation to finish loading.
30    ///
31    /// This timeout applies to navigation commands such as opening a URL, refreshing, or moving
32    /// through browser history. It covers the browser's page-load lifecycle, not arbitrary
33    /// application readiness after the document has loaded.
34    ///
35    /// In browser tests this can fail a `driver.goto(...)` call when the target page, redirects,
36    /// or blocking resources take too long. It is not a replacement for explicit waits after
37    /// navigation: single-page app hydration, background requests, animations, and delayed DOM
38    /// updates should still be handled with element-query waits or test-specific polling.
39    ///
40    /// Use the builders `page_load_timeout(...)` to update the timeout. Use
41    /// `page_load_timeout_opt(None)` to leave the session's current page-load timeout unchanged.
42    #[builder(default, setter(strip_option(fallback_suffix = "_opt")))]
43    page_load_timeout: Option<Duration>,
44
45    /// Maximum time `WebDriver` waits while locating elements through raw element lookup commands.
46    ///
47    /// This timeout affects implicit waiting in the browser driver itself. When it is non-zero,
48    /// element lookup commands can block until a matching element appears or the duration expires.
49    /// That can make missing-element assertions slower and can compound with explicit polling.
50    ///
51    /// For tests using `thirtyfour` element queries, `WebElement::wait_until`, or this crate's
52    /// [`ElementQueryWaitConfig`](crate::ElementQueryWaitConfig), prefer keeping this at
53    /// `Duration::ZERO` and using explicit waits instead. Explicit waits make the waiting behavior
54    /// local to the assertion or action that needs it, while a non-zero implicit wait affects every
55    /// element lookup in the session.
56    ///
57    /// Use the builders `implicit_wait_timeout(...)` to update the timeout. Passing
58    /// `Duration::ZERO` explicitly disables implicit waiting for the session. Use
59    /// `implicit_wait_timeout_opt(None)` to leave the session's current implicit wait timeout
60    /// unchanged.
61    #[builder(default, setter(strip_option(fallback_suffix = "_opt")))]
62    implicit_wait_timeout: Option<Duration>,
63}
64
65impl BrowserTimeouts {
66    /// Maximum time `WebDriver` waits for asynchronous script execution.
67    ///
68    /// Returning `None` means this timeout is not updated.
69    #[must_use]
70    pub const fn script_timeout(self) -> Option<Duration> {
71        self.script_timeout
72    }
73
74    /// Maximum time `WebDriver` waits for page navigation to finish loading.
75    ///
76    /// Returning `None` means this timeout is not updated.
77    #[must_use]
78    pub const fn page_load_timeout(self) -> Option<Duration> {
79        self.page_load_timeout
80    }
81
82    /// Maximum time `WebDriver` waits while locating elements through raw element lookup commands.
83    ///
84    /// Returning `None` means this timeout is not updated.
85    #[must_use]
86    pub const fn implicit_wait_timeout(self) -> Option<Duration> {
87        self.implicit_wait_timeout
88    }
89
90    pub(crate) fn into_thirtyfour_timeout_configuration(self) -> thirtyfour::TimeoutConfiguration {
91        thirtyfour::TimeoutConfiguration::new(
92            self.script_timeout,
93            self.page_load_timeout,
94            self.implicit_wait_timeout,
95        )
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use assertr::prelude::*;
103
104    #[test]
105    fn builder_preserves_all_timeout_fields() {
106        let timeouts = BrowserTimeouts::builder()
107            .script_timeout(Duration::from_secs(5))
108            .page_load_timeout(Duration::from_secs(10))
109            .implicit_wait_timeout(Duration::from_secs(20))
110            .build();
111
112        assert_that!(timeouts.script_timeout()).is_equal_to(Some(Duration::from_secs(5)));
113        assert_that!(timeouts.page_load_timeout()).is_equal_to(Some(Duration::from_secs(10)));
114        assert_that!(timeouts.implicit_wait_timeout()).is_equal_to(Some(Duration::from_secs(20)));
115    }
116
117    #[test]
118    fn builder_leaves_unset_fields_unconfigured() {
119        let timeouts = BrowserTimeouts::builder().build();
120
121        assert_that!(timeouts.script_timeout()).is_none();
122        assert_that!(timeouts.page_load_timeout()).is_none();
123        assert_that!(timeouts.implicit_wait_timeout()).is_none();
124    }
125
126    #[test]
127    fn builder_accepts_wrapped_option_values() {
128        let timeouts = BrowserTimeouts::builder()
129            .script_timeout_opt(Some(Duration::from_secs(5)))
130            .page_load_timeout_opt(None)
131            .implicit_wait_timeout_opt(Some(Duration::ZERO))
132            .build();
133
134        assert_that!(timeouts.script_timeout()).is_equal_to(Some(Duration::from_secs(5)));
135        assert_that!(timeouts.page_load_timeout()).is_none();
136        assert_that!(timeouts.implicit_wait_timeout()).is_equal_to(Some(Duration::ZERO));
137    }
138
139    #[test]
140    fn conversion_preserves_unset_fields() {
141        let timeouts = BrowserTimeouts::builder()
142            .script_timeout(Duration::from_secs(5))
143            .build()
144            .into_thirtyfour_timeout_configuration();
145
146        assert_that!(timeouts.script()).is_equal_to(Some(Duration::from_secs(5)));
147        assert_that!(timeouts.page_load()).is_none();
148        assert_that!(timeouts.implicit()).is_none();
149    }
150}