Skip to main content

browser_test/
test_case.rs

1use std::{borrow::Cow, fmt};
2
3use async_trait::async_trait;
4use rootcause::Report;
5use thirtyfour::WebDriver;
6
7use crate::{BrowserTimeouts, ElementQueryWaitConfig};
8
9/// A browser test that can run against one fresh `WebDriver` session.
10#[async_trait]
11pub trait BrowserTest<Context = (), TestError = rootcause::markers::Dynamic>: Send + Sync
12where
13    Context: Sync + ?Sized,
14    TestError: ?Sized,
15{
16    /// A human-readable test name for logs and failure context.
17    ///
18    /// The runner owns the returned name before running the test body, so implementations may
19    /// return either a borrowed name stored on the test or a freshly generated owned name.
20    fn name(&self) -> Cow<'_, str>;
21
22    /// Optional timeouts for this test.
23    ///
24    /// Returning `None` uses the runner's default timeout configuration, if one is set.
25    fn timeouts(&self) -> Option<BrowserTimeouts> {
26        None
27    }
28
29    /// Optional element query wait configuration for this test.
30    ///
31    /// Returning `None` uses the runner's default element query wait configuration, if one is set.
32    fn element_query_wait(&self) -> Option<ElementQueryWaitConfig> {
33        None
34    }
35
36    /// Execute the test body.
37    async fn run(&self, driver: &WebDriver, context: &Context) -> Result<(), Report<TestError>>;
38}
39
40/// A collection of browser tests for [`crate::BrowserTestRunner`].
41///
42/// This type erases each concrete test into the boxed trait object used by the runner while
43/// keeping call sites concise.
44///
45/// # Examples
46///
47/// ```rust,no_run
48/// # use std::borrow::Cow;
49/// # use browser_test::thirtyfour::WebDriver;
50/// # use browser_test::{async_trait, BrowserTest, BrowserTests};
51/// # use rootcause::Report;
52/// # struct OpensHomePage;
53///
54/// #[async_trait]
55/// impl BrowserTest for OpensHomePage {
56///     fn name(&self) -> Cow<'_, str> { "opens home page".into() }
57///     async fn run(&self, _driver: &WebDriver, _context: &()) -> Result<(), Report> { Ok(()) }
58/// }
59///
60/// struct SearchWorks;
61/// #[async_trait]
62/// impl BrowserTest for SearchWorks {
63///     fn name(&self) -> Cow<'_, str> { "search works".into() }
64///     async fn run(&self, _driver: &WebDriver, _context: &()) -> Result<(), Report> { Ok(()) }
65/// }
66///
67/// let tests = BrowserTests::new()
68///     .with(OpensHomePage)
69///     .with(SearchWorks);
70/// ```
71pub struct BrowserTests<Context = (), TestError = rootcause::markers::Dynamic>
72where
73    Context: Sync + ?Sized,
74    TestError: ?Sized,
75{
76    tests: Vec<Box<dyn BrowserTest<Context, TestError>>>,
77}
78
79impl<Context, TestError> BrowserTests<Context, TestError>
80where
81    Context: Sync + ?Sized,
82    TestError: ?Sized,
83{
84    /// Creates an empty browser test collection.
85    #[must_use]
86    pub const fn new() -> Self {
87        Self { tests: Vec::new() }
88    }
89
90    /// Adds a test and returns the collection for chaining.
91    #[must_use]
92    pub fn with<T>(mut self, test: T) -> Self
93    where
94        T: BrowserTest<Context, TestError> + 'static,
95    {
96        self.push(test);
97        self
98    }
99
100    /// Adds a test to the collection.
101    pub fn push<T>(&mut self, test: T) -> &mut Self
102    where
103        T: BrowserTest<Context, TestError> + 'static,
104    {
105        self.tests.push(Box::new(test));
106        self
107    }
108
109    /// Returns `true` if the collection contains no tests.
110    #[must_use]
111    pub fn is_empty(&self) -> bool {
112        self.tests.is_empty()
113    }
114
115    pub(crate) fn into_vec(self) -> Vec<Box<dyn BrowserTest<Context, TestError>>> {
116        self.tests
117    }
118}
119
120impl<Context, TestError> Default for BrowserTests<Context, TestError>
121where
122    Context: Sync + ?Sized,
123    TestError: ?Sized,
124{
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130impl<Context, TestError> fmt::Debug for BrowserTests<Context, TestError>
131where
132    Context: Sync + ?Sized,
133    TestError: ?Sized,
134{
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        f.debug_struct("BrowserTests")
137            .field("tests", &BrowserTestNames(&self.tests))
138            .finish()
139    }
140}
141
142struct BrowserTestNames<'a, Context, TestError>(&'a [Box<dyn BrowserTest<Context, TestError>>])
143where
144    Context: Sync + ?Sized,
145    TestError: ?Sized;
146
147impl<Context, TestError> fmt::Debug for BrowserTestNames<'_, Context, TestError>
148where
149    Context: Sync + ?Sized,
150    TestError: ?Sized,
151{
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        f.debug_list()
154            .entries(self.0.iter().map(|test| test.name()))
155            .finish()
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    struct NamedTest(&'static str);
164
165    #[async_trait::async_trait]
166    impl BrowserTest for NamedTest {
167        fn name(&self) -> Cow<'_, str> {
168            Cow::Borrowed(self.0)
169        }
170
171        async fn run(&self, _driver: &WebDriver, _context: &()) -> Result<(), Report> {
172            Ok(())
173        }
174    }
175
176    #[test]
177    fn browser_tests_debug_prints_test_names() {
178        let tests = BrowserTests::new()
179            .with(NamedTest("opens home page"))
180            .with(NamedTest("search works"));
181
182        assert_eq!(
183            format!("{tests:?}"),
184            r#"BrowserTests { tests: ["opens home page", "search works"] }"#
185        );
186    }
187}