Skip to content

Commit a212109

Browse files
committed
WIP Proposed changes to Selenium/WebDriver module for ongoing compatibility
Co-authored with @kiview, this PR started out with some minor (hacky) workarounds but evolved into a rethink of what features are sensible for us to keep on trying to make work given: * existence of Selenium 4.0.0, with some breaking API changes * some mutable tags being used for Selenium Docker images, and the (IMHO correct) recommendations by the Selenium team that more exactly pinnable tags should be used in our use case. Very WIP - all of this could change. Primary changes: - Selenium 4.0.0 compatibility fixes - Remove in-code workaround for Chrome GPU issue, which created coupling to a Selenium 3.x API - Remove support for Selenium 2.x, as supporting 3 major versions of the library becomes infeasible - Use non-debug docker images automatically for Selenium 4.x, as `-debug` images are not required (and apparently not available for Firefox) - Remove `provided` scope dependencies, since we already require some Selenium JARs to be on the classpath. Gradle `implementation` scope is used despite Selenium classes being in our API - this is a slight and deliberate abuse, but seems better than us potentially forcing an upgrade of Selenium by updating our dependency version. - TODO: update docs - TODO: further testing of the assumption that this will not have real impacts to users. - Steps to phase out implicit docker image detection and to remove API methods that are coupled to Selenium classes - Deprecate constructors that do not supply a user-pinnable Docker image - Deprecate constructors/methods that involve Capabilities and RemoteWebDriver - Suggest alternative approaches for obtaining a WebDriver instance, predicated on future removal of APIs that are coupled to Selenium classes - Have *not* deprecated the `determineClasspathSeleniumVersion` convenience method that can be used to identify Selenium version on the classpath, as this may be something that users want to use - TODO: plenty of docs, examples and migration notes - TODO: specific documentation around best-practices for selecting a pinnable Selenium image - TODO: phase out implicit use of Chrome
1 parent 0654ef6 commit a212109

File tree

2 files changed

+76
-54
lines changed

2 files changed

+76
-54
lines changed

modules/selenium/build.gradle

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ description = "Testcontainers :: Selenium"
33
dependencies {
44
api project(':testcontainers')
55

6-
provided 'org.seleniumhq.selenium:selenium-remote-driver:3.141.59'
7-
provided 'org.seleniumhq.selenium:selenium-chrome-driver:3.141.59'
8-
testImplementation 'org.seleniumhq.selenium:selenium-firefox-driver:3.141.59'
9-
testImplementation 'org.seleniumhq.selenium:selenium-support:3.141.59'
6+
implementation 'org.seleniumhq.selenium:selenium-remote-driver:4.0.0'
7+
implementation 'org.seleniumhq.selenium:selenium-chrome-driver:4.0.0'
8+
implementation 'org.seleniumhq.selenium:selenium-firefox-driver:4.0.0'
9+
implementation 'org.seleniumhq.selenium:selenium-support:4.0.0'
1010

1111
testImplementation 'org.mortbay.jetty:jetty:6.1.26'
1212
testImplementation project(':nginx')

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

Lines changed: 72 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@
4444
*/
4545
public class BrowserWebDriverContainer<SELF extends BrowserWebDriverContainer<SELF>> extends GenericContainer<SELF> implements LinkableContainer, TestLifecycleAware {
4646

47-
private static final DockerImageName CHROME_IMAGE = DockerImageName.parse("selenium/standalone-chrome-debug");
48-
private static final DockerImageName FIREFOX_IMAGE = DockerImageName.parse("selenium/standalone-firefox-debug");
47+
private static final DockerImageName CHROME_IMAGE = DockerImageName.parse("selenium/standalone-chrome");
48+
private static final DockerImageName FIREFOX_IMAGE = DockerImageName.parse("selenium/standalone-firefox");
49+
private static final DockerImageName CHROME_DEBUG_IMAGE = DockerImageName.parse("selenium/standalone-chrome-debug");
50+
private static final DockerImageName FIREFOX_DEBUG_IMAGE = DockerImageName.parse("selenium/standalone-firefox-debug");
4951
private static final DockerImageName[] COMPATIBLE_IMAGES = new DockerImageName[] {
52+
CHROME_DEBUG_IMAGE,
53+
FIREFOX_DEBUG_IMAGE,
5054
CHROME_IMAGE,
51-
FIREFOX_IMAGE,
52-
DockerImageName.parse("selenium/standalone-chrome"),
53-
DockerImageName.parse("selenium/standalone-firefox")
55+
FIREFOX_IMAGE
5456
};
5557

5658
private static final String DEFAULT_PASSWORD = "secret";
@@ -75,6 +77,10 @@ public class BrowserWebDriverContainer<SELF extends BrowserWebDriverContainer<SE
7577

7678
private static final Logger LOGGER = LoggerFactory.getLogger(BrowserWebDriverContainer.class);
7779

80+
/**
81+
* @deprecated please use {@link BrowserWebDriverContainer#BrowserWebDriverContainer(DockerImageName)}
82+
*/
83+
@Deprecated
7884
public BrowserWebDriverContainer() {
7985
super();
8086
final WaitStrategy logWaitStrategy = new LogMessageWaitStrategy()
@@ -92,7 +98,10 @@ public BrowserWebDriverContainer() {
9298
/**
9399
* Constructor taking a specific webdriver container name and tag
94100
* @param dockerImageName Name of the selenium docker image
101+
*
102+
* @deprecated please use {@link BrowserWebDriverContainer#BrowserWebDriverContainer(DockerImageName)}
95103
*/
104+
@Deprecated
96105
public BrowserWebDriverContainer(String dockerImageName) {
97106
this(DockerImageName.parse(dockerImageName));
98107
}
@@ -103,17 +112,16 @@ public BrowserWebDriverContainer(String dockerImageName) {
103112
*/
104113
public BrowserWebDriverContainer(DockerImageName dockerImageName) {
105114
super(dockerImageName);
106-
107115
// we assert compatibility with the chrome/firefox image later, after capabilities are processed
108116

109117
final WaitStrategy logWaitStrategy = new LogMessageWaitStrategy()
110-
.withRegEx(".*(RemoteWebDriver instances should connect to|Selenium Server is up and running|Started Selenium Standalone).*\n")
111-
.withStartupTimeout(Duration.of(15, SECONDS));
118+
.withRegEx(".*(RemoteWebDriver instances should connect to|Selenium Server is up and running|Started Selenium Standalone).*\n")
119+
.withStartupTimeout(Duration.of(15, SECONDS));
112120

113121
this.waitStrategy = new WaitAllStrategy()
114-
.withStrategy(logWaitStrategy)
115-
.withStrategy(new HostPortWaitStrategy())
116-
.withStartupTimeout(Duration.of(15, SECONDS));
122+
.withStrategy(logWaitStrategy)
123+
.withStrategy(new HostPortWaitStrategy())
124+
.withStartupTimeout(Duration.of(15, SECONDS));
117125

118126
this.withRecordingFileFactory(new DefaultRecordingFileFactory());
119127

@@ -128,8 +136,9 @@ public SELF withCapabilities(Capabilities capabilities) {
128136
}
129137

130138
/**
131-
* @deprecated Use withCapabilities(Capabilities capabilities) instead:
132-
* withCapabilities(new FirefoxOptions())
139+
* @deprecated please use {@link BrowserWebDriverContainer#getSeleniumAddress()} to obtain the selenium server URL,
140+
* and call the {@link RemoteWebDriver} constructor ({@link RemoteWebDriver#RemoteWebDriver(URL, Capabilities)}),
141+
* passing in the URL and {@link Capabilities} object instead.
133142
*
134143
* @param capabilities DesiredCapabilities
135144
* @return SELF
@@ -153,26 +162,8 @@ protected Set<Integer> getLivenessCheckPorts() {
153162

154163
@Override
155164
protected void configure() {
156-
157165
String seleniumVersion = SeleniumUtils.determineClasspathSeleniumVersion();
158166

159-
if (capabilities == null) {
160-
if (seleniumVersion.startsWith("2.")) {
161-
logger().info("No capabilities provided, falling back to DesiredCapabilities.chrome()");
162-
capabilities = DesiredCapabilities.chrome();
163-
} else {
164-
logger().info("No capabilities provided, falling back to ChromeOptions");
165-
capabilities = new ChromeOptions();
166-
}
167-
}
168-
169-
// Hack for new selenium-chrome image that contains Chrome 92.
170-
// If not disabled, container startup will fail in most cases and consume excessive amounts of CPU.
171-
if (capabilities instanceof ChromeOptions) {
172-
ChromeOptions options = (ChromeOptions) this.capabilities;
173-
options.addArguments("--disable-gpu");
174-
}
175-
176167
if (recordingMode != VncRecordingMode.SKIP) {
177168

178169
if (vncRecordingDirectory == null) {
@@ -200,6 +191,13 @@ protected void configure() {
200191
super.setDockerImageName(customImageName.asCanonicalNameString());
201192
} else {
202193
DockerImageName standardImageForCapabilities = getStandardImageForCapabilities(capabilities, seleniumVersion);
194+
logger().warn(
195+
"Image name for selenium image has not been set, and one will be inferred automatically based " +
196+
"on capabilities ({}). Inferred image: {}. This feature is deprecated and will be removed " +
197+
"in the future.",
198+
standardImageForCapabilities.asCanonicalNameString(),
199+
this.capabilities
200+
);
203201
super.setDockerImageName(standardImageForCapabilities.asCanonicalNameString());
204202
}
205203

@@ -233,34 +231,46 @@ protected void configure() {
233231
* @param seleniumVersion the version of selenium in use
234232
* @return an image name for the default standalone Docker image for the appropriate browser
235233
*
236-
* @deprecated note that this method is deprecated and may be removed in the future. The no-args
237-
* {@link BrowserWebDriverContainer#BrowserWebDriverContainer()} combined with the
238-
* {@link BrowserWebDriverContainer#withCapabilities(Capabilities)} method should be considered. A decision on
239-
* removal of this deprecated method will be taken at a future date.
234+
* @deprecated note that this method is deprecated and will be removed in the future.
240235
*/
241236
@Deprecated
242237
public static String getDockerImageForCapabilities(Capabilities capabilities, String seleniumVersion) {
243238
return getStandardImageForCapabilities(capabilities, seleniumVersion).asCanonicalNameString();
244239
}
245240

246241
private static DockerImageName getStandardImageForCapabilities(Capabilities capabilities, String seleniumVersion) {
247-
String browserName = capabilities.getBrowserName();
248-
switch (browserName) {
249-
case BrowserType.CHROME:
250-
return CHROME_IMAGE.withTag(seleniumVersion);
251-
case BrowserType.FIREFOX:
252-
return FIREFOX_IMAGE.withTag(seleniumVersion);
253-
default:
254-
throw new UnsupportedOperationException("Browser name must be 'chrome' or 'firefox'; provided '" + browserName + "' is not supported");
242+
boolean supportsVncWithoutDebugImage = seleniumVersion.startsWith("4.");
243+
244+
String browserName;
245+
if (capabilities == null) {
246+
// opinionated default for deprecated path
247+
browserName = "chrome";
248+
} else {
249+
browserName = capabilities.getBrowserName();
250+
}
251+
252+
if (browserName.equals("chrome")) {
253+
if (supportsVncWithoutDebugImage) {
254+
return CHROME_IMAGE;
255+
} else {
256+
return CHROME_DEBUG_IMAGE;
257+
}
258+
} else if (browserName.equals("firefox")) {
259+
if (supportsVncWithoutDebugImage) {
260+
return FIREFOX_IMAGE;
261+
} else {
262+
return FIREFOX_DEBUG_IMAGE;
263+
}
255264
}
265+
266+
throw new UnsupportedOperationException("Browser name must be 'chrome' or 'firefox'; provided '" + browserName + "' is not supported");
256267
}
257268

258269
public URL getSeleniumAddress() {
259270
try {
260271
return new URL("http", getHost(), getMappedPort(SELENIUM_PORT), "/wd/hub");
261272
} catch (MalformedURLException e) {
262-
e.printStackTrace();// TODO
263-
return null;
273+
throw new RuntimeException("Failed to construct a valid URL!", e);
264274
}
265275
}
266276

@@ -280,10 +290,6 @@ public int getPort() {
280290

281291
@Override
282292
protected void containerIsStarted(InspectContainerResponse containerInfo) {
283-
driver = Unreliables.retryUntilSuccess(30, TimeUnit.SECONDS,
284-
() -> Timeouts.getWithTimeout(10, TimeUnit.SECONDS,
285-
() -> new RemoteWebDriver(getSeleniumAddress(), capabilities)));
286-
287293
if (vncRecordingContainer != null) {
288294
LOGGER.debug("Starting VNC recording");
289295
vncRecordingContainer.start();
@@ -296,9 +302,25 @@ protected void containerIsStarted(InspectContainerResponse containerInfo) {
296302
* All containers and drivers will be automatically shut down after the test method finishes (if used as a @Rule) or the test
297303
* class (if used as a @ClassRule)
298304
*
305+
* @deprecated please use {@link BrowserWebDriverContainer#getSeleniumAddress()} to obtain the selenium server URL,
306+
* and call the {@link RemoteWebDriver} constructor ({@link RemoteWebDriver#RemoteWebDriver(URL, Capabilities)}),
307+
* passing in the URL and {@link Capabilities} object instead.
308+
*
299309
* @return a new Remote Web Driver instance
300310
*/
301-
public RemoteWebDriver getWebDriver() {
311+
@Deprecated
312+
public synchronized RemoteWebDriver getWebDriver() {
313+
if (driver == null) {
314+
if (capabilities == null) {
315+
logger().warn("No capabilities provided - this will cause an exception in future versions. Falling back to ChromeOptions");
316+
capabilities = new ChromeOptions();
317+
}
318+
319+
driver = Unreliables.retryUntilSuccess(30, TimeUnit.SECONDS,
320+
() -> Timeouts.getWithTimeout(10, TimeUnit.SECONDS,
321+
() -> new RemoteWebDriver(getSeleniumAddress(), capabilities)));
322+
}
323+
302324
return driver;
303325
}
304326

0 commit comments

Comments
 (0)