diff --git a/README.md b/README.md
index cc22ad51..82289203 100644
--- a/README.md
+++ b/README.md
@@ -129,10 +129,10 @@ In addition to support for all of the standard Java-based browser drivers, the `
Unlike the other drivers supported by **Selenium Foundation** which are implemented in Java, the "engines" provided by [Appium](https://appium.io) are implemented in NodeJS. To launch a **Selenium Grid** collection that includes Appium nodes, you'll need the following additional tools:
* Platform-specific Node Version Manager: The installation page for `npm` (below) provides links to recommended version managers.
-* [NodeJS (node)](https://nodejs.org): Currently, I'm running version 17.5.0
-* [Node Package Manager (npm)](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm): Currently, I'm running version 8.13.2
-* [Node Process Manager (pm2)](https://pm2.io/): Currently, I'm running version 5.2.0
-* [Appium](https://appium.io): Currently, I'm running version 1.22.3
+* [NodeJS (node)](https://nodejs.org): Currently, I'm running version 22.7.0
+* [Node Package Manager (npm)](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm): Currently, I'm running version 10.8.2
+* [Node Process Manager (pm2)](https://pm2.io/): Currently, I'm running version 5.4.2
+* [Appium](https://appium.io): Currently, I'm running version 2.11.3
Typically, these tools must be on the system file path. However, you can provide specific paths for each of these via **Selenium Foundation** settings:
* **NPM_BINARY_PATH**: If unspecified, the `PATH` is searched
diff --git a/build.gradle b/build.gradle
index 29b4ec14..23925b85 100644
--- a/build.gradle
+++ b/build.gradle
@@ -237,7 +237,7 @@ repositories {
dependencies {
constraints {
- api 'com.nordstrom.tools:java-utils:3.2.1'
+ api 'com.nordstrom.tools:java-utils:3.3.1'
api 'com.nordstrom.tools:settings:3.0.5'
api 'com.nordstrom.tools:junit-foundation:17.1.1'
api 'com.github.sbabcoc:logback-testng:2.0.0'
diff --git a/espressoDeps.gradle b/espressoDeps.gradle
index 05bc9ed3..40f9c021 100644
--- a/espressoDeps.gradle
+++ b/espressoDeps.gradle
@@ -8,5 +8,6 @@ dependencies {
testImplementation('io.appium:java-client') {
exclude group: 'org.seleniumhq.selenium', module: 'selenium-java'
exclude group: 'org.seleniumhq.selenium', module: 'selenium-support'
+ exclude group: 'org.slf4j', module: 'slf4j-api'
}
}
diff --git a/mac2Deps.gradle b/mac2Deps.gradle
index f80fe0d0..3ebfa2c1 100644
--- a/mac2Deps.gradle
+++ b/mac2Deps.gradle
@@ -8,5 +8,6 @@ dependencies {
testImplementation('io.appium:java-client') {
exclude group: 'org.seleniumhq.selenium', module: 'selenium-java'
exclude group: 'org.seleniumhq.selenium', module: 'selenium-support'
+ exclude group: 'org.slf4j', module: 'slf4j-api'
}
}
diff --git a/selenium4Deps.gradle b/selenium4Deps.gradle
index c5809a93..c444658d 100644
--- a/selenium4Deps.gradle
+++ b/selenium4Deps.gradle
@@ -3,7 +3,7 @@ ext.libsDir = new File(buildRoot, 'libs')
java {
toolchain {
- languageVersion = JavaLanguageVersion.of(11)
+ languageVersion = JavaLanguageVersion.of(17)
}
}
@@ -25,13 +25,13 @@ sourceSets {
dependencies {
constraints {
api 'com.nordstrom.tools:testng-foundation:5.1.1-j11'
- api 'org.seleniumhq.selenium:selenium-grid:4.23.0'
- api 'org.seleniumhq.selenium:selenium-support:4.23.0'
- api 'org.seleniumhq.selenium:selenium-chrome-driver:4.23.0'
- api 'org.seleniumhq.selenium:selenium-edge-driver:4.23.0'
- api 'org.seleniumhq.selenium:selenium-firefox-driver:4.23.0'
+ api 'org.seleniumhq.selenium:selenium-grid:4.25.0'
+ api 'org.seleniumhq.selenium:selenium-support:4.25.0'
+ api 'org.seleniumhq.selenium:selenium-chrome-driver:4.25.0'
+ api 'org.seleniumhq.selenium:selenium-edge-driver:4.25.0'
+ api 'org.seleniumhq.selenium:selenium-firefox-driver:4.25.0'
api 'org.seleniumhq.selenium:selenium-opera-driver:4.4.0'
- api 'org.seleniumhq.selenium:selenium-safari-driver:4.23.0'
+ api 'org.seleniumhq.selenium:selenium-safari-driver:4.25.0'
api 'com.nordstrom.ui-tools:htmlunit-remote:4.23.0'
api 'org.seleniumhq.selenium:htmlunit3-driver:4.23.0'
api 'org.htmlunit:htmlunit:4.4.0'
@@ -39,11 +39,11 @@ dependencies {
api 'org.apache.httpcomponents:httpclient:4.5.14'
api 'org.eclipse.jetty:jetty-servlet:9.4.50.v20221201'
api 'org.jsoup:jsoup:1.15.3'
- api 'org.apache.commons:commons-lang3:3.12.0'
+ api 'org.apache.commons:commons-lang3:3.16.0'
api 'com.beust:jcommander:1.82'
api 'io.netty:netty-transport-native-epoll:4.1.93.Final'
api 'io.netty:netty-transport-native-kqueue:4.1.93.Final'
- testImplementation 'io.appium:java-client:7.6.0'
+ testImplementation 'io.appium:java-client:9.3.0'
testImplementation 'org.mockito:mockito-core:4.6.1'
}
api 'com.nordstrom.tools:testng-foundation'
diff --git a/src/main/java/com/nordstrom/automation/selenium/AbstractSeleniumConfig.java b/src/main/java/com/nordstrom/automation/selenium/AbstractSeleniumConfig.java
index 23a3fc15..ea366ff8 100644
--- a/src/main/java/com/nordstrom/automation/selenium/AbstractSeleniumConfig.java
+++ b/src/main/java/com/nordstrom/automation/selenium/AbstractSeleniumConfig.java
@@ -32,6 +32,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nordstrom.automation.selenium.core.FoundationSlotMatcher;
import com.nordstrom.automation.selenium.core.GridUtility;
import com.nordstrom.automation.selenium.core.SeleniumGrid;
import com.nordstrom.automation.selenium.servlet.ExamplePageLauncher;
@@ -183,6 +184,14 @@ public enum SeleniumSettings implements SettingsCore.SettingsAPI {
*/
HUB_PORT("selenuim.hub.port", null),
+ /**
+ * This setting specifies the slot matcher used by the local Selenium Grid hub server.
+ *
+ * name: selenium.slot.matcher
+ * default: com.nordstrom.automation.selenium.core.FoundationSlotMatcher
+ */
+ SLOT_MATCHER("selenium.slot.matcher", FoundationSlotMatcher.class.getName()),
+
/**
* This setting specifies a comma-delimited list of fully-qualified names of servlet classes to extend the
* capabilities of the local Selenium Grid hub server.
@@ -564,6 +573,11 @@ public static SeleniumConfig getConfig() {
throw new IllegalStateException("SELENIUM_CONFIG must be populated by subclass static initializer");
}
+ /**
+ * Get the major version of the target Selenium API.
+ *
+ * @return target Selenium major version
+ */
public abstract int getVersion();
/**
@@ -610,7 +624,7 @@ public synchronized URL getHubUrl() {
String hostStr = getString(SeleniumSettings.HUB_HOST.key());
if (hostStr != null) {
try {
- hubUrl = new URL(hostStr);
+ hubUrl = URI.create(hostStr).toURL();
} catch (MalformedURLException e) {
throw UncheckedThrow.throwUnchecked(e);
}
@@ -926,12 +940,12 @@ private static URI getConfigUri(final String path, final URL url) throws URISynt
public String[] getDependencyContexts() {
String gridLauncher = getString(SeleniumSettings.GRID_LAUNCHER.key());
if (gridLauncher != null) {
+ StringBuilder builder = new StringBuilder(gridLauncher);
+ String slotMatcher = getString(SeleniumSettings.SLOT_MATCHER.key());
+ if (slotMatcher != null) builder.append(File.pathSeparator).append(slotMatcher);
String dependencies = getString(SeleniumSettings.LAUNCHER_DEPS.key());
- if (dependencies != null) {
- return (gridLauncher + File.pathSeparator + dependencies).split(File.pathSeparator);
- } else {
- return new String[] { gridLauncher };
- }
+ if (dependencies != null) builder.append(File.pathSeparator).append(dependencies);
+ return builder.toString().split(File.pathSeparator);
} else {
return new String[] {};
}
@@ -1007,6 +1021,15 @@ public String getContextPlatform() {
return getConfig().getString(SeleniumSettings.CONTEXT_PLATFORM.key());
}
+ /**
+ * Determine if the {@code Appium} server should be managed by the {@code PM2} utility.
+ *
+ * @return {@code true} if Appium should be managed by PM2; otherwise {@code false}
+ */
+ public boolean appiumWithPM2() {
+ return getConfig().getBoolean(SeleniumSettings.APPIUM_WITH_PM2.key());
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/src/main/java/com/nordstrom/automation/selenium/core/GridUtility.java b/src/main/java/com/nordstrom/automation/selenium/core/GridUtility.java
index 2e5c3acf..3db021e9 100644
--- a/src/main/java/com/nordstrom/automation/selenium/core/GridUtility.java
+++ b/src/main/java/com/nordstrom/automation/selenium/core/GridUtility.java
@@ -9,6 +9,7 @@
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
+import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
@@ -47,6 +48,7 @@
import com.nordstrom.automation.selenium.utility.NetIdentity;
import com.nordstrom.common.base.UncheckedThrow;
import com.nordstrom.common.file.PathUtils;
+import com.nordstrom.common.uri.UriUtils;
/**
* This class provides basic support for interacting with a Selenium Grid instance.
@@ -67,12 +69,12 @@ private GridUtility() {
* Determine if the specified Selenium Grid host (hub or node) is active.
*
* @param hostUrl {@link URL} to be checked
- * @param request request path (may include parameters)
+ * @param pathAndParams path and query parameters
* @return 'true' if specified host is active; otherwise 'false'
*/
- public static boolean isHostActive(final URL hostUrl, final String request) {
+ public static boolean isHostActive(final URL hostUrl, final String... pathAndParams) {
try {
- HttpResponse response = getHttpResponse(hostUrl, request);
+ HttpResponse response = getHttpResponse(hostUrl, pathAndParams);
return (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK);
} catch (IOException eaten) {
// nothing to do here
@@ -84,16 +86,15 @@ public static boolean isHostActive(final URL hostUrl, final String request) {
* Send the specified GET request to the indicated host.
*
* @param hostUrl {@link URL} of target host
- * @param request request path (may include parameters)
+ * @param pathAndParams path and query parameters
* @return host response for the specified GET request
* @throws IOException if the request triggered an I/O exception
*/
- public static HttpResponse getHttpResponse(final URL hostUrl, final String request) throws IOException {
+ public static HttpResponse getHttpResponse(final URL hostUrl, final String... pathAndParams) throws IOException {
Objects.requireNonNull(hostUrl, "[hostUrl] must be non-null");
- Objects.requireNonNull(request, "[request] must be non-null");
HttpClient client = HttpClientBuilder.create().build();
- URL url = new URL(hostUrl.getProtocol(), hostUrl.getHost(), hostUrl.getPort(), request);
- return client.execute(extractHost(hostUrl), new HttpGet(url.toExternalForm()));
+ URI uri = UriUtils.makeBasicURI(hostUrl.getProtocol(), hostUrl.getHost(), hostUrl.getPort(), pathAndParams);
+ return client.execute(extractHost(hostUrl), new HttpGet(uri.toURL().toExternalForm()));
}
/**
@@ -108,8 +109,8 @@ public static HttpResponse callGraphQLService(final URL hostUrl, String query) t
Objects.requireNonNull(hostUrl, "[hostUrl] must be non-null");
Objects.requireNonNull(query, "[query] must be non-null");
HttpClient client = HttpClientBuilder.create().build();
- URL url = new URL(hostUrl.getProtocol(), hostUrl.getHost(), hostUrl.getPort(), "/graphql");
- HttpPost httpRequest = new HttpPost(url.toExternalForm());
+ URI uri = UriUtils.makeBasicURI(hostUrl.getProtocol(), hostUrl.getHost(), hostUrl.getPort(), "/graphql");
+ HttpPost httpRequest = new HttpPost(uri.toURL().toExternalForm());
httpRequest.setEntity(new StringEntity(query, ContentType.APPLICATION_JSON));
return client.execute(extractHost(hostUrl), httpRequest);
}
@@ -294,14 +295,18 @@ public static String getLocalHost() {
* Get next configured output path for Grid server of specified role.
*
* @param config {@link SeleniumConfig} object
- * @param isHub role of Grid server being started ({@code true} = hub; {@code false} = node)
+ * @param isHub role of Grid server being started:
+ *
{@code true} = hub
+ *
{@code false} = node
+ *
{@code null} = relay
+ *
* @return Grid server output path (may be {@code null})
*/
- public static Path getOutputPath(SeleniumConfig config, boolean isHub) {
+ public static Path getOutputPath(SeleniumConfig config, Boolean isHub) {
Path outputPath = null;
if (!config.getBoolean(SeleniumSettings.GRID_NO_REDIRECT.key())) {
- String gridRole = isHub ? "hub" : "node";
+ String gridRole = (isHub == null) ? "relay" : (isHub) ? "hub" : "node";
String logsFolder = config.getString(SeleniumSettings.GRID_LOGS_FOLDER.key());
Path logsPath = Paths.get(logsFolder);
if (!logsPath.isAbsolute()) {
diff --git a/src/main/java/com/nordstrom/automation/selenium/core/SeleniumGrid.java b/src/main/java/com/nordstrom/automation/selenium/core/SeleniumGrid.java
index 6ca2a07e..30800ec5 100644
--- a/src/main/java/com/nordstrom/automation/selenium/core/SeleniumGrid.java
+++ b/src/main/java/com/nordstrom/automation/selenium/core/SeleniumGrid.java
@@ -32,6 +32,7 @@
import com.nordstrom.automation.selenium.SeleniumConfig;
import com.nordstrom.automation.selenium.core.LocalSeleniumGrid.LocalGridServer;
import com.nordstrom.automation.selenium.plugins.PluginUtils;
+import com.nordstrom.common.uri.UriUtils;
/**
* The {@code SeleniumGrid} Object
@@ -91,8 +92,8 @@ public SeleniumGrid(SeleniumConfig config, URL hubUrl) throws IOException {
} else {
LOGGER.debug("Mapping structure of grid at: {}", hubUrl);
for (URL nodeEndpoint : nodeEndpoints) {
- URL nodeUrl = new URL(nodeEndpoint, GridServer.HUB_BASE);
- nodeServers.put(nodeEndpoint, new GridServer(nodeUrl, false));
+ URI nodeUri = UriUtils.uriForPath(nodeEndpoint, GridServer.HUB_BASE);
+ nodeServers.put(nodeEndpoint, new GridServer(nodeUri.toURL(), false));
addNodePersonalities(config, hubServer.getUrl(), nodeEndpoint);
}
LOGGER.debug("{}: Personalities => {}", hubServer.getUrl(), personalities.keySet());
diff --git a/src/main/java/com/nordstrom/automation/selenium/model/ComponentContainer.java b/src/main/java/com/nordstrom/automation/selenium/model/ComponentContainer.java
index f0fcab4d..db976301 100644
--- a/src/main/java/com/nordstrom/automation/selenium/model/ComponentContainer.java
+++ b/src/main/java/com/nordstrom/automation/selenium/model/ComponentContainer.java
@@ -8,6 +8,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -20,6 +21,7 @@
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.StaleElementReferenceException;
@@ -91,31 +93,6 @@ public interface ByEnum {
private static final String UPDATE_VALUE =
"arguments[0].value=arguments[1]; arguments[0].dispatchEvent(new Event('input',{bubbles:true}));";
- private static final Class> ACTIVITY_CLASS;
- private static final Constructor> ACTIVITY_CTOR;
- private static final Method START_ACTIVITY;
-
- static {
- Class> activityClass;
- Constructor> activityCtor;
- Method startActivity;
-
- try {
- Class> androidDriver = Class.forName("io.appium.java_client.android.AndroidDriver");
- activityClass = Class.forName("io.appium.java_client.android.Activity");
- activityCtor = activityClass.getConstructor(String.class, String.class);
- startActivity = androidDriver.getMethod("startActivity", activityClass);
- } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) {
- activityClass = null;
- activityCtor = null;
- startActivity = null;
- }
-
- ACTIVITY_CLASS = activityClass;
- ACTIVITY_CTOR = activityCtor;
- START_ACTIVITY = startActivity;
- }
-
private final Logger logger;
/**
@@ -620,14 +597,12 @@ public static void getUrl(final String url, final WebDriver driver) {
Objects.requireNonNull(driver, "[driver] must be non-null");
if (url.startsWith("activity://")) {
- Objects.requireNonNull(ACTIVITY_CLASS, "AndroidDriver is required to launch activities");
- try {
- String[] components = url.split("/");
- START_ACTIVITY.invoke(driver, ACTIVITY_CTOR.newInstance(components[2], components[3]));
- } catch (SecurityException | InstantiationException | IllegalAccessException
- | IllegalArgumentException | InvocationTargetException e) {
- throw new RuntimeException("Unable to launch specified activity", e);
- }
+ String[] components = url.split("/");
+ ((JavascriptExecutor) driver).executeScript("mobile: startActivity",
+ new HashMap() {{
+ put("package", components[2]);
+ put("appActivity", components[3]);
+ }});
} else {
driver.get(url);
}
diff --git a/src/main/java/com/nordstrom/automation/selenium/plugins/AbstractAppiumPlugin.java b/src/main/java/com/nordstrom/automation/selenium/plugins/AbstractAppiumPlugin.java
index 1c50971e..cf040e5a 100644
--- a/src/main/java/com/nordstrom/automation/selenium/plugins/AbstractAppiumPlugin.java
+++ b/src/main/java/com/nordstrom/automation/selenium/plugins/AbstractAppiumPlugin.java
@@ -1,13 +1,21 @@
package com.nordstrom.automation.selenium.plugins;
+import static org.openqa.selenium.json.Json.MAP_TYPE;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.Reader;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.net.URL;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
@@ -17,6 +25,8 @@
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
+import org.openqa.selenium.grid.config.ConfigException;
+import org.openqa.selenium.json.Json;
import org.openqa.selenium.net.PortProber;
import org.openqa.selenium.os.CommandLine;
import org.openqa.selenium.remote.RemoteWebDriver;
@@ -109,35 +119,16 @@ public String[] getPropertyNames(String capabilities) {
* {@inheritDoc}
*/
@Override
+ @SuppressWarnings("unchecked")
public LocalGridServer create(SeleniumConfig config, String launcherClassName, String[] dependencyContexts,
URL hubUrl, Path workingPath, Path outputPath) throws IOException {
+ String address;
+ Integer portNum;
List argsList = new ArrayList<>();
- String hostUrl = GridUtility.getLocalHost();
- Integer portNum = PortProber.findFreePort();
-
- // get node capabilities for this plug-in
- String nodeCapabilities = getCapabilities(config);
- // determine if using 'pm2' for stand-alone execution of 'appium'
- boolean appiumWithPM2 = config.getBoolean(SeleniumSettings.APPIUM_WITH_PM2.key());
-
- // if running with 'pm2'
- if (appiumWithPM2) {
- // add indication of stand-alone execution of 'appium' with 'pm2'
- Capabilities capabilities = config.getCapabilitiesForJson(nodeCapabilities)[0];
- Capabilities nordOptions = config.getCapabilitiesForJson(APPIUM_WITH_PM2)[0];
- nodeCapabilities = config.toJson(config.mergeCapabilities(capabilities, nordOptions));
- }
- Path nodeConfigPath = config.createNodeConfig(nodeCapabilities, hubUrl);
-
- // specify server host
- argsList.add("--address");
- argsList.add(hostUrl);
-
- // specify server port
- argsList.add("--port");
- argsList.add(portNum.toString());
+ // create node configuration for this plug-in
+ Path nodeConfigPath = config.createNodeConfig(getCapabilities(config), hubUrl);
// allow specification of multiple command line arguments
String[] cliArgs = config.getStringArray(SeleniumSettings.APPIUM_CLI_ARGS.key());
@@ -193,39 +184,91 @@ public LocalGridServer create(SeleniumConfig config, String launcherClassName, S
}
}
- argsList.add("--nodeconfig");
- argsList.add(nodeConfigPath.toString());
+ // if target is Selenium 3
+ if (config.getVersion() == 3) {
+ // get 'localhost' and free port
+ address = GridUtility.getLocalHost();
+ portNum = PortProber.findFreePort();
+
+ // add 'base-path' argument
+ argsList.add("--base-path");
+ argsList.add("/wd/hub");
+
+ // add 'nodeconfig' path
+ argsList.add("--nodeconfig");
+ argsList.add(nodeConfigPath.toString());
+ // otherwise (target is Selenium 4+)
+ } else {
+ // extract address and port from relay configuration
+ try (Reader reader = Files.newBufferedReader(nodeConfigPath)) {
+ Map nodeConfig = new Json().toType(reader, MAP_TYPE);
+ Map relayOptions = (Map) nodeConfig.get("relay");
+ address = (String) relayOptions.get("host");
+ portNum = ((Long) relayOptions.get("port")).intValue();
+ } catch (IOException e) {
+ throw new ConfigException("Failed reading node configuration.", e);
+ }
+
+ // add driver specification
+ argsList.add("--use-drivers");
+ argsList.add(getBrowserName().toLowerCase());
+ }
+
+ // specify server port
+ argsList.add(0, portNum.toString());
+ argsList.add(0, "--port");
+
+ // specify server host
+ argsList.add(0, address);
+ argsList.add(0, "--address");
CommandLine process;
String appiumBinaryPath = findMainScript().getAbsolutePath();
// if running with 'pm2'
- if (appiumWithPM2) {
+ if (config.appiumWithPM2()) {
File pm2Binary = findPM2Binary().getAbsoluteFile();
argsList.add(0, "--");
+
+ // if capturing output
+ if (outputPath != null) {
+ // specify 'pm2' log output path
+ argsList.add(0, "\"" + outputPath.toString() + "\"");
+ argsList.add(0, "--log");
+ }
+
+ // specify 'pm2' process name
argsList.add(0, "appium-" + portNum);
argsList.add(0, "--name");
- argsList.add(0, appiumBinaryPath);
+
+ // specify path to 'appium' main script
+ argsList.add(0, "\"" + appiumBinaryPath + "\"");
argsList.add(0, "start");
String executable;
if (SystemUtils.IS_OS_WINDOWS) {
+ argsList.add(0, "\"" + pm2Binary.getAbsolutePath() + "\"");
+ String command = String.join(" ", argsList);
+ argsList.clear();
+
executable = "cmd.exe";
- argsList.add(0, pm2Binary.getAbsolutePath());
- argsList.add(0, "/c");
+ argsList.add("/c");
+ argsList.add("\"" + command + "\"");
} else {
executable = pm2Binary.getAbsolutePath();
}
process = new CommandLine(executable, argsList.toArray(new String[0]));
- // otherwise
- } else { // (running with 'node')
+ // otherwise (running with 'node')
+ } else {
argsList.add(0, appiumBinaryPath);
process = new CommandLine(findNodeBinary().getAbsolutePath(), argsList.toArray(new String[0]));
}
- return new AppiumGridServer(hostUrl, portNum, false, process, workingPath, outputPath);
+ // if target is Selenium 4+, store path to relay configuration in Appium process environment
+ if (config.getVersion() > 3) process.setEnvironmentVariable("nodeConfigPath", nodeConfigPath.toString());
+ return new AppiumGridServer(address, portNum, false, process, workingPath, outputPath);
}
/**
@@ -263,6 +306,24 @@ public Constructor getRemoteWebDriverCtor(Capabil
*/
public abstract String getDriverClassName();
+ /**
+ * Add the 'nord:options' object to the specified node capabilities string.
+ * NOTE: The 'nord:options' object is only added if Appium is being managed by PM2.
+ *
+ * @param config {@link SelenikumConfig} object
+ * @param nodeCapabilities node capabilities string
+ * @return node capabilities string
+ */
+ String addNordOptions(SeleniumConfig config, String nodeCapabilities) {
+ // if not running with 'pm2', no options to add
+ if (!config.appiumWithPM2()) return nodeCapabilities;
+
+ // add indication of stand-alone execution of 'appium' with 'pm2'
+ Capabilities capabilities = config.getCapabilitiesForJson(nodeCapabilities)[0];
+ Capabilities nordOptions = config.getCapabilitiesForJson(APPIUM_WITH_PM2)[0];
+ return config.toJson(config.mergeCapabilities(capabilities, nordOptions));
+ }
+
/**
* Find the 'npm' (Node Package Manager) binary.
*
@@ -318,7 +379,7 @@ private static File findMainScript() throws GridServerLaunchFailedException {
if (SystemUtils.IS_OS_WINDOWS) {
executable = "cmd.exe";
argsList.add("/c");
- argsList.add(npm.getAbsolutePath());
+ argsList.add("\"" + npm.getAbsolutePath() + "\"");
} else {
executable = npm.getAbsolutePath();
}
@@ -391,6 +452,36 @@ public boolean shutdown(final boolean localOnly) throws InterruptedException {
}
return true;
}
+
+ /**
+ * Get stored relay node configuration path.
+ * NOTE: A relay node is needed to connect the Appium server to a Selenium 4+ Grid hub.
+ *
+ * @return path to relay node configuration; may be {@code null}
+ */
+ public Path getNodeConfigPath() {
+ String nodeConfigPath = getEnvironment().get("nodeConfigPath");
+ return (nodeConfigPath != null) ? Paths.get(nodeConfigPath) : null;
+ }
+
+ /**
+ * Get process environment from this AppiumGridServer object.
+ *
+ * @return map of process environment variables
+ */
+ @SuppressWarnings("unchecked")
+ public Map getEnvironment() {
+ try {
+ Field processField = CommandLine.class.getDeclaredField("process");
+ processField.setAccessible(true);
+ Object osProcess = processField.get(getProcess());
+ Method getEnvironment = osProcess.getClass().getDeclaredMethod("getEnvironment");
+ getEnvironment.setAccessible(true);
+ return (Map) getEnvironment.invoke(osProcess);
+ } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
+ return Collections.emptyMap();
+ }
+ }
/**
* If the specified URL is a local 'appium' node running with 'pm2', delete the process.
@@ -412,7 +503,7 @@ public static boolean shutdownAppiumWithPM2(URL nodeUrl) {
if (SystemUtils.IS_OS_WINDOWS) {
executable = "cmd.exe";
- argsList.add(0, pm2Binary.getAbsolutePath());
+ argsList.add(0, "\"" + pm2Binary.getAbsolutePath() + "\"");
argsList.add(0, "/c");
} else {
executable = pm2Binary.getAbsolutePath();
diff --git a/src/main/java/com/nordstrom/automation/selenium/plugins/EspressoPlugin.java b/src/main/java/com/nordstrom/automation/selenium/plugins/EspressoPlugin.java
index 675a54df..a9602077 100644
--- a/src/main/java/com/nordstrom/automation/selenium/plugins/EspressoPlugin.java
+++ b/src/main/java/com/nordstrom/automation/selenium/plugins/EspressoPlugin.java
@@ -34,7 +34,7 @@ public EspressoPlugin() {
@Override
public String getCapabilities(SeleniumConfig config) {
- return CAPABILITIES;
+ return addNordOptions(config, CAPABILITIES);
}
@Override
diff --git a/src/main/java/com/nordstrom/automation/selenium/plugins/HtmlUnitCaps.java b/src/main/java/com/nordstrom/automation/selenium/plugins/HtmlUnitCaps.java
index dc43e5c2..ad4dde37 100644
--- a/src/main/java/com/nordstrom/automation/selenium/plugins/HtmlUnitCaps.java
+++ b/src/main/java/com/nordstrom/automation/selenium/plugins/HtmlUnitCaps.java
@@ -14,7 +14,7 @@ private HtmlUnitCaps() {
private static final String[] PROPERTY_NAMES = { };
private static final String CAPABILITIES =
- "{\"browserName\":\"htmlunit\",\"browserVersion\":\"chrome\"}";
+ "{\"browserName\":\"htmlunit\"}";
private static final String BASELINE =
"{\"browserName\":\"htmlunit\"," +
diff --git a/src/main/java/com/nordstrom/automation/selenium/plugins/Mac2Plugin.java b/src/main/java/com/nordstrom/automation/selenium/plugins/Mac2Plugin.java
index 229c00bb..fd32ba5d 100644
--- a/src/main/java/com/nordstrom/automation/selenium/plugins/Mac2Plugin.java
+++ b/src/main/java/com/nordstrom/automation/selenium/plugins/Mac2Plugin.java
@@ -34,7 +34,7 @@ public Mac2Plugin() {
@Override
public String getCapabilities(SeleniumConfig config) {
- return CAPABILITIES;
+ return addNordOptions(config, CAPABILITIES);
}
@Override
diff --git a/src/main/java/com/nordstrom/automation/selenium/plugins/UiAutomator2Plugin.java b/src/main/java/com/nordstrom/automation/selenium/plugins/UiAutomator2Plugin.java
index a300a9ed..2e5984e0 100644
--- a/src/main/java/com/nordstrom/automation/selenium/plugins/UiAutomator2Plugin.java
+++ b/src/main/java/com/nordstrom/automation/selenium/plugins/UiAutomator2Plugin.java
@@ -35,7 +35,7 @@ public UiAutomator2Plugin() {
@Override
public String getCapabilities(SeleniumConfig config) {
- return CAPABILITIES;
+ return addNordOptions(config, CAPABILITIES);
}
@Override
diff --git a/src/main/java/com/nordstrom/automation/selenium/plugins/WindowsPlugin.java b/src/main/java/com/nordstrom/automation/selenium/plugins/WindowsPlugin.java
index 2e40afd4..4d5dcf99 100644
--- a/src/main/java/com/nordstrom/automation/selenium/plugins/WindowsPlugin.java
+++ b/src/main/java/com/nordstrom/automation/selenium/plugins/WindowsPlugin.java
@@ -34,7 +34,7 @@ public WindowsPlugin() {
@Override
public String getCapabilities(SeleniumConfig config) {
- return CAPABILITIES;
+ return addNordOptions(config, CAPABILITIES);
}
@Override
diff --git a/src/main/java/com/nordstrom/automation/selenium/plugins/XCUITestPlugin.java b/src/main/java/com/nordstrom/automation/selenium/plugins/XCUITestPlugin.java
index 83c8a25f..724ebd38 100644
--- a/src/main/java/com/nordstrom/automation/selenium/plugins/XCUITestPlugin.java
+++ b/src/main/java/com/nordstrom/automation/selenium/plugins/XCUITestPlugin.java
@@ -35,7 +35,7 @@ public XCUITestPlugin() {
@Override
public String getCapabilities(SeleniumConfig config) {
- return CAPABILITIES;
+ return addNordOptions(config, CAPABILITIES);
}
@Override
diff --git a/src/main/java/com/nordstrom/automation/selenium/servlet/ExamplePageLauncher.java b/src/main/java/com/nordstrom/automation/selenium/servlet/ExamplePageLauncher.java
index 35870bf6..90945412 100644
--- a/src/main/java/com/nordstrom/automation/selenium/servlet/ExamplePageLauncher.java
+++ b/src/main/java/com/nordstrom/automation/selenium/servlet/ExamplePageLauncher.java
@@ -2,6 +2,7 @@
import java.io.File;
import java.net.MalformedURLException;
+import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@@ -68,7 +69,7 @@ public void shutdown() {
public URL getUrl() {
try {
- return new URL("http://" + GridUtility.getLocalHost() + ":8080");
+ return URI.create("http://" + GridUtility.getLocalHost() + ":8080").toURL();
} catch (MalformedURLException e) {
// nothing to do here
}
diff --git a/src/main/resources/hubConfig-s3.json b/src/main/resources/hubConfig-s3.json
index 804def64..fba44df9 100644
--- a/src/main/resources/hubConfig-s3.json
+++ b/src/main/resources/hubConfig-s3.json
@@ -1,6 +1,6 @@
{
"newSessionWaitTimeout": -1,
- "capabilityMatcher": "com.nordstrom.automation.selenium.utility.RevisedCapabilityMatcher",
+ "capabilityMatcher": "com.nordstrom.automation.selenium.core.FoundationSlotMatcher",
"throwOnCapabilityNotPresent": true,
"cleanUpCycle": 5000,
"role": "hub",
diff --git a/src/main/resources/hubConfig-s4.json b/src/main/resources/hubConfig-s4.json
index 1ce9c51a..1fe1bf01 100644
--- a/src/main/resources/hubConfig-s4.json
+++ b/src/main/resources/hubConfig-s4.json
@@ -1,5 +1,6 @@
{
- "session-request-timeout": -1,
- "reject-unsupported-caps": true,
- "session-timeout": 300
+ "distributor": {
+ "slot-matcher": "com.nordstrom.automation.selenium.core.FoundationSlotMatcher",
+ "reject-unsupported-caps": true
+ }
}
\ No newline at end of file
diff --git a/src/main/resources/nodeConfig-s4.json b/src/main/resources/nodeConfig-s4.json
index 6332a23c..a42c5c2b 100644
--- a/src/main/resources/nodeConfig-s4.json
+++ b/src/main/resources/nodeConfig-s4.json
@@ -1,6 +1,5 @@
{
"node": {
- "detect-drivers": false,
- "driver-configuration": [ ]
+ "detect-drivers": false
}
}
\ No newline at end of file
diff --git a/src/selenium3/java/com/nordstrom/automation/selenium/SeleniumConfig.java b/src/selenium3/java/com/nordstrom/automation/selenium/SeleniumConfig.java
index 8ff5ee13..6f60bfda 100644
--- a/src/selenium3/java/com/nordstrom/automation/selenium/SeleniumConfig.java
+++ b/src/selenium3/java/com/nordstrom/automation/selenium/SeleniumConfig.java
@@ -18,11 +18,13 @@
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.lang3.StringUtils;
+import org.openqa.grid.internal.utils.CapabilityMatcher;
import org.openqa.grid.internal.utils.configuration.GridHubConfiguration;
import org.openqa.grid.internal.utils.configuration.GridNodeConfiguration;
import org.openqa.grid.web.servlet.LifecycleServlet;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.MutableCapabilities;
+import org.openqa.selenium.grid.config.ConfigException;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.json.JsonInput;
@@ -169,23 +171,13 @@ public class SeleniumConfig extends AbstractSeleniumConfig {
* <version>1.4.10</version>
*</dependency>
*/
- private static final String[] DEPENDENCY_CONTEXTS = {
- "com.nordstrom.automation.selenium.utility.RevisedCapabilityMatcher",
- "com.nordstrom.common.file.PathUtils",
- "org.apache.commons.lang3.reflect.FieldUtils",
- "net.bytebuddy.matcher.ElementMatcher",
- "org.openqa.selenium.BuildInfo",
- "com.google.common.collect.ImmutableMap",
- "com.beust.jcommander.JCommander",
- "org.openqa.selenium.json.Json",
- "org.seleniumhq.jetty9.util.thread.ThreadPool",
- "javax.servlet.Servlet",
- "okhttp3.ConnectionPool",
- "okio.BufferedSource",
- "ch.qos.logback.classic.spi.ThrowableProxy",
- "kotlin.jvm.internal.Intrinsics",
- "org.apache.commons.exec.Executor"
- };
+ private static final String[] DEPENDENCY_CONTEXTS = { "com.nordstrom.automation.selenium.core.LocalSeleniumGrid",
+ "com.nordstrom.common.file.PathUtils", "org.apache.commons.lang3.reflect.FieldUtils",
+ "net.bytebuddy.matcher.ElementMatcher", "org.openqa.selenium.BuildInfo",
+ "com.google.common.collect.ImmutableMap", "com.beust.jcommander.JCommander",
+ "org.openqa.selenium.json.Json", "org.seleniumhq.jetty9.util.thread.ThreadPool", "javax.servlet.Servlet",
+ "okhttp3.ConnectionPool", "okio.BufferedSource", "ch.qos.logback.classic.spi.ThrowableProxy",
+ "kotlin.jvm.internal.Intrinsics", "org.apache.commons.exec.Executor" };
static {
try {
@@ -214,6 +206,10 @@ public static SeleniumConfig getConfig() {
return seleniumConfig;
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public int getVersion() {
return 3;
}
@@ -242,6 +238,8 @@ public Path createHubConfig() throws IOException {
// create hub configuration from template
GridHubConfiguration hubConfig = GridHubConfiguration.loadFromJSON(hubConfigPath);
+ String slotMatcher = getString(SeleniumSettings.SLOT_MATCHER.key());
+
// get configured hub servlet collection
Set servlets = getHubServlets();
// merge with hub template servlets
@@ -249,13 +247,18 @@ public Path createHubConfig() throws IOException {
// strip extension to get template base path
String configPathBase = hubConfigPath.substring(0, hubConfigPath.length() - 5);
- // get hash code of servlets as 8-digit hexadecimal string
- String hashCode = String.format("%08X", servlets.hashCode());
+ // get hash code of slot matcher and servlets as 8-digit hexadecimal string
+ String hashCode = String.format("%08X", Objects.hash(slotMatcher, servlets));
// assemble hub configuration file path with servlets hash code
Path filePath = Paths.get(configPathBase + "-" + hashCode + ".json");
// if assembled path does not exist
if (filePath.toFile().createNewFile()) {
+ try {
+ hubConfig.capabilityMatcher = (CapabilityMatcher) Class.forName(slotMatcher).newInstance();
+ } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
+ throw new ConfigException("Failed instantiating capability matcher: " + slotMatcher, e);
+ }
hubConfig.servlets = Arrays.asList(servlets.toArray(new String[0]));
try(OutputStream fos = new FileOutputStream(filePath.toFile());
OutputStream out = new BufferedOutputStream(fos)) {
diff --git a/src/selenium3/java/com/nordstrom/automation/selenium/core/FoundationSlotMatcher.java b/src/selenium3/java/com/nordstrom/automation/selenium/core/FoundationSlotMatcher.java
new file mode 100644
index 00000000..4744c737
--- /dev/null
+++ b/src/selenium3/java/com/nordstrom/automation/selenium/core/FoundationSlotMatcher.java
@@ -0,0 +1,198 @@
+// Licensed to the Software Freedom Conservancy (SFC) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The SFC licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package com.nordstrom.automation.selenium.core;
+
+import static org.openqa.selenium.remote.BrowserType.SAFARI;
+import static org.openqa.selenium.remote.CapabilityType.BROWSER_NAME;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.openqa.grid.internal.utils.CapabilityMatcher;
+import org.openqa.selenium.Platform;
+import org.openqa.selenium.WebDriverException;
+import org.openqa.selenium.remote.CapabilityType;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.stream.Stream;
+
+/**
+ * Default (naive) implementation of the capability matcher.
+ *
+ * The default capability matcher will look at all the key from the request do not start with _ and
+ * will try to find a node that has at least those capabilities.
+ */
+public class FoundationSlotMatcher implements CapabilityMatcher {
+
+ private static final String GRID_TOKEN = "_";
+
+ interface Validator extends BiFunction