diff --git a/pom.xml b/pom.xml
index 9548b28..31c1112 100644
--- a/pom.xml
+++ b/pom.xml
@@ -110,6 +110,12 @@
commons-exec
1.3
+
+ junit
+ junit
+ 4.12
+ test
+
diff --git a/src/main/java/com/github/markusbernhardt/selenium2library/keywords/BrowserManagement.java b/src/main/java/com/github/markusbernhardt/selenium2library/keywords/BrowserManagement.java
index 26537b8..f0dfc41 100644
--- a/src/main/java/com/github/markusbernhardt/selenium2library/keywords/BrowserManagement.java
+++ b/src/main/java/com/github/markusbernhardt/selenium2library/keywords/BrowserManagement.java
@@ -1,21 +1,18 @@
package com.github.markusbernhardt.selenium2library.keywords;
+import com.github.markusbernhardt.selenium2library.RunOnFailureKeywordsAdapter;
+import com.github.markusbernhardt.selenium2library.Selenium2LibraryFatalException;
+import com.github.markusbernhardt.selenium2library.Selenium2LibraryNonFatalException;
+import com.github.markusbernhardt.selenium2library.locators.ElementFinder;
+import com.github.markusbernhardt.selenium2library.locators.WindowManager;
+import com.github.markusbernhardt.selenium2library.utils.CustomHttpClientFactory;
+import com.github.markusbernhardt.selenium2library.utils.Robotframework;
+import com.github.markusbernhardt.selenium2library.utils.TimeUtils;
+import com.github.markusbernhardt.selenium2library.utils.WebDriverCache;
+import com.github.markusbernhardt.selenium2library.utils.WebDriverCache.SessionIdAliasWebDriverTuple;
+import com.opera.core.systems.OperaDriver;
import io.appium.java_client.ios.IOSDriver;
import io.selendroid.client.SelendroidDriver;
-
-import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.Field;
-import java.net.InetAddress;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.UnknownHostException;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map.Entry;
-import java.util.concurrent.TimeUnit;
-
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.NTCredentials;
@@ -36,9 +33,11 @@
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriver;
import org.openqa.selenium.remote.Augmenter;
+import org.openqa.selenium.remote.CommandInfo;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.HttpCommandExecutor;
import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.remote.http.HttpClient;
import org.openqa.selenium.safari.SafariDriver;
import org.robotframework.javalib.annotation.ArgumentNames;
import org.robotframework.javalib.annotation.Autowired;
@@ -46,15 +45,19 @@
import org.robotframework.javalib.annotation.RobotKeywordOverload;
import org.robotframework.javalib.annotation.RobotKeywords;
-import com.github.markusbernhardt.selenium2library.RunOnFailureKeywordsAdapter;
-import com.github.markusbernhardt.selenium2library.Selenium2LibraryFatalException;
-import com.github.markusbernhardt.selenium2library.Selenium2LibraryNonFatalException;
-import com.github.markusbernhardt.selenium2library.locators.ElementFinder;
-import com.github.markusbernhardt.selenium2library.locators.WindowManager;
-import com.github.markusbernhardt.selenium2library.utils.Robotframework;
-import com.github.markusbernhardt.selenium2library.utils.WebDriverCache;
-import com.github.markusbernhardt.selenium2library.utils.WebDriverCache.SessionIdAliasWebDriverTuple;
-import com.opera.core.systems.OperaDriver;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
@SuppressWarnings("deprecation")
@RobotKeywords
@@ -211,6 +214,16 @@ public String openBrowser(String url, String browserName, String alias, String r
return openBrowser(url, browserName, alias, remoteUrl, desiredCapabilities, null);
}
+ @RobotKeywordOverload
+ public String openBrowser(String url, String browserName, String alias, String remoteUrl,
+ String desiredCapabilities, String browserOptions) throws Throwable {
+ // Magic constants '2 minutes' and '3 hours' are defined at HttpClientFactory, and hard-coded by default:
+ // HttpClientFactory.TIMEOUT_TWO_MINUTES
+ // HttpClientFactory.TIMEOUT_THREE_HOURS
+ return openBrowser(url, browserName, alias, remoteUrl, desiredCapabilities, browserOptions,
+ "2 minutes", "3 hours");
+ }
+
/**
* Opens a new browser instance to given URL.
*
@@ -345,7 +358,7 @@ public String openBrowser(String url, String browserName, String alias, String r
* href=
* "http://selenium-grid.seleniumhq.org/faq.html#i_get_some_strange_errors_when_i_run_multiple_internet_explorer_instances_on_the_same_machine"
* >Strange errors with multiple IE instances
- *
+ *
* @param url
* The URL to open in the newly created browser instance.
* @param browserName
@@ -369,18 +382,23 @@ public String openBrowser(String url, String browserName, String alias, String r
* >DesiredCapabilities
* @param browserOptions
* Default=NONE. Extended browser options as JSON structure.
+ * @param connectionTimeout
+ * Default=2 minutes. Connection timeout value for (Remote) WebDriver.
+ * @param socketTimeout
+ * Default=3 hours. Socket timeout value for (Remote) WebDriver.
* @return The index of the newly created browser instance.
* @throws Throwable - if anything goes wrong
- *
+ *
* @see BrowserManagement#closeAllBrowsers
* @see BrowserManagement#closeBrowser
* @see BrowserManagement#switchBrowser
*/
@RobotKeyword
@ArgumentNames({ "url", "browserName=firefox", "alias=NONE", "remoteUrl=False", "desiredCapabilities=NONE",
- "browserOptions=NONE" })
+ "browserOptions=NONE", "connectionTimeout=2 minutes", "socketTimeout=3 hours" })
public String openBrowser(String url, String browserName, String alias, String remoteUrl,
- String desiredCapabilities, String browserOptions) throws Throwable {
+ String desiredCapabilities, String browserOptions, String connectionTimeout,
+ String socketTimeout) throws Throwable {
try {
logging.info("browserName: " + browserName);
if (remoteUrl != null) {
@@ -389,8 +407,10 @@ public String openBrowser(String url, String browserName, String alias, String r
} else {
logging.info(String.format("Opening browser '%s' to base url '%s'", browserName, url));
}
-
- WebDriver webDriver = createWebDriver(browserName, desiredCapabilities, remoteUrl, browserOptions);
+ final int connectionTimeoutMillis = TimeUtils.convertRobotTimeToMillis(connectionTimeout);
+ final int socketTimeoutMillis = TimeUtils.convertRobotTimeToMillis(socketTimeout);
+ WebDriver webDriver = createWebDriver(browserName, desiredCapabilities, remoteUrl, browserOptions,
+ CustomHttpClientFactory.createWithSpecificTimeout(connectionTimeoutMillis, socketTimeoutMillis));
webDriver.get(url);
String sessionId = webDriverCache.register(webDriver, alias);
logging.debug(String.format("Opened browser with session id %s", sessionId));
@@ -1350,14 +1370,14 @@ protected String getPasswordFromURL(URL url) {
}
protected WebDriver createWebDriver(String browserName, String desiredCapabilitiesString, String remoteUrlString,
- String browserOptions) throws MalformedURLException {
+ String browserOptions, HttpClient.Factory factory) throws MalformedURLException {
browserName = browserName.toLowerCase().replace(" ", "");
DesiredCapabilities desiredCapabilities = createDesiredCapabilities(browserName, desiredCapabilitiesString,
browserOptions);
WebDriver webDriver;
if (remoteUrlString != null && !"False".equals(remoteUrlString)) {
- webDriver = createRemoteWebDriver(desiredCapabilities, new URL(remoteUrlString));
+ webDriver = createRemoteWebDriver(desiredCapabilities, new URL(remoteUrlString), factory);
} else {
webDriver = createLocalWebDriver(browserName, desiredCapabilities);
}
@@ -1404,8 +1424,10 @@ protected WebDriver createLocalWebDriver(String browserName, DesiredCapabilities
throw new Selenium2LibraryFatalException(browserName + " is not a supported browser.");
}
- protected WebDriver createRemoteWebDriver(DesiredCapabilities desiredCapabilities, URL remoteUrl) {
- HttpCommandExecutor httpCommandExecutor = new HttpCommandExecutor(remoteUrl);
+ protected WebDriver createRemoteWebDriver(DesiredCapabilities desiredCapabilities, URL remoteUrl,
+ HttpClient.Factory factory) {
+ HttpCommandExecutor httpCommandExecutor = new HttpCommandExecutor(Collections.emptyMap(),
+ remoteUrl, factory);
setRemoteWebDriverProxy(httpCommandExecutor);
return new Augmenter().augment(new RemoteWebDriver(httpCommandExecutor, desiredCapabilities));
}
diff --git a/src/main/java/com/github/markusbernhardt/selenium2library/utils/CustomHttpClientFactory.java b/src/main/java/com/github/markusbernhardt/selenium2library/utils/CustomHttpClientFactory.java
new file mode 100644
index 0000000..5b99c9b
--- /dev/null
+++ b/src/main/java/com/github/markusbernhardt/selenium2library/utils/CustomHttpClientFactory.java
@@ -0,0 +1,61 @@
+package com.github.markusbernhardt.selenium2library.utils;
+
+import org.apache.http.auth.Credentials;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.openqa.selenium.remote.http.HttpClient;
+import org.openqa.selenium.remote.internal.ApacheHttpClient;
+import org.openqa.selenium.remote.internal.HttpClientFactory;
+
+import java.net.URL;
+
+/**
+ * Provides customized {@link HttpClient.Factory} instances.
+ * {@link HttpClient} creation is via delegation to {@link ApacheHttpClient.Factory} as default behavior does.
+ */
+public class CustomHttpClientFactory implements HttpClient.Factory {
+ private final HttpClient.Factory delegate;
+
+ private CustomHttpClientFactory(HttpClientFactory factory) {
+ delegate = new ApacheHttpClient.Factory(factory);
+ }
+
+ @Override
+ public HttpClient createClient(URL url) {
+ return delegate.createClient(url);
+ }
+
+ /**
+ * Creates a HttpClient.Factory with customized connection and socked timeouts.
+ * Default timeout values are 2 minutes for connection, and 3 hours for socket as hard-coded in default
+ * {@link HttpClientFactory} class.
+ *
+ * @param connectionTimeout the connection timeout in milliseconds
+ * @param socketTimeout the socket timeout in milliseconds
+ * @return a HttpClient.Factory instance that will create
+ */
+ public static HttpClient.Factory createWithSpecificTimeout(int connectionTimeout, int socketTimeout) {
+ HttpClientFactory factory = new CustomTimeoutHttpClientFactory(connectionTimeout, socketTimeout);
+ return new CustomHttpClientFactory(factory);
+ }
+
+ /**
+ * {@link HttpClientFactory} is using hard-coded timeouts for connection timeout (2m) and socket timeout (3h).
+ * This behavior is undesired in some cases, when socket timeout keeps the webdriver blocked for 3 hours on too
+ * early connection attempt to opened port.
+ */
+ private static class CustomTimeoutHttpClientFactory extends HttpClientFactory {
+ private final int connectionTimeout;
+ private final int socketTimeout;
+
+ private CustomTimeoutHttpClientFactory(int connectionTimeout, int socketTimeout) {
+ super(connectionTimeout, socketTimeout);
+ this.connectionTimeout = connectionTimeout;
+ this.socketTimeout = socketTimeout;
+ }
+
+ @Override
+ public CloseableHttpClient createHttpClient(Credentials credentials) {
+ return super.createHttpClient(credentials, connectionTimeout, socketTimeout);
+ }
+ }
+}
diff --git a/src/main/java/com/github/markusbernhardt/selenium2library/utils/TimeUtils.java b/src/main/java/com/github/markusbernhardt/selenium2library/utils/TimeUtils.java
new file mode 100644
index 0000000..e729c36
--- /dev/null
+++ b/src/main/java/com/github/markusbernhardt/selenium2library/utils/TimeUtils.java
@@ -0,0 +1,89 @@
+package com.github.markusbernhardt.selenium2library.utils;
+
+/**
+ * Utilities to convert Robot Framework time.
+ */
+public final class TimeUtils {
+ private static final int SECONDS_TO_MILLISECS = 1000;
+ private static final int MINUTES_TO_MILLISECS = 60 * SECONDS_TO_MILLISECS;
+ private static final int HOURS_TO_MILLISECS = 60 * MINUTES_TO_MILLISECS;
+ private static final int DAYS_TO_MILLISECS = 24 * HOURS_TO_MILLISECS;
+
+ private static final String DAYS_PATTERN = "(\\s*\\d+(\\.\\d+)?\\s*d(ays?)?)?";
+ private static final String HOURS_PATTERN = "(\\s*\\d+(\\.\\d+)?\\s*h(ours?)?)?";
+ private static final String MINUTES_PATTERN = "(\\s*\\d+(\\.\\d+)?\\s*m(in((ute)?s?)?)?)?";
+ private static final String SECONDS_PATTERN = "(\\s*\\d+(\\.\\d+)?\\s*s(ec((ond)?s?))?)?";
+ private static final String MILLISECONDS_PATTERN = "(\\s*\\d+(\\.\\d+)?\\s*(millis(ec((ond)?s?))?|ms))?";
+ private static final String TIME_STRING_PATTERN = "-?(\\s*\\d+(\\.\\d+)?\\s*|" + DAYS_PATTERN + HOURS_PATTERN +
+ MINUTES_PATTERN + SECONDS_PATTERN + MILLISECONDS_PATTERN + ")";
+
+ private TimeUtils() {
+ // this is a utility class
+ }
+
+ /**
+ * Converts a Robot Framework time string to milliseconds value.
+ * See http://robotframework.org/robotframework/latest/libraries/DateTime.html
+ *
+ * @param robotTimeString a valid time string
+ * @return the time in milliseconds
+ */
+ public static int convertRobotTimeToMillis(String robotTimeString) {
+ int sum = 0;
+ if (!robotTimeString.matches(TIME_STRING_PATTERN)) {
+ throw new IllegalArgumentException("Invalid time string " + robotTimeString);
+ }
+ String[] values = robotTimeString.replaceAll("(\\d)([dhms])", "$1 $2").split("\\s+");
+ try {
+ if (values.length == 1) {
+ return Math.round(Float.parseFloat(values[0]) * SECONDS_TO_MILLISECS);
+ }
+ final int signum = values[0].startsWith("-") ? -1 : 1;
+ if (values.length % 2 != 0) {
+ throw new IllegalArgumentException("Invalid time string " + robotTimeString);
+ }
+ for (int i = 0; i < values.length - 1; i+=2) {
+ final float value = Math.abs(Float.parseFloat(values[i]));
+ final int multiplier = getMultiplier(values[i + 1]);
+ sum += signum * Math.round(value * multiplier);
+ }
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid time string " + robotTimeString, e);
+ }
+ return sum;
+ }
+
+ private static int getMultiplier(String specifier) {
+ if ("days".equalsIgnoreCase(specifier)
+ || "day".equalsIgnoreCase(specifier)
+ || "d".equalsIgnoreCase(specifier)) {
+ return DAYS_TO_MILLISECS;
+ }
+ if ("hours".equalsIgnoreCase(specifier)
+ || "hour".equalsIgnoreCase(specifier)
+ || "h".equalsIgnoreCase(specifier)) {
+ return HOURS_TO_MILLISECS;
+ }
+ if ("minutes".equalsIgnoreCase(specifier)
+ || "minute".equalsIgnoreCase(specifier)
+ || "mins".equalsIgnoreCase(specifier)
+ || "min".equalsIgnoreCase(specifier)
+ || "m".equalsIgnoreCase(specifier)) {
+ return MINUTES_TO_MILLISECS;
+ }
+ if ("seconds".equalsIgnoreCase(specifier)
+ || "second".equalsIgnoreCase(specifier)
+ || "secs".equalsIgnoreCase(specifier)
+ || "sec".equalsIgnoreCase(specifier)
+ || "s".equalsIgnoreCase(specifier)) {
+ return SECONDS_TO_MILLISECS;
+ }
+ if ("milliseconds".equalsIgnoreCase(specifier)
+ || "millisecond".equalsIgnoreCase(specifier)
+ || "millis".equalsIgnoreCase(specifier)
+ || "ms".equalsIgnoreCase(specifier)) {
+ return 1;
+ }
+ throw new IllegalArgumentException("Invalid time specifier " + specifier);
+ }
+}
diff --git a/src/test/java/com/github/markusbernhardt/selenium2library/utils/TimeUtilsTest.java b/src/test/java/com/github/markusbernhardt/selenium2library/utils/TimeUtilsTest.java
new file mode 100644
index 0000000..2d8c926
--- /dev/null
+++ b/src/test/java/com/github/markusbernhardt/selenium2library/utils/TimeUtilsTest.java
@@ -0,0 +1,60 @@
+package com.github.markusbernhardt.selenium2library.utils;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class TimeUtilsTest {
+
+ @Test
+ public void whenNoSpecifier_ShouldConvertSeconds() {
+ assertEquals(1000, TimeUtils.convertRobotTimeToMillis("1"));
+ assertEquals(-2000, TimeUtils.convertRobotTimeToMillis("-2"));
+ assertEquals(500, TimeUtils.convertRobotTimeToMillis("0.5"));
+ }
+
+ @Test
+ public void whenMillisecondsSpecified_ShouldConvertMillisecsonds() {
+ assertEquals(-10, TimeUtils.convertRobotTimeToMillis("-10ms"));
+ assertEquals(1, TimeUtils.convertRobotTimeToMillis("0.8 milliseconds"));
+ assertEquals(1, TimeUtils.convertRobotTimeToMillis("1 millisecond"));
+ assertEquals(1, TimeUtils.convertRobotTimeToMillis("1 millis"));
+ }
+
+ @Test
+ public void whenSecondsSpecified_ShouldConvertSeconds() {
+ assertEquals(-10000, TimeUtils.convertRobotTimeToMillis("-10seconds"));
+ assertEquals(800, TimeUtils.convertRobotTimeToMillis("0.8 s"));
+ assertEquals(1000, TimeUtils.convertRobotTimeToMillis("1 sec"));
+ assertEquals(2000, TimeUtils.convertRobotTimeToMillis("2second"));
+ }
+
+ @Test
+ public void whenMinutesSpecified_ShouldConvertMinutes() {
+ assertEquals(-600000, TimeUtils.convertRobotTimeToMillis("-10min"));
+ assertEquals(48000, TimeUtils.convertRobotTimeToMillis("0.8 m"));
+ assertEquals(60000, TimeUtils.convertRobotTimeToMillis("1 minute"));
+ assertEquals(120000, TimeUtils.convertRobotTimeToMillis("2minutes"));
+ }
+
+ @Test
+ public void whenHoursSpecified_ShouldConvertHours() {
+ assertEquals(-36000000, TimeUtils.convertRobotTimeToMillis("-10h"));
+ assertEquals(3600000, TimeUtils.convertRobotTimeToMillis("1 hour"));
+ assertEquals(5400000, TimeUtils.convertRobotTimeToMillis("1.5 hours"));
+ }
+
+ @Test
+ public void whenDaysSpecified_ShouldConvertDays() {
+ assertEquals(-864000000, TimeUtils.convertRobotTimeToMillis("-10days"));
+ assertEquals(129600000, TimeUtils.convertRobotTimeToMillis("1.5d"));
+ assertEquals(86400000, TimeUtils.convertRobotTimeToMillis("1 day"));
+ }
+
+ @Test
+ public void whenComplexSpecified_ShouldConvertComplex() {
+ assertEquals(151264015, TimeUtils.convertRobotTimeToMillis("1.5d 6 hours 1 minute 4 secs 15ms"));
+ assertEquals(-151264015, TimeUtils.convertRobotTimeToMillis("-1.5d 6 hours 1 minute 4 secs 15ms"));
+ }
+
+}