package org.testcontainers.containers;
import com.github.dockerjava.api.command.InspectContainerResponse;
import org.jetbrains.annotations.Nullable;
import org.junit.runner.Description;
import org.openqa.selenium.remote.BrowserType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.traits.LinkableContainer;
import org.testcontainers.containers.traits.VncService;
import org.testcontainers.containers.wait.LogMessageWaitStrategy;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import static com.google.common.base.Preconditions.checkState;
import static java.time.temporal.ChronoUnit.SECONDS;
/**
* A chrome/firefox/custom container based on SeleniumHQ's standalone container sets.
* <p>
* The container should expose Selenium remote control protocol and VNC.
*/
public class BrowserWebDriverContainer<SELF extends BrowserWebDriverContainer<SELF>> extends GenericContainer<SELF> implements VncService, LinkableContainer {
private static final String CHROME_IMAGE = "selenium/standalone-chrome-debug:%s";
private static final String FIREFOX_IMAGE = "selenium/standalone-firefox-debug:%s";
private static final String DEFAULT_PASSWORD = "secret";
private static final int SELENIUM_PORT = 4444;
private static final int VNC_PORT = 5900;
@Nullable
private DesiredCapabilities desiredCapabilities;
private boolean customImageNameIsSet = false;
@Nullable
private RemoteWebDriver driver;
private VncRecordingMode recordingMode = VncRecordingMode.RECORD_FAILING;
private File vncRecordingDirectory = new File("/tmp");
private final Collection<VncRecordingSidekickContainer> currentVncRecordings = new ArrayList<>();
private static final Logger LOGGER = LoggerFactory.getLogger(BrowserWebDriverContainer.class);
private static final SimpleDateFormat filenameDateFormat = new SimpleDateFormat("YYYYMMdd-HHmmss");
/**
*/
public BrowserWebDriverContainer() {
this.waitStrategy = new LogMessageWaitStrategy()
.withRegEx(".*RemoteWebDriver instances should connect to.*\n")
.withStartupTimeout(Duration.of(15, SECONDS));
}
/**
* Constructor taking a specific webdriver container name and tag
* @param dockerImageName
*/
public BrowserWebDriverContainer(String dockerImageName) {
this();
super.setDockerImageName(dockerImageName);
this.customImageNameIsSet = true;
}
public SELF withDesiredCapabilities(DesiredCapabilities desiredCapabilities) {
this.desiredCapabilities = desiredCapabilities;
return self();
}
@Override
protected Integer getLivenessCheckPort() {
return getMappedPort(SELENIUM_PORT);
}
@Override
protected void configure() {
checkState(desiredCapabilities != null);
if (! customImageNameIsSet) {
super.setDockerImageName(getImageForCapabilities(desiredCapabilities));
}
String timeZone = System.getProperty("user.timezone");
if (timeZone == null || timeZone.isEmpty()) {
timeZone = "Etc/UTC";
}
addExposedPorts(SELENIUM_PORT, VNC_PORT);
addEnv("TZ", timeZone);
addEnv("no_proxy", "localhost");
setCommand("/opt/bin/entry_point.sh");
/*
* Some unreliability of the selenium browser containers has been observed, so allow multiple attempts to start.
*/
setStartupAttempts(3);
}
public static String getImageForCapabilities(DesiredCapabilities desiredCapabilities) {
String seleniumVersion = SeleniumUtils.determineClasspathSeleniumVersion();
String browserName = desiredCapabilities.getBrowserName();
switch (browserName) {
case BrowserType.CHROME:
return String.format(CHROME_IMAGE, seleniumVersion);
case BrowserType.FIREFOX:
return String.format(FIREFOX_IMAGE, seleniumVersion);
default:
throw new UnsupportedOperationException("Browser name must be 'chrome' or 'firefox'; provided '" + browserName + "' is not supported");
}
}
public URL getSeleniumAddress() {
try {
return new URL("http", getContainerIpAddress(), getMappedPort(SELENIUM_PORT), "/wd/hub");
} catch (MalformedURLException e) {
e.printStackTrace();// TODO
return null;
}
}
@Override
public String getVncAddress() {
return "vnc://vnc:secret@" + getContainerIpAddress() + ":" + getMappedPort(VNC_PORT);
}
@Override
public String getPassword() {
return DEFAULT_PASSWORD;
}
@Override
public int getPort() {
return VNC_PORT;
}
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
if (recordingMode != VncRecordingMode.SKIP) {
LOGGER.debug("Starting VNC recording");
// Use multiple startup attempts due to race condition between Selenium being available and VNC being available
VncRecordingSidekickContainer recordingSidekickContainer = new VncRecordingSidekickContainer<>(this)
.withStartupAttempts(3);
recordingSidekickContainer.start();
currentVncRecordings.add(recordingSidekickContainer);
}
this.driver = new RemoteWebDriver(getSeleniumAddress(), desiredCapabilities);
}
/**
* Obtain a RemoteWebDriver instance that is bound to an instance of the browser running inside a new container.
* <p>
* All containers and drivers will be automatically shut down after the test method finishes (if used as a @Rule) or the test
* class (if used as a @ClassRule)
*
* @return a new Remote Web Driver instance
*/
public RemoteWebDriver getWebDriver() {
return driver;
}
@Override
protected void failed(Throwable e, Description description) {
switch (recordingMode) {
case RECORD_FAILING:
case RECORD_ALL:
stopAndRetainRecording(description);
break;
}
currentVncRecordings.clear();
}
@Override
protected void succeeded(Description description) {
switch (recordingMode) {
case RECORD_ALL:
stopAndRetainRecording(description);
break;
}
currentVncRecordings.clear();
}
@Override
protected void finished(Description description) {
if (driver != null) {
driver.quit();
}
this.stop();
}
private void stopAndRetainRecording(Description description) {
File recordingFile = new File(vncRecordingDirectory, "recording-" + filenameDateFormat.format(new Date()) + ".flv");
LOGGER.info("Screen recordings for test {} will be stored at: {}", description.getDisplayName(), recordingFile);
for (VncRecordingSidekickContainer container : currentVncRecordings) {
container.stopAndRetainRecording(recordingFile);
}
}
/**
* Remember any other containers this needs to link to. We have to pass these down to the container so that
* the other containers will be initialized before linking occurs.
*
* @param otherContainer the container rule to link to
* @param alias the alias (hostname) that this other container should be referred to by
* @return this
*/
public SELF withLinkToContainer(LinkableContainer otherContainer, String alias) {
addLink(otherContainer, alias);
return self();
}
public SELF withRecordingMode(VncRecordingMode recordingMode, File vncRecordingDirectory) {
this.recordingMode = recordingMode;
this.vncRecordingDirectory = vncRecordingDirectory;
return self();
}
public enum VncRecordingMode {
SKIP, RECORD_ALL, RECORD_FAILING
}
}