chrome_for_testing_manager/
chromedriver.rs1use crate::mgr::{ChromeForTestingManager, LoadedChromePackage, VersionRequest};
2use crate::port::{Port, PortRequest};
3use anyhow::anyhow;
4use chrome_for_testing::Channel;
5use std::fmt::{Debug, Formatter};
6use std::process::ExitStatus;
7use std::time::Duration;
8use tokio::runtime::RuntimeFlavor;
9use tokio_process_tools::broadcast::BroadcastOutputStream;
10use tokio_process_tools::{TerminateOnDrop, TerminationError};
11
12pub struct Chromedriver {
20 mgr: ChromeForTestingManager,
22
23 loaded: LoadedChromePackage,
25
26 process: Option<TerminateOnDrop<BroadcastOutputStream>>,
31
32 port: Port,
34}
35
36impl Debug for Chromedriver {
37 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38 f.debug_struct("Chromedriver")
39 .field("mgr", &self.mgr)
40 .field("loaded", &self.loaded)
41 .field("process", &self.process)
42 .field("port", &self.port)
43 .finish()
44 }
45}
46
47impl Chromedriver {
48 pub async fn run(version: VersionRequest, port: PortRequest) -> anyhow::Result<Chromedriver> {
55 match tokio::runtime::Handle::current().runtime_flavor() {
59 RuntimeFlavor::MultiThread => { }
60 unsupported_flavor => {
61 return Err(anyhow!(indoc::formatdoc! {r#"
62 The Chromedriver type requires a multithreaded tokio runtime,
63 as we rely on async-drop functionality not available on a single-threaded runtime.
64
65 Detected runtime flavor: {unsupported_flavor:?}.
66
67 If you are writing a test, annotate it with `#[tokio::test(flavor = "multi_thread")]`.
68 "#}));
69 }
70 }
71
72 let mgr = ChromeForTestingManager::new()?;
73 let selected = mgr.resolve_version(version).await?;
74 let loaded = mgr.download(selected).await?;
75 let (process_handle, actual_port) = mgr.launch_chromedriver(&loaded, port).await?;
76 Ok(Chromedriver {
77 process: Some(
78 process_handle.terminate_on_drop(Duration::from_secs(3), Duration::from_secs(3)),
79 ),
80 port: actual_port,
81 loaded,
82 mgr,
83 })
84 }
85
86 pub async fn run_latest_stable() -> anyhow::Result<Chromedriver> {
92 Self::run(VersionRequest::LatestIn(Channel::Stable), PortRequest::Any).await
93 }
94
95 pub async fn run_latest_beta() -> anyhow::Result<Chromedriver> {
101 Self::run(VersionRequest::LatestIn(Channel::Beta), PortRequest::Any).await
102 }
103
104 pub async fn run_latest_dev() -> anyhow::Result<Chromedriver> {
110 Self::run(VersionRequest::LatestIn(Channel::Dev), PortRequest::Any).await
111 }
112
113 pub async fn run_latest_canary() -> anyhow::Result<Chromedriver> {
119 Self::run(VersionRequest::LatestIn(Channel::Canary), PortRequest::Any).await
120 }
121
122 pub async fn terminate(self) -> Result<ExitStatus, TerminationError> {
128 self.terminate_with_timeouts(Duration::from_secs(3), Duration::from_secs(3))
129 .await
130 }
131
132 #[expect(clippy::missing_panics_doc)] pub async fn terminate_with_timeouts(
139 mut self,
140 interrupt_timeout: Duration,
141 terminate_timeout: Duration,
142 ) -> Result<ExitStatus, TerminationError> {
143 self.process
144 .take()
145 .expect("present")
146 .terminate(interrupt_timeout, terminate_timeout)
147 .await
148 }
149
150 #[cfg(feature = "thirtyfour")]
157 pub async fn with_session(
158 &self,
159 f: impl AsyncFnOnce(&crate::session::Session) -> Result<(), crate::session::SessionError>,
160 ) -> anyhow::Result<()> {
161 self.with_custom_session(|_caps| Ok(()), f).await
162 }
163
164 #[cfg(feature = "thirtyfour")]
171 pub async fn with_custom_session<F>(
172 &self,
173 setup: impl Fn(
174 &mut thirtyfour::ChromeCapabilities,
175 ) -> Result<(), thirtyfour::prelude::WebDriverError>,
176 f: F,
177 ) -> anyhow::Result<()>
178 where
179 F: for<'a> AsyncFnOnce(
180 &'a crate::session::Session,
181 ) -> Result<(), crate::session::SessionError>,
182 {
183 use crate::session::Session;
184 use anyhow::Context;
185 use futures::FutureExt;
186
187 let mut caps = self.mgr.prepare_caps(&self.loaded)?;
188 setup(&mut caps).context("Failed to set up chrome capabilities.")?;
189 let driver =
190 thirtyfour::WebDriver::new(format!("http://localhost:{}", self.port), caps).await?;
191
192 let session = Session { driver };
193
194 let maybe_panicked = core::panic::AssertUnwindSafe(f(&session))
196 .catch_unwind()
197 .await;
198
199 session.quit().await?;
201
202 let result = maybe_panicked.map_err(|err| {
204 let err = anyhow::anyhow!("{err:?}");
205 crate::session::SessionError::Panic {
206 reason: err.to_string(),
207 }
208 })?;
209
210 result.map_err(Into::into)
212 }
213}