Skip to main content

chrome_for_testing_manager/
session_builder.rs

1use crate::ChromeForTestingManagerError;
2use crate::chromedriver::Chromedriver;
3use crate::session::Session;
4use rootcause::prelude::ResultExt;
5use rootcause::{IntoReportCollection, Report, markers::SendSync};
6use thirtyfour::prelude::WebDriverError;
7use thirtyfour::{ChromeCapabilities, WebDriverBuilder};
8
9/// Type-state marker: no capability customization will be applied to the session.
10#[doc(hidden)]
11#[derive(Debug, Clone, Copy)]
12pub struct DefaultCaps;
13
14/// Type-state marker: no `WebDriverBuilder` customization will be applied to the session.
15#[doc(hidden)]
16#[derive(Debug, Clone, Copy)]
17pub struct DefaultConfig;
18
19/// Type-state wrapper carrying a user-provided capabilities setup closure.
20#[doc(hidden)]
21pub struct CapsSetup<F>(F);
22
23/// Type-state wrapper carrying a user-provided `WebDriverBuilder` setup closure.
24#[doc(hidden)]
25pub struct ConfigSetup<F>(F);
26
27mod sealed {
28    pub trait Sealed {}
29}
30
31#[doc(hidden)]
32pub trait ApplyCaps: sealed::Sealed {
33    fn apply(self, caps: &mut ChromeCapabilities) -> Result<(), WebDriverError>;
34}
35
36impl sealed::Sealed for DefaultCaps {}
37impl ApplyCaps for DefaultCaps {
38    fn apply(self, _caps: &mut ChromeCapabilities) -> Result<(), WebDriverError> {
39        Ok(())
40    }
41}
42
43impl<F> sealed::Sealed for CapsSetup<F> {}
44impl<F> ApplyCaps for CapsSetup<F>
45where
46    F: FnOnce(&mut ChromeCapabilities) -> Result<(), WebDriverError>,
47{
48    fn apply(self, caps: &mut ChromeCapabilities) -> Result<(), WebDriverError> {
49        self.0(caps)
50    }
51}
52
53#[doc(hidden)]
54pub trait ApplyConfig: sealed::Sealed {
55    fn apply(self, builder: WebDriverBuilder) -> WebDriverBuilder;
56}
57
58impl sealed::Sealed for DefaultConfig {}
59impl ApplyConfig for DefaultConfig {
60    fn apply(self, builder: WebDriverBuilder) -> WebDriverBuilder {
61        builder
62    }
63}
64
65impl<F> sealed::Sealed for ConfigSetup<F> {}
66impl<F> ApplyConfig for ConfigSetup<F>
67where
68    F: FnOnce(WebDriverBuilder) -> WebDriverBuilder,
69{
70    fn apply(self, builder: WebDriverBuilder) -> WebDriverBuilder {
71        self.0(builder)
72    }
73}
74
75/// A scoped, chainable builder for opening a `thirtyfour` [`Session`] against a running
76/// [`Chromedriver`].
77///
78/// Obtained via [`Chromedriver::session`]. Optional setup steps:
79///
80/// - [`Self::with_caps`] mutates the [`ChromeCapabilities`] before the session opens (e.g. unset
81///   headless, add Chrome args).
82/// - [`Self::with_config`] receives the [`WebDriverBuilder`] and may configure the element poller,
83///   request timeout, user-agent, or keep-alive flag.
84///
85/// Call [`Self::run`] to open the session and execute the user closure inside scoped, panic-safe
86/// cleanup that always calls `WebDriver::quit().await`.
87pub struct SessionBuilder<'a, C, B> {
88    chromedriver: &'a Chromedriver,
89    caps_setup: C,
90    config_setup: B,
91}
92
93impl<'a> SessionBuilder<'a, DefaultCaps, DefaultConfig> {
94    pub(crate) fn new(chromedriver: &'a Chromedriver) -> Self {
95        Self {
96            chromedriver,
97            caps_setup: DefaultCaps,
98            config_setup: DefaultConfig,
99        }
100    }
101}
102
103impl<'a, B> SessionBuilder<'a, DefaultCaps, B> {
104    /// Provide a closure that mutates the [`ChromeCapabilities`] used to create the session.
105    pub fn with_caps<F>(self, f: F) -> SessionBuilder<'a, CapsSetup<F>, B>
106    where
107        F: FnOnce(&mut ChromeCapabilities) -> Result<(), WebDriverError>,
108    {
109        SessionBuilder {
110            chromedriver: self.chromedriver,
111            caps_setup: CapsSetup(f),
112            config_setup: self.config_setup,
113        }
114    }
115}
116
117impl<'a, C> SessionBuilder<'a, C, DefaultConfig> {
118    /// Provide a closure that configures the [`WebDriverBuilder`] before the session is opened.
119    pub fn with_config<F>(self, f: F) -> SessionBuilder<'a, C, ConfigSetup<F>>
120    where
121        F: FnOnce(WebDriverBuilder) -> WebDriverBuilder,
122    {
123        SessionBuilder {
124            chromedriver: self.chromedriver,
125            caps_setup: self.caps_setup,
126            config_setup: ConfigSetup(f),
127        }
128    }
129}
130
131impl<C, B> SessionBuilder<'_, C, B>
132where
133    C: ApplyCaps,
134    B: ApplyConfig,
135{
136    /// Open a [`Session`], hand it to the user closure, and tear it down once the closure resolves
137    /// or panics.
138    ///
139    /// Cleanup runs regardless of outcome. A panic in the user closure is caught, the session is
140    /// quit, and the panic is resumed afterwards.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if capability setup, session creation, the user closure, or the quit call
145    /// fails. A user error combined with a quit error is reported with the quit error attached as
146    /// a child.
147    pub async fn run<T, E, F>(self, f: F) -> Result<T, Report<ChromeForTestingManagerError>>
148    where
149        F: for<'b> AsyncFnOnce(&'b Session) -> Result<T, E>,
150        E: IntoReportCollection<SendSync>,
151    {
152        use futures::FutureExt;
153
154        let chromedriver = self.chromedriver;
155        let port = chromedriver.port();
156        let mut caps = chromedriver.mgr.prepare_caps(&chromedriver.loaded)?;
157        self.caps_setup
158            .apply(&mut caps)
159            .context(ChromeForTestingManagerError::ConfigureSessionCapabilities)?;
160        let builder = thirtyfour::WebDriver::builder(format!("http://localhost:{port}"), caps);
161        let driver = self
162            .config_setup
163            .apply(builder)
164            .connect()
165            .await
166            .context(ChromeForTestingManagerError::StartWebDriverSession { port })?;
167
168        let session = Session { driver };
169
170        let maybe_panicked = core::panic::AssertUnwindSafe(f(&session))
171            .catch_unwind()
172            .await;
173
174        let user_result = match maybe_panicked {
175            Ok(result) => result.context(ChromeForTestingManagerError::RunSessionCallback),
176            Err(payload) => {
177                if let Err(quit_err) = session.quit().await {
178                    tracing::error!(
179                        "Failed to quit WebDriver session after user callback panic: {quit_err:?}"
180                    );
181                }
182                std::panic::resume_unwind(payload);
183            }
184        };
185
186        let quit_result = session.quit().await;
187
188        match (user_result, quit_result) {
189            (Ok(value), Ok(())) => Ok(value),
190            (Ok(_), Err(quit_err)) => Err(quit_err),
191            (Err(user_err), Ok(())) => Err(user_err),
192            (Err(mut user_err), Err(quit_err)) => {
193                tracing::error!(
194                    "Failed to quit WebDriver session after user failure: {quit_err:?}"
195                );
196                user_err
197                    .children_mut()
198                    .push(quit_err.into_dynamic().into_cloneable());
199                Err(user_err)
200            }
201        }
202    }
203}