chrome_for_testing_manager/
chromedriver.rs

1use crate::mgr::{ChromeForTestingManager, LoadedChromePackage, VersionRequest};
2use crate::port::{Port, PortRequest};
3use anyhow::anyhow;
4use chrome_for_testing::api::channel::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
12/// A wrapper struct for a spawned chromedriver process.
13/// Keep this alive until your test is complete.
14///
15/// Automatically terminates the spawned chromedriver process when dropped.
16///
17/// You can always manually call `terminate`, but the on-drop automation makes it much safer in
18/// quickly panicking contexts, such as tests.
19pub struct Chromedriver {
20    /// The manager instance used to resolve a version, download it and starting the chromedriver.
21    mgr: ChromeForTestingManager,
22
23    /// Chrome and chromedriver binaries used for testing.
24    loaded: LoadedChromePackage,
25
26    /// The running chromedriver process. Terminated when dropped.
27    ///
28    /// Always stores a process handle. The value is only taken out on termination,
29    /// notifying our `Drop` impl that the process was gracefully terminated when seeing `None`.
30    chromedriver_process: Option<TerminateOnDrop<BroadcastOutputStream>>,
31
32    /// The port the chromedriver process listens on.
33    chromedriver_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("chromedriver_process", &self.chromedriver_process)
42            .field("chromedriver_port", &self.chromedriver_port)
43            .finish()
44    }
45}
46
47impl Chromedriver {
48    pub async fn run(version: VersionRequest, port: PortRequest) -> anyhow::Result<Chromedriver> {
49        // Assert that async-drop will work.
50        // This is the only way of constructing a `Chromedriver` instance,
51        // so it's safe to do this here.
52        match tokio::runtime::Handle::current().runtime_flavor() {
53            RuntimeFlavor::MultiThread => { /* we want this */ }
54            unsupported_flavor => {
55                return Err(anyhow!(indoc::formatdoc! {r#"
56                    The Chromedriver type requires a multithreaded tokio runtime,
57                    as we rely on async-drop functionality not available on a single-threaded runtime.
58
59                    Detected runtime flavor: {unsupported_flavor:?}.
60
61                    If you are writing a test, annotate it with `#[tokio::test(flavor = "multi_thread")]`.
62                "#}));
63            }
64        }
65
66        let mgr = ChromeForTestingManager::new();
67        let selected = mgr.resolve_version(version).await?;
68        let loaded = mgr.download(selected).await?;
69        let (chromedriver_process, chromedriver_port) =
70            mgr.launch_chromedriver(&loaded, port).await?;
71        Ok(Chromedriver {
72            chromedriver_process: Some(
73                chromedriver_process
74                    .terminate_on_drop(Duration::from_secs(3), Duration::from_secs(3)),
75            ),
76            chromedriver_port,
77            loaded,
78            mgr,
79        })
80    }
81
82    pub async fn run_latest_stable() -> anyhow::Result<Chromedriver> {
83        Self::run(VersionRequest::LatestIn(Channel::Stable), PortRequest::Any).await
84    }
85
86    pub async fn run_latest_beta() -> anyhow::Result<Chromedriver> {
87        Self::run(VersionRequest::LatestIn(Channel::Beta), PortRequest::Any).await
88    }
89
90    pub async fn run_latest_dev() -> anyhow::Result<Chromedriver> {
91        Self::run(VersionRequest::LatestIn(Channel::Dev), PortRequest::Any).await
92    }
93
94    pub async fn run_latest_canary() -> anyhow::Result<Chromedriver> {
95        Self::run(VersionRequest::LatestIn(Channel::Canary), PortRequest::Any).await
96    }
97
98    pub async fn terminate(self) -> Result<ExitStatus, TerminationError> {
99        self.terminate_with_timeouts(Duration::from_secs(3), Duration::from_secs(3))
100            .await
101    }
102
103    pub async fn terminate_with_timeouts(
104        mut self,
105        interrupt_timeout: Duration,
106        terminate_timeout: Duration,
107    ) -> Result<ExitStatus, TerminationError> {
108        self.chromedriver_process
109            .take()
110            .expect("present")
111            .terminate(interrupt_timeout, terminate_timeout)
112            .await
113    }
114
115    /// Execute an async closure with a WebDriver session.
116    /// The session will be automatically cleaned up after the closure completes.
117    #[cfg(feature = "thirtyfour")]
118    pub async fn with_session(
119        &self,
120        f: impl AsyncFnOnce(&crate::session::Session) -> Result<(), crate::session::SessionError>,
121    ) -> anyhow::Result<()> {
122        self.with_custom_session(|_caps| Ok(()), f).await
123    }
124
125    /// Execute an async closure with a custom-configured WebDriver session.
126    /// The session will be automatically cleaned up after the closure completes.
127    #[cfg(feature = "thirtyfour")]
128    pub async fn with_custom_session<F>(
129        &self,
130        setup: impl Fn(
131            &mut thirtyfour::ChromeCapabilities,
132        ) -> Result<(), thirtyfour::prelude::WebDriverError>,
133        f: F,
134    ) -> anyhow::Result<()>
135    where
136        F: for<'a> AsyncFnOnce(
137            &'a crate::session::Session,
138        ) -> Result<(), crate::session::SessionError>,
139    {
140        use crate::session::Session;
141        use anyhow::Context;
142        use futures::FutureExt;
143
144        let mut caps = self.mgr.prepare_caps(&self.loaded).await?;
145        setup(&mut caps).context("Failed to set up chrome capabilities.")?;
146        let driver = thirtyfour::WebDriver::new(
147            format!("http://localhost:{}", self.chromedriver_port),
148            caps,
149        )
150        .await?;
151
152        let session = Session { driver };
153
154        // Execute the user function.
155        let maybe_panicked = core::panic::AssertUnwindSafe(f(&session))
156            .catch_unwind()
157            .await;
158
159        // No matter what happened, clean up the session!
160        session.quit().await?;
161
162        // Handle panics.
163        let result = maybe_panicked.map_err(|err| {
164            let err = anyhow::anyhow!("{err:?}");
165            crate::session::SessionError::Panic {
166                reason: err.to_string(),
167            }
168        })?;
169
170        // Map the `SessionError` into an `anyhow::Error`.
171        result.map_err(Into::into)
172    }
173}