Skip to content

Commit 188c9c2

Browse files
authored
Add Selenium container implementation under org.testcontainers.selenium (#11096)
1 parent 1540256 commit 188c9c2

13 files changed

+342
-74
lines changed

docs/modules/webdriver_containers.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,22 @@ every test.
2323

2424
The following field in your JUnit UI test class will prepare a container running Chrome:
2525
<!--codeinclude-->
26-
[Chrome](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeWebDriverContainerTest.java) inside_block:junitRule
26+
[Chrome](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeWebDriverContainerTest.java) inside_block:junitRule
2727
<!--/codeinclude-->
2828

2929
3030
Now, instead of instantiating an instance of WebDriver directly, use the following to obtain an instance inside your
3131
test methods:
3232
<!--codeinclude-->
33-
[RemoteWebDriver](../../modules/selenium/src/test/java/org/testcontainers/junit/LocalServerWebDriverContainerTest.java) inside_block:getWebDriver
33+
[RemoteWebDriver](../../modules/selenium/src/test/java/org/testcontainers/selenium/LocalServerWebDriverContainerTest.java) inside_block:getWebDriver
3434
<!--/codeinclude-->
3535

3636
You can then use this driver instance like a regular WebDriver.
3737

3838
Note that, if you want to test a **web application running on the host machine** (the machine the JUnit tests are
3939
running on - which is quite likely), you'll need to use [the host exposing](../features/networking.md#exposing-host-ports-to-the-container) feature of Testcontainers, e.g.:
4040
<!--codeinclude-->
41-
[Open Web Page](../../modules/selenium/src/test/java/org/testcontainers/junit/LocalServerWebDriverContainerTest.java) inside_block:getPage
41+
[Open Web Page](../../modules/selenium/src/test/java/org/testcontainers/selenium/LocalServerWebDriverContainerTest.java) inside_block:getPage
4242
<!--/codeinclude-->
4343

4444

@@ -48,9 +48,9 @@ running on - which is quite likely), you'll need to use [the host exposing](../f
4848

4949
At the moment, Chrome, Firefox and Edge are supported. To switch, simply change the first parameter to the rule constructor:
5050
<!--codeinclude-->
51-
[Chrome](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeWebDriverContainerTest.java) inside_block:junitRule
52-
[Firefox](../../modules/selenium/src/test/java/org/testcontainers/junit/FirefoxWebDriverContainerTest.java) inside_block:junitRule
53-
[Edge](../../modules/selenium/src/test/java/org/testcontainers/junit/EdgeWebDriverContainerTest.java) inside_block:junitRule
51+
[Chrome](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeWebDriverContainerTest.java) inside_block:junitRule
52+
[Firefox](../../modules/selenium/src/test/java/org/testcontainers/selenium/FirefoxWebDriverContainerTest.java) inside_block:junitRule
53+
[Edge](../../modules/selenium/src/test/java/org/testcontainers/selenium/EdgeWebDriverContainerTest.java) inside_block:junitRule
5454
<!--/codeinclude-->
5555

5656
### Recording videos
@@ -59,30 +59,30 @@ By default, no videos will be recorded. However, you can instruct Testcontainers
5959
just for failing tests.
6060

6161
<!--codeinclude-->
62-
[Record all Tests](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordAll
63-
[Record failing Tests](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFailing
62+
[Record all Tests](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:recordAll
63+
[Record failing Tests](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFailing
6464
<!--/codeinclude-->
6565

6666
Note that the second parameter of `withRecordingMode` should be a directory where recordings can be saved.
6767

6868
By default, the video will be recorded in [FLV](https://en.wikipedia.org/wiki/Flash_Video) format, but you can specify it explicitly or change it to [MP4](https://en.wikipedia.org/wiki/MPEG-4_Part_14) using `withRecordingMode` method with `VncRecordingFormat` option:
6969

7070
<!--codeinclude-->
71-
[Video Format in MP4](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordMp4
72-
[Video Format in FLV](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFlv
71+
[Video Format in MP4](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:recordMp4
72+
[Video Format in FLV](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFlv
7373
<!--/codeinclude-->
7474

7575
If you would like to customise the file name of the recording, or provide a different directory at runtime based on the description of the test and/or its success or failure, you may provide a custom recording file factory as follows:
7676
<!--codeinclude-->
77-
[CustomRecordingFileFactory](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:withRecordingFileFactory
77+
[CustomRecordingFileFactory](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:withRecordingFileFactory
7878
<!--/codeinclude-->
7979

8080

8181
Note the factory must implement `org.testcontainers.containers.RecordingFileFactory`.
8282

8383
## More examples
8484

85-
A few different examples are shown in [ChromeWebDriverContainerTest.java](https://github.com/testcontainers/testcontainers-java/blob/main/modules/selenium/src/test/java/org/testcontainers/junit/ChromeWebDriverContainerTest.java).
85+
A few different examples are shown in [ChromeWebDriverContainerTest.java](https://github.com/testcontainers/testcontainers-java/blob/main/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeWebDriverContainerTest.java).
8686

8787
## Adding this module to your project dependencies
8888

modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@
4646
* {@code selenium/standalone-edge}, {@code selenium/standalone-chrome-debug}, {@code selenium/standalone-firefox-debug}
4747
* <p>
4848
* Exposed ports: 4444
49+
*
50+
* @deprecated use {@link org.testcontainers.selenium.BrowserWebDriverContainer} instead.
4951
*/
52+
@Deprecated
5053
public class BrowserWebDriverContainer<SELF extends BrowserWebDriverContainer<SELF>>
5154
extends GenericContainer<SELF>
5255
implements LinkableContainer, TestLifecycleAware {
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package org.testcontainers.selenium;
2+
3+
import com.github.dockerjava.api.command.InspectContainerResponse;
4+
import com.github.dockerjava.api.model.AccessMode;
5+
import com.github.dockerjava.api.model.Bind;
6+
import com.github.dockerjava.api.model.Volume;
7+
import com.google.common.collect.ImmutableSet;
8+
import org.apache.commons.io.FileUtils;
9+
import org.apache.commons.lang3.SystemUtils;
10+
import org.jetbrains.annotations.NotNull;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
import org.testcontainers.containers.ContainerLaunchException;
14+
import org.testcontainers.containers.DefaultRecordingFileFactory;
15+
import org.testcontainers.containers.GenericContainer;
16+
import org.testcontainers.containers.Network;
17+
import org.testcontainers.containers.RecordingFileFactory;
18+
import org.testcontainers.containers.VncRecordingContainer;
19+
import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat;
20+
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
21+
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
22+
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
23+
import org.testcontainers.containers.wait.strategy.WaitStrategy;
24+
import org.testcontainers.lifecycle.TestDescription;
25+
import org.testcontainers.lifecycle.TestLifecycleAware;
26+
import org.testcontainers.utility.DockerImageName;
27+
28+
import java.io.File;
29+
import java.io.IOException;
30+
import java.net.MalformedURLException;
31+
import java.net.URL;
32+
import java.nio.file.Files;
33+
import java.time.Duration;
34+
import java.util.Optional;
35+
import java.util.Set;
36+
37+
/**
38+
* A chrome/firefox/custom container based on SeleniumHQ's standalone container sets.
39+
* <p>
40+
* Supported images: {@code selenium/standalone-chrome}, {@code selenium/standalone-firefox},
41+
* {@code selenium/standalone-edge}, {@code selenium/standalone-chrome-debug}, {@code selenium/standalone-firefox-debug}
42+
* <p>
43+
* Exposed ports: 4444
44+
*/
45+
public class BrowserWebDriverContainer
46+
extends GenericContainer<BrowserWebDriverContainer>
47+
implements TestLifecycleAware {
48+
49+
private static final DockerImageName CHROME_IMAGE = DockerImageName.parse("selenium/standalone-chrome");
50+
51+
private static final DockerImageName FIREFOX_IMAGE = DockerImageName.parse("selenium/standalone-firefox");
52+
53+
private static final DockerImageName EDGE_IMAGE = DockerImageName.parse("selenium/standalone-edge");
54+
55+
private static final DockerImageName CHROME_DEBUG_IMAGE = DockerImageName.parse("selenium/standalone-chrome-debug");
56+
57+
private static final DockerImageName FIREFOX_DEBUG_IMAGE = DockerImageName.parse(
58+
"selenium/standalone-firefox-debug"
59+
);
60+
61+
private static final DockerImageName[] COMPATIBLE_IMAGES = new DockerImageName[] {
62+
CHROME_IMAGE,
63+
FIREFOX_IMAGE,
64+
EDGE_IMAGE,
65+
CHROME_DEBUG_IMAGE,
66+
FIREFOX_DEBUG_IMAGE,
67+
};
68+
69+
private static final String DEFAULT_PASSWORD = "secret";
70+
71+
private static final int SELENIUM_PORT = 4444;
72+
73+
private static final int VNC_PORT = 5900;
74+
75+
private static final String NO_PROXY_KEY = "no_proxy";
76+
77+
private static final String TC_TEMP_DIR_PREFIX = "tc";
78+
79+
private VncRecordingMode recordingMode = VncRecordingMode.RECORD_FAILING;
80+
81+
private VncRecordingFormat recordingFormat;
82+
83+
private RecordingFileFactory recordingFileFactory;
84+
85+
private File vncRecordingDirectory;
86+
87+
private VncRecordingContainer vncRecordingContainer = null;
88+
89+
private static final Logger LOGGER = LoggerFactory.getLogger(BrowserWebDriverContainer.class);
90+
91+
/**
92+
* Constructor taking a specific webdriver container name and tag
93+
* @param dockerImageName Name of the selenium docker image
94+
*/
95+
public BrowserWebDriverContainer(String dockerImageName) {
96+
this(DockerImageName.parse(dockerImageName));
97+
}
98+
99+
/**
100+
* Constructor taking a specific webdriver container name and tag
101+
* @param dockerImageName Name of the selenium docker image
102+
*/
103+
public BrowserWebDriverContainer(DockerImageName dockerImageName) {
104+
super(dockerImageName);
105+
dockerImageName.assertCompatibleWith(COMPATIBLE_IMAGES);
106+
107+
waitingFor(getDefaultWaitStrategy());
108+
109+
withRecordingFileFactory(new DefaultRecordingFileFactory());
110+
// We have to force SKIP mode for the recording by default because we don't know if the image has VNC or not
111+
recordingMode = VncRecordingMode.SKIP;
112+
}
113+
114+
@NotNull
115+
@Override
116+
protected Set<Integer> getLivenessCheckPorts() {
117+
Integer seleniumPort = getMappedPort(SELENIUM_PORT);
118+
if (recordingMode == VncRecordingMode.SKIP) {
119+
return ImmutableSet.of(seleniumPort);
120+
} else {
121+
return ImmutableSet.of(seleniumPort, getMappedPort(VNC_PORT));
122+
}
123+
}
124+
125+
@Override
126+
protected void configure() {
127+
if (recordingMode != VncRecordingMode.SKIP) {
128+
if (vncRecordingDirectory == null) {
129+
try {
130+
vncRecordingDirectory = Files.createTempDirectory(TC_TEMP_DIR_PREFIX).toFile();
131+
} catch (IOException e) {
132+
// should never happen as per javadoc, since we use valid prefix
133+
logger().error("Exception while trying to create temp directory", e);
134+
throw new ContainerLaunchException("Exception while trying to create temp directory", e);
135+
}
136+
}
137+
138+
if (getNetwork() == null) {
139+
withNetwork(Network.SHARED);
140+
}
141+
142+
vncRecordingContainer =
143+
new VncRecordingContainer(this)
144+
.withVncPassword(DEFAULT_PASSWORD)
145+
.withVncPort(VNC_PORT)
146+
.withVideoFormat(recordingFormat);
147+
}
148+
149+
String timeZone = System.getProperty("user.timezone");
150+
151+
if (timeZone == null || timeZone.isEmpty()) {
152+
timeZone = "Etc/UTC";
153+
}
154+
155+
addExposedPorts(SELENIUM_PORT, VNC_PORT);
156+
addEnv("TZ", timeZone);
157+
158+
if (!getEnvMap().containsKey(NO_PROXY_KEY)) {
159+
addEnv(NO_PROXY_KEY, "localhost");
160+
}
161+
162+
setCommand("/opt/bin/entry_point.sh");
163+
164+
if (getShmSize() == null) {
165+
if (SystemUtils.IS_OS_WINDOWS) {
166+
withSharedMemorySize(512 * FileUtils.ONE_MB);
167+
} else {
168+
this.getBinds().add(new Bind("/dev/shm", new Volume("/dev/shm"), AccessMode.rw));
169+
}
170+
}
171+
172+
/*
173+
* Some unreliability of the selenium browser containers has been observed, so allow multiple attempts to start.
174+
*/
175+
setStartupAttempts(3);
176+
}
177+
178+
public URL getSeleniumAddress() {
179+
try {
180+
return new URL("http", getHost(), getMappedPort(SELENIUM_PORT), "/wd/hub");
181+
} catch (MalformedURLException e) {
182+
e.printStackTrace(); // TODO
183+
return null;
184+
}
185+
}
186+
187+
public String getVncAddress() {
188+
return "vnc://vnc:secret@" + getHost() + ":" + getMappedPort(VNC_PORT);
189+
}
190+
191+
@Override
192+
protected void containerIsStarted(InspectContainerResponse containerInfo) {
193+
if (vncRecordingContainer != null) {
194+
LOGGER.debug("Starting VNC recording");
195+
vncRecordingContainer.start();
196+
}
197+
}
198+
199+
@Override
200+
public void afterTest(TestDescription description, Optional<Throwable> throwable) {
201+
retainRecordingIfNeeded(description.getFilesystemFriendlyName(), !throwable.isPresent());
202+
}
203+
204+
@Override
205+
public void stop() {
206+
if (vncRecordingContainer != null) {
207+
try {
208+
vncRecordingContainer.stop();
209+
} catch (Exception e) {
210+
LOGGER.debug("Failed to stop vncRecordingContainer", e);
211+
}
212+
vncRecordingContainer = null;
213+
}
214+
215+
super.stop();
216+
}
217+
218+
private void retainRecordingIfNeeded(String prefix, boolean succeeded) {
219+
final boolean shouldRecord;
220+
switch (recordingMode) {
221+
case RECORD_ALL:
222+
shouldRecord = true;
223+
break;
224+
case RECORD_FAILING:
225+
shouldRecord = !succeeded;
226+
break;
227+
default:
228+
shouldRecord = false;
229+
break;
230+
}
231+
232+
if (shouldRecord) {
233+
File recordingFile = recordingFileFactory.recordingFileForTest(
234+
vncRecordingDirectory,
235+
prefix,
236+
succeeded,
237+
vncRecordingContainer.getVideoFormat()
238+
);
239+
LOGGER.info("Screen recordings for test {} will be stored at: {}", prefix, recordingFile);
240+
241+
vncRecordingContainer.saveRecordingToFile(recordingFile);
242+
}
243+
}
244+
245+
public BrowserWebDriverContainer withRecordingMode(VncRecordingMode recordingMode, File vncRecordingDirectory) {
246+
return withRecordingMode(recordingMode, vncRecordingDirectory, null);
247+
}
248+
249+
public BrowserWebDriverContainer withRecordingMode(
250+
VncRecordingMode recordingMode,
251+
File vncRecordingDirectory,
252+
VncRecordingFormat recordingFormat
253+
) {
254+
this.recordingMode = recordingMode;
255+
this.vncRecordingDirectory = vncRecordingDirectory;
256+
this.recordingFormat = recordingFormat;
257+
return self();
258+
}
259+
260+
public BrowserWebDriverContainer withRecordingFileFactory(RecordingFileFactory recordingFileFactory) {
261+
this.recordingFileFactory = recordingFileFactory;
262+
return self();
263+
}
264+
265+
private WaitStrategy getDefaultWaitStrategy() {
266+
final WaitStrategy logWaitStrategy = new LogMessageWaitStrategy()
267+
.withRegEx(
268+
".*(RemoteWebDriver instances should connect to|Selenium Server is up and running|Started Selenium Standalone).*\n"
269+
)
270+
.withStartupTimeout(Duration.ofMinutes(1));
271+
272+
return new WaitAllStrategy()
273+
.withStrategy(logWaitStrategy)
274+
.withStrategy(new HostPortWaitStrategy())
275+
.withStartupTimeout(Duration.ofMinutes(1));
276+
}
277+
278+
public enum VncRecordingMode {
279+
SKIP,
280+
RECORD_ALL,
281+
RECORD_FAILING,
282+
}
283+
}

0 commit comments

Comments
 (0)