chrome_for_testing_manager/
session_builder.rs1use crate::ChromeForTestingManagerError;
2use crate::chromedriver::Chromedriver;
3use crate::mgr::{HeadlessShellSession, LoadedBrowserPackage};
4use crate::session::Session;
5use rootcause::prelude::ResultExt;
6use rootcause::{IntoReportCollection, Report, markers::SendSync};
7use thirtyfour::ChromiumLikeCapabilities;
8use thirtyfour::prelude::WebDriverError;
9use thirtyfour::{ChromeCapabilities, WebDriverBuilder};
10
11#[doc(hidden)]
13#[derive(Debug, Clone, Copy)]
14pub struct InitialCaps;
15
16#[doc(hidden)]
18#[derive(Debug, Clone, Copy)]
19pub struct InitialConfig;
20
21#[doc(hidden)]
23pub struct CapsSetup<F>(F);
24
25#[doc(hidden)]
27pub struct ConfigSetup<F>(F);
28
29mod sealed {
30 pub trait Sealed {}
31}
32
33#[doc(hidden)]
34pub trait ApplyCaps: sealed::Sealed {
35 fn apply(self, caps: &mut ChromeCapabilities) -> Result<(), WebDriverError>;
36}
37
38impl sealed::Sealed for InitialCaps {}
39impl ApplyCaps for InitialCaps {
40 fn apply(self, _caps: &mut ChromeCapabilities) -> Result<(), WebDriverError> {
41 Ok(())
42 }
43}
44
45impl<F> sealed::Sealed for CapsSetup<F> {}
46impl<F> ApplyCaps for CapsSetup<F>
47where
48 F: FnOnce(&mut ChromeCapabilities) -> Result<(), WebDriverError>,
49{
50 fn apply(self, caps: &mut ChromeCapabilities) -> Result<(), WebDriverError> {
51 self.0(caps)
52 }
53}
54
55#[doc(hidden)]
56pub trait ApplyConfig: sealed::Sealed {
57 fn apply(self, builder: WebDriverBuilder) -> WebDriverBuilder;
58}
59
60impl sealed::Sealed for InitialConfig {}
61impl ApplyConfig for InitialConfig {
62 fn apply(self, builder: WebDriverBuilder) -> WebDriverBuilder {
63 builder
64 }
65}
66
67impl<F> sealed::Sealed for ConfigSetup<F> {}
68impl<F> ApplyConfig for ConfigSetup<F>
69where
70 F: FnOnce(WebDriverBuilder) -> WebDriverBuilder,
71{
72 fn apply(self, builder: WebDriverBuilder) -> WebDriverBuilder {
73 self.0(builder)
74 }
75}
76
77pub struct SessionBuilder<'a, C, B> {
90 chromedriver: &'a Chromedriver,
91 caps_setup: C,
92 config_setup: B,
93}
94
95impl<'a> SessionBuilder<'a, InitialCaps, InitialConfig> {
96 pub(crate) fn new(chromedriver: &'a Chromedriver) -> Self {
97 Self {
98 chromedriver,
99 caps_setup: InitialCaps,
100 config_setup: InitialConfig,
101 }
102 }
103}
104
105impl<'a, B> SessionBuilder<'a, InitialCaps, B> {
106 pub fn with_caps<F>(self, f: F) -> SessionBuilder<'a, CapsSetup<F>, B>
108 where
109 F: FnOnce(&mut ChromeCapabilities) -> Result<(), WebDriverError>,
110 {
111 SessionBuilder {
112 chromedriver: self.chromedriver,
113 caps_setup: CapsSetup(f),
114 config_setup: self.config_setup,
115 }
116 }
117}
118
119impl<'a, C> SessionBuilder<'a, C, InitialConfig> {
120 pub fn with_config<F>(self, f: F) -> SessionBuilder<'a, C, ConfigSetup<F>>
122 where
123 F: FnOnce(WebDriverBuilder) -> WebDriverBuilder,
124 {
125 SessionBuilder {
126 chromedriver: self.chromedriver,
127 caps_setup: self.caps_setup,
128 config_setup: ConfigSetup(f),
129 }
130 }
131}
132
133impl<C, B> SessionBuilder<'_, C, B>
134where
135 C: ApplyCaps,
136 B: ApplyConfig,
137{
138 pub async fn run<T, E, F>(self, f: F) -> Result<T, Report<ChromeForTestingManagerError>>
150 where
151 F: for<'b> AsyncFnOnce(&'b Session) -> Result<T, E>,
152 E: IntoReportCollection<SendSync>,
153 {
154 use futures::FutureExt;
155
156 let chromedriver = self.chromedriver;
157 let port = chromedriver.port();
158
159 let mut caps = chromedriver.mgr.prepare_caps(&chromedriver.loaded)?;
160 self.caps_setup
161 .apply(&mut caps)
162 .context(ChromeForTestingManagerError::ConfigureSessionCapabilities)?;
163 let mut headless_shell = match &chromedriver.loaded {
164 LoadedBrowserPackage::Chrome(_) => None,
165 LoadedBrowserPackage::ChromeHeadlessShell(headless_shell_package) => {
166 let headless_shell = chromedriver
167 .mgr
168 .launch_headless_shell_session(
169 headless_shell_package,
170 &caps,
171 chromedriver.graceful_shutdown.clone(),
172 )
173 .await?;
174
175 caps.set_debugger_address(headless_shell.debugger_address())
176 .context(ChromeForTestingManagerError::ConfigureSessionCapabilities)?;
177
178 Some(headless_shell)
179 }
180 };
181 let builder = thirtyfour::WebDriver::builder(format!("http://localhost:{port}"), caps);
182 let driver = match self
183 .config_setup
184 .apply(builder)
185 .connect()
186 .await
187 .context(ChromeForTestingManagerError::StartWebDriverSession { port })
188 {
189 Ok(driver) => driver,
190 Err(err) => {
191 if let Err(termination_err) = terminate_headless_shell(headless_shell.take()).await
192 {
193 tracing::warn!(
194 error = %termination_err,
195 "failed to terminate Chrome Headless Shell after WebDriver session start failure"
196 );
197 }
198 return Err(err);
199 }
200 };
201
202 let session = Session { driver };
203
204 let maybe_panicked = core::panic::AssertUnwindSafe(f(&session))
205 .catch_unwind()
206 .await;
207
208 let user_result = match maybe_panicked {
209 Ok(result) => result.context(ChromeForTestingManagerError::RunSessionCallback),
210 Err(payload) => {
211 if let Err(quit_err) = session.quit().await {
212 tracing::error!(
213 "Failed to quit WebDriver session after user callback panic: {quit_err:?}"
214 );
215 }
216 if let Err(terminate_err) = terminate_headless_shell(headless_shell).await {
217 tracing::error!(
218 "Failed to terminate Chrome Headless Shell after user callback panic: {terminate_err:?}"
219 );
220 }
221 std::panic::resume_unwind(payload);
222 }
223 };
224
225 let quit_result = session.quit().await;
226 let browser_termination_result = terminate_headless_shell(headless_shell).await;
227
228 let session_result = match (user_result, quit_result) {
229 (Ok(value), Ok(())) => Ok(value),
230 (Ok(_), Err(quit_err)) => Err(quit_err),
231 (Err(user_err), Ok(())) => Err(user_err),
232 (Err(mut user_err), Err(quit_err)) => {
233 tracing::error!(
234 "Failed to quit WebDriver session after user failure: {quit_err:?}"
235 );
236 user_err
237 .children_mut()
238 .push(quit_err.into_dynamic().into_cloneable());
239 Err(user_err)
240 }
241 };
242
243 match (session_result, browser_termination_result) {
244 (Ok(value), Ok(())) => Ok(value),
245 (Ok(_), Err(terminate_err)) => Err(terminate_err),
246 (Err(session_err), Ok(())) => Err(session_err),
247 (Err(mut session_err), Err(terminate_err)) => {
248 tracing::error!(
249 "Failed to terminate Chrome Headless Shell after session failure: {terminate_err:?}"
250 );
251 session_err
252 .children_mut()
253 .push(terminate_err.into_dynamic().into_cloneable());
254 Err(session_err)
255 }
256 }
257 }
258}
259
260async fn terminate_headless_shell(
261 headless_shell: Option<HeadlessShellSession>,
262) -> Result<(), Report<ChromeForTestingManagerError>> {
263 if let Some(headless_shell) = headless_shell {
264 headless_shell.terminate().await?;
265 }
266 Ok(())
267}