testcontainers_modules/selenium/
mod.rs

1use testcontainers::{
2    core::{ContainerPort, WaitFor},
3    Image,
4};
5
6const FIREFOX_IMAGE: &str = "selenium/standalone-firefox";
7const FIREFOX_TAG: &str = "144.0-geckodriver-0.36-grid-4.38.0-20251101";
8
9const CHROME_IMAGE: &str = "selenium/standalone-chrome";
10const CHROME_TAG: &str = "136.0-chromedriver-136.0-grid-4.38.0-20251101";
11
12/// Port where the Selenium Grid is listening.
13pub const DRIVER_PORT: ContainerPort = ContainerPort::Tcp(4444);
14/// Port where the noVNC WebUI is listening.
15pub const WEB_UI_PORT: ContainerPort = ContainerPort::Tcp(7900);
16
17/// Module to work with [Selenium] inside of tests.
18///
19/// Starts an instance of Selenium Standalone (Firefox or Chrome). This uses either:
20/// - the official [`selenium/standalone-firefox` docker image], or
21/// - the official [`selenium/standalone-chrome` docker image].
22///
23/// # Example
24///
25/// ```
26/// use testcontainers_modules::{
27///     selenium::{self, Selenium},
28///     testcontainers::runners::SyncRunner,
29/// };
30///
31/// let selenium_instance = Selenium::new_firefox().start().unwrap();
32///
33/// let driver_port = selenium_instance
34///     .get_host_port_ipv4(selenium::DRIVER_PORT)
35///     .unwrap();
36/// let web_ui_port = selenium_instance
37///     .get_host_port_ipv4(selenium::WEB_UI_PORT)
38///     .unwrap();
39///
40/// let driver_url = format!("http://localhost:{driver_port}");
41/// let web_ui_url = format!("http://localhost:{web_ui_port}");
42/// ```
43///
44/// [Selenium]: https://www.selenium.dev/
45/// [`selenium/standalone-firefox` docker image]: https://hub.docker.com/r/selenium/standalone-firefox
46/// [`selenium/standalone-chrome` docker image]: https://hub.docker.com/r/selenium/standalone-chrome
47#[derive(Debug, Clone)]
48pub struct Selenium {
49    image: String,
50    tag: String,
51}
52
53impl Selenium {
54    /// Creates a new instance of a Selenium Standalone Firefox container.
55    ///
56    /// Image: [`selenium/standalone-firefox`](https://hub.docker.com/r/selenium/standalone-firefox)
57    pub fn new_firefox() -> Self {
58        Self {
59            image: FIREFOX_IMAGE.to_owned(),
60            tag: FIREFOX_TAG.to_owned(),
61        }
62    }
63
64    /// Creates a new instance of a Selenium Standalone Chrome container.
65    ///
66    /// Image: [`selenium/standalone-chrome`](https://hub.docker.com/r/selenium/standalone-chrome)
67    pub fn new_chrome() -> Self {
68        Self {
69            image: CHROME_IMAGE.to_owned(),
70            tag: CHROME_TAG.to_owned(),
71        }
72    }
73}
74
75impl Default for Selenium {
76    fn default() -> Self {
77        Self::new_firefox()
78    }
79}
80
81impl Image for Selenium {
82    fn name(&self) -> &str {
83        &self.image
84    }
85
86    fn tag(&self) -> &str {
87        &self.tag
88    }
89
90    fn ready_conditions(&self) -> Vec<WaitFor> {
91        vec![WaitFor::message_on_stdout("Started Selenium Standalone")]
92    }
93
94    fn expose_ports(&self) -> &[ContainerPort] {
95        &[DRIVER_PORT, WEB_UI_PORT]
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use testcontainers::runners::AsyncRunner;
102
103    use super::*;
104
105    #[tokio::test]
106    async fn selenium_firefox_check_status() -> Result<(), Box<dyn std::error::Error + 'static>> {
107        let selenium = Selenium::new_firefox();
108        check_status_impl(selenium).await?;
109        Ok(())
110    }
111
112    #[tokio::test]
113    async fn selenium_chrome_check_status() -> Result<(), Box<dyn std::error::Error + 'static>> {
114        let selenium = Selenium::new_chrome();
115        check_status_impl(selenium).await?;
116        Ok(())
117    }
118
119    async fn check_status_impl(
120        selenium: Selenium,
121    ) -> Result<(), Box<dyn std::error::Error + 'static>> {
122        let node = selenium.start().await?;
123
124        let driver_port = node.get_host_port_ipv4(DRIVER_PORT).await?;
125        let web_ui_port = node.get_host_port_ipv4(WEB_UI_PORT).await?;
126
127        // Check Selenium Grid Status
128        let status_url = format!("http://127.0.0.1:{driver_port}/status");
129        let response = reqwest::get(&status_url).await?;
130        assert!(response.status().is_success());
131        let body = response.text().await?;
132        assert!(body.contains("\"ready\": true"));
133
134        // Check WebUI
135        let web_ui_url = format!("http://127.0.0.1:{web_ui_port}");
136        let response = reqwest::get(&web_ui_url).await?;
137        assert!(response.status().is_success());
138
139        Ok(())
140    }
141
142    #[tokio::test]
143    async fn selenium_firefox_run_js() -> Result<(), Box<dyn std::error::Error + 'static>> {
144        let node = Selenium::new_firefox().start().await?;
145        let driver_port = node.get_host_port_ipv4(DRIVER_PORT).await?;
146        let driver_url = format!("http://127.0.0.1:{driver_port}");
147
148        let client = fantoccini::ClientBuilder::native()
149            .connect(&driver_url)
150            .await?;
151
152        let result = client.execute("return 1 + 1", vec![]).await?;
153        let value = result.as_i64().unwrap();
154        assert_eq!(value, 2);
155
156        client.close().await?;
157
158        Ok(())
159    }
160}