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}