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: * @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, Map, Boolean> { + } + + private boolean anything(Object requested) { + return requested == null || ImmutableSet.of("any", "", "*").contains(requested.toString().toLowerCase()); + } + + class PlatformValidator implements Validator { + @Override + @SuppressWarnings("deprecation") + public Boolean apply(Map providedCapabilities, Map requestedCapabilities) { + Object requested = Optional.ofNullable(requestedCapabilities.get(CapabilityType.PLATFORM)) + .orElse(requestedCapabilities.get(CapabilityType.PLATFORM_NAME)); + if (anything(requested)) { + return true; + } + Object provided = Optional.ofNullable(providedCapabilities.get(CapabilityType.PLATFORM)) + .orElse(providedCapabilities.get(CapabilityType.PLATFORM_NAME)); + Platform requestedPlatform = extractPlatform(requested); + if (requestedPlatform != null) { + Platform providedPlatform = extractPlatform(provided); + return providedPlatform != null && providedPlatform.is(requestedPlatform); + } + + return provided != null && Objects.equals(requested.toString(), provided.toString()); + } + } + + class AliasedPropertyValidator implements Validator { + private String[] propertyAliases; + + AliasedPropertyValidator(String... propertyAliases) { + this.propertyAliases = propertyAliases; + } + + @Override + public Boolean apply(Map providedCapabilities, Map requestedCapabilities) { + Object requested = Stream.of(propertyAliases).map(requestedCapabilities::get).filter(Objects::nonNull) + .findFirst().orElse(null); + + if (anything(requested)) { + return true; + } + + Object provided = Stream.of(propertyAliases).map(providedCapabilities::get).filter(Objects::nonNull) + .findFirst().orElse(null); + return Objects.equals(requested, provided); + } + } + + class SimplePropertyValidator implements Validator { + private List toConsider; + + SimplePropertyValidator(String... toConsider) { + this.toConsider = Arrays.asList(toConsider); + } + + @Override + public Boolean apply(Map providedCapabilities, Map requestedCapabilities) { + return requestedCapabilities.entrySet().stream().filter(entry -> !entry.getKey().startsWith(GRID_TOKEN)) + .filter(entry -> toConsider.contains(entry.getKey())).filter(entry -> !anything(entry.getValue())) + .allMatch(entry -> entry.getValue().equals(providedCapabilities.get(entry.getKey()))); + } + } + + class FirefoxSpecificValidator implements Validator { + @Override + public Boolean apply(Map providedCapabilities, Map requestedCapabilities) { + if (!"firefox".equals(requestedCapabilities.get(BROWSER_NAME))) { + return true; + } + + if (requestedCapabilities.get("marionette") != null + && !Boolean.valueOf(requestedCapabilities.get("marionette").toString())) { + return providedCapabilities.get("marionette") != null + && !Boolean.valueOf(providedCapabilities.get("marionette").toString()); + } else { + return providedCapabilities.get("marionette") == null + || Boolean.valueOf(providedCapabilities.get("marionette").toString()); + } + } + } + + class SafariSpecificValidator implements Validator { + static final String SAFARI_TECH_PREVIEW = "Safari Technology Preview"; + static final String AUTOMATIC_INSPECTION = "safari:automaticInspection"; + static final String AUTOMATIC_PROFILING = "safari:automaticProfiling"; + static final String TECHNOLOGY_PREVIEW = "technologyPreview"; + + @Override + public Boolean apply(Map providedCapabilities, Map requestedCapabilities) { + if (!SAFARI.equals(getBrowserName(requestedCapabilities)) + && !SAFARI_TECH_PREVIEW.equals(getBrowserName(requestedCapabilities))) { + return true; + } + + return getAutomaticInspection(requestedCapabilities) == getAutomaticInspection(providedCapabilities) + && getAutomaticProfiling(requestedCapabilities) == getAutomaticProfiling(providedCapabilities) + && getUseTechnologyPreview(requestedCapabilities) == getUseTechnologyPreview(providedCapabilities); + } + + private String getBrowserName(Map capabilities) { + return (String) capabilities.get(BROWSER_NAME); + } + + private boolean getAutomaticInspection(Map capabilities) { + return Boolean.TRUE.equals(capabilities.get(AUTOMATIC_INSPECTION)); + } + + private boolean getAutomaticProfiling(Map capabilities) { + return Boolean.TRUE.equals(capabilities.get(AUTOMATIC_PROFILING)); + } + + private boolean getUseTechnologyPreview(Map capabilities) { + return SAFARI_TECH_PREVIEW.equals(getBrowserName(capabilities)) + || Boolean.TRUE.equals(capabilities.get(TECHNOLOGY_PREVIEW)); + } + } + + private final List validators = new ArrayList<>(); + { + validators.addAll(Arrays.asList(new PlatformValidator(), new AliasedPropertyValidator(BROWSER_NAME, "browser"), + new AliasedPropertyValidator(CapabilityType.BROWSER_VERSION, CapabilityType.VERSION), + new SimplePropertyValidator(CapabilityType.APPLICATION_NAME), new FirefoxSpecificValidator(), + new SafariSpecificValidator())); + } + + public void addToConsider(String capabilityName) { + validators.add(new SimplePropertyValidator(capabilityName)); + } + + public boolean matches(Map providedCapabilities, Map requestedCapabilities) { + return providedCapabilities != null && requestedCapabilities != null + && validators.stream().allMatch(v -> v.apply(providedCapabilities, requestedCapabilities)); + } + + private Platform extractPlatform(Object o) { + if (o == null) { + return null; + } + if (o instanceof Platform) { + return (Platform) o; + } + try { + return Platform.fromString(o.toString()); + } catch (WebDriverException ex) { + return null; + } + } +} diff --git a/src/selenium3/java/com/nordstrom/automation/selenium/core/GridServer.java b/src/selenium3/java/com/nordstrom/automation/selenium/core/GridServer.java index b61b62e9..2763504a 100644 --- a/src/selenium3/java/com/nordstrom/automation/selenium/core/GridServer.java +++ b/src/selenium3/java/com/nordstrom/automation/selenium/core/GridServer.java @@ -29,7 +29,7 @@ public class GridServer { private boolean isHub; private URL serverUrl; protected String statusRequest; - protected String shutdownRequest; + protected String[] shutdownRequest; public static final String GRID_CONSOLE = "/grid/console"; public static final String HUB_BASE = "/wd/hub"; @@ -37,8 +37,8 @@ public class GridServer { public static final String HUB_CONFIG = "/grid/api/hub/"; public static final String NODE_CONFIG = "/grid/api/proxy"; - private static final String HUB_SHUTDOWN = "/lifecycle-manager?action=shutdown"; - private static final String NODE_SHUTDOWN = "/extra/LifecycleServlet?action=shutdown"; + private static final String[] HUB_SHUTDOWN = { "/lifecycle-manager", "action=shutdown" }; + private static final String[] NODE_SHUTDOWN = { "/extra/LifecycleServlet", "action=shutdown" }; private static final long SHUTDOWN_DELAY = 15; public GridServer(URL url, boolean isHub) { @@ -180,7 +180,7 @@ public static List getNodeCapabilities(SeleniumConfig config, URL private static String getStatusOfNode(SeleniumConfig config, URL hubUrl, URL nodeUrl) throws IOException { String nodeEndpoint = nodeUrl.getProtocol() + "://" + nodeUrl.getAuthority(); String url = hubUrl.getProtocol() + "://" + hubUrl.getAuthority() + NODE_CONFIG + "?id=" + nodeEndpoint; - try (InputStream is = new URL(url).openStream()) { + try (InputStream is = URI.create(url).toURL().openStream()) { return GridUtility.readAvailable(is); } } diff --git a/src/selenium3/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java b/src/selenium3/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java index 3e9f9fd6..fb9a2417 100644 --- a/src/selenium3/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java +++ b/src/selenium3/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java @@ -25,6 +25,7 @@ import com.nordstrom.common.base.UncheckedThrow; import com.nordstrom.common.file.PathUtils; import com.nordstrom.common.jar.JarUtils; +import com.nordstrom.common.uri.UriUtils; /** * This class launches Selenium Grid server instances, each in its own system process. Clients of this class specify @@ -351,7 +352,7 @@ public boolean shutdown(final boolean localOnly) throws InterruptedException { */ public static URL getServerUrl(String host, Integer port) { try { - return new URL("http://" + host + ":" + port.toString() + GridServer.HUB_BASE); + return UriUtils.makeBasicURI("http", host, port, GridServer.HUB_BASE).toURL(); } catch (MalformedURLException e) { throw UncheckedThrow.throwUnchecked(e); } diff --git a/src/selenium3/java/com/nordstrom/automation/selenium/utility/RevisedCapabilityMatcher.java b/src/selenium3/java/com/nordstrom/automation/selenium/utility/RevisedCapabilityMatcher.java deleted file mode 100644 index 53bb2260..00000000 --- a/src/selenium3/java/com/nordstrom/automation/selenium/utility/RevisedCapabilityMatcher.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.nordstrom.automation.selenium.utility; - -import static net.bytebuddy.matcher.ElementMatchers.*; -import static org.openqa.selenium.remote.BrowserType.SAFARI; -import static org.openqa.selenium.remote.CapabilityType.BROWSER_NAME; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang3.reflect.FieldUtils; -import org.openqa.grid.internal.utils.DefaultCapabilityMatcher; - -import com.nordstrom.automation.selenium.model.Enhanceable; - -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.implementation.MethodDelegation; - -/** - * This capability matcher is functionally equivalent to {@link DefaultCapabilityMatcher}, implemented to avoid direct - * references to the {@code SafariOptions} class. This avoids the need to include the path to the safari-driver - * JAR on the class path provided to the Selenium Grid hub process. - */ -public class RevisedCapabilityMatcher extends DefaultCapabilityMatcher { - - private static final String SAFARI_SPECIFIC_VALIDATOR = "org.openqa.grid.internal.utils.DefaultCapabilityMatcher$SafariSpecificValidator"; - private static final String REVISED_SAFARI_VALIDATOR = "org.openqa.grid.internal.utils.DefaultCapabilityMatcher$RevisedSafariValidator"; - - private static Class safariValidator; - - /** - * This constructor replaces the {@code SafariSpecificValidator} instance in the validators list of the - * {@link DefaultCapabilityMatcher} with an instance of a dynamically-generated {@code RevisedSafariValidator} - * class. This dynamic validator is functionally equivalent, but is implemented without explicit references to - * the {@code SafariOptions} class. - */ - @SuppressWarnings("unchecked") - public RevisedCapabilityMatcher() { - super(); - Field field = FieldUtils.getField(DefaultCapabilityMatcher.class, "validators", true); - if (field != null) { - field.setAccessible(true); - try { - List list = (List) field.get(this); - Iterator iter = list.iterator(); - while (iter.hasNext()) { - Object item = iter.next(); - Class clazz = item.getClass(); - if (SAFARI_SPECIFIC_VALIDATOR.equals(clazz.getName())) { - Object validator = newSafariValidator(clazz); - iter.remove(); - list.add(validator); - break; - } - } - } catch (IllegalArgumentException | IllegalAccessException | ClassCastException | InstantiationException - | InvocationTargetException | NoSuchMethodException | SecurityException e) { - // just eat the exception - } - } - } - - /** - * Create a new instance of the dynamically-generated {@code RevisedSafariValidator} class. - * - * @param validatorClass {@code SafariSpecificValidator} class - * @return instance of dynamically-generated replacement class - * @throws InstantiationException if this class represents an abstract class, an interface, an array class, a - * primitive type, or {@code void}; if the class lacks a no-argument constructor; or if instantiation - * fails for some other reason. - * @throws IllegalAccessException if the class or its no-argument constructor are inaccessible. - * @throws SecurityException if not authorized to access target class loader - * @throws NoSuchMethodException if no-argument constructor is absent - * @throws InvocationTargetException if constructor threw an exception - * @throws IllegalArgumentException if (non-existent) constructor arguments don't match - */ - private static Object newSafariValidator(Class validatorClass) - throws InstantiationException, IllegalAccessException, IllegalArgumentException, - InvocationTargetException, NoSuchMethodException, SecurityException { - if (safariValidator == null) { - safariValidator = subclassSafariValidator(validatorClass); - } - return safariValidator.getConstructor().newInstance(); - } - - /** - * Dynamically generate a replacement for the {@code SafariSpecificValidator} class. - * - * @param validatorClass {@code SafariSpecificValidator} class - * @return dynamically-generated {@code RevisedSafariValidator} class - */ - private static Class subclassSafariValidator(Class validatorClass) { - Class validatorIntfc = validatorClass.getInterfaces()[0]; - return new ByteBuddy() - .subclass(validatorIntfc) - .name(REVISED_SAFARI_VALIDATOR) - .method(named("apply")) - .intercept(MethodDelegation.to(SafariValidator.class)) - .make() - .load(validatorClass.getClassLoader(), Enhanceable.getClassLoadingStrategy(validatorClass)) - .getLoaded(); - } - - /** - * This class implements the {@code apply()} method declared by the {@code Validator} interface. It also implements - * methods to extract Safari-specific settings from specified capabilities maps. - */ - public static class SafariValidator { - static final String SAFARI_TECH_PREVIEW = "Safari Technology Preview"; - static final String AUTOMATIC_INSPECTION = "safari:automaticInspection"; - static final String AUTOMATIC_PROFILING = "safari:automaticProfiling"; - static final String TECHNOLOGY_PREVIEW = "technologyPreview"; - - public static Boolean apply(Map providedCapabilities, Map requestedCapabilities) { - if (!SAFARI.equals(getBrowserName(requestedCapabilities)) && - !SAFARI_TECH_PREVIEW.equals(getBrowserName(requestedCapabilities))) { - return true; - } - - return getAutomaticInspection(requestedCapabilities) == getAutomaticInspection(providedCapabilities) && - getAutomaticProfiling(requestedCapabilities) == getAutomaticProfiling(providedCapabilities) && - getUseTechnologyPreview(requestedCapabilities) == getUseTechnologyPreview(providedCapabilities); - } - - public static String getBrowserName(Map capabilities) { - return (String) capabilities.get(BROWSER_NAME); - } - - public static boolean getAutomaticInspection(Map capabilities) { - return Boolean.TRUE.equals(capabilities.get(AUTOMATIC_INSPECTION)); - } - - public static boolean getAutomaticProfiling(Map capabilities) { - return Boolean.TRUE.equals(capabilities.get(AUTOMATIC_PROFILING)); - } - - public static boolean getUseTechnologyPreview(Map capabilities) { - return SAFARI_TECH_PREVIEW.equals(getBrowserName(capabilities)) || - Boolean.TRUE.equals(capabilities.get(TECHNOLOGY_PREVIEW)); - } - } -} diff --git a/src/selenium4/java/com/nordstrom/automation/selenium/SeleniumConfig.java b/src/selenium4/java/com/nordstrom/automation/selenium/SeleniumConfig.java index f0a10999..08ba3f1f 100644 --- a/src/selenium4/java/com/nordstrom/automation/selenium/SeleniumConfig.java +++ b/src/selenium4/java/com/nordstrom/automation/selenium/SeleniumConfig.java @@ -27,7 +27,9 @@ import org.openqa.selenium.MutableCapabilities; import org.openqa.selenium.grid.config.ConfigException; import org.openqa.selenium.json.Json; +import org.openqa.selenium.net.PortProber; +import com.nordstrom.automation.selenium.core.GridServer; import com.nordstrom.automation.selenium.core.GridUtility; import com.nordstrom.automation.settings.SettingsCore; @@ -308,10 +310,13 @@ public class SeleniumConfig extends AbstractSeleniumConfig { * <version>1.0.1</version> *</dependency> */ - private static final String[] DEPENDENCY_CONTEXTS = { "org.openqa.selenium.grid.Main", - "com.beust.jcommander.Strings", "org.openqa.selenium.remote.http.Route", "com.google.common.base.Utf8", - "org.openqa.selenium.Keys", "org.openqa.selenium.remote.tracing.Tracer", "org.openqa.selenium.json.Json", - "io.opentelemetry.sdk.autoconfigure.ResourceConfiguration", + private static final String[] DEPENDENCY_CONTEXTS = { "com.nordstrom.automation.selenium.core.LocalSeleniumGrid", + "com.nordstrom.common.file.PathUtils", "org.apache.commons.lang3.StringUtils", + "org.eclipse.jetty.util.Attributes", "javax.servlet.http.HttpServletResponse", + "org.eclipse.jetty.http.HttpField", "org.openqa.selenium.chromium.ChromiumDriver", + "org.openqa.selenium.grid.Main", "com.beust.jcommander.Strings", "org.openqa.selenium.remote.http.Route", + "com.google.common.base.Utf8", "org.openqa.selenium.Keys", "org.openqa.selenium.remote.tracing.Tracer", + "org.openqa.selenium.json.Json", "io.opentelemetry.sdk.autoconfigure.ResourceConfiguration", "io.opentelemetry.sdk.autoconfigure.spi.Ordered", "io.opentelemetry.api.trace.Span", "io.opentelemetry.sdk.trace.SdkSpan", "io.opentelemetry.context.Scope", "io.opentelemetry.sdk.metrics.View", "io.opentelemetry.sdk.logs.LogLimits", "io.opentelemetry.sdk.common.Clock", @@ -353,6 +358,10 @@ public static SeleniumConfig getConfig() { return seleniumConfig; } + /** + * {@inheritDoc} + */ + @Override public int getVersion() { return 4; } @@ -375,26 +384,68 @@ protected Map getDefaults() { * {@inheritDoc} */ @Override + @SuppressWarnings("unchecked") public Path createHubConfig() throws IOException { - return getHubConfigPath(); + Map hubConfig; + // get path to hub configuration template + String hubConfigPath = getHubConfigPath().toString(); + // create hub configuration from template + try (Reader reader = Files.newBufferedReader(getHubConfigPath())) { + hubConfig = new Json().toType(reader, MAP_TYPE); + } catch (IOException e) { + throw new ConfigException("Failed reading hub configuration template.", e); + } + + String slotMatcher = getString(SeleniumSettings.SLOT_MATCHER.key()); + + // strip extension to get template base path + String configPathBase = hubConfigPath.substring(0, hubConfigPath.length() - 5); + // get hash code of slot matcher as 8-digit hexadecimal string + String hashCode = String.format("%08X", Objects.hash(slotMatcher)); + // assemble hub configuration file path with aggregated hash code + Path filePath = Paths.get(configPathBase + "-" + hashCode + ".json"); + + // if assembled path does not exist + if (filePath.toFile().createNewFile()) { + // add driver configuration + Map distributorOptions = (Map) hubConfig.get("distributor"); + distributorOptions.put("slot-matcher", slotMatcher); + try (OutputStream fos = new FileOutputStream(filePath.toFile()); + OutputStream out = new BufferedOutputStream(fos)) { + out.write(new Json().toJson(hubConfig).getBytes(StandardCharsets.UTF_8)); + } + } + return filePath; } /** * {@inheritDoc} */ - @SuppressWarnings("unchecked") @Override + @SuppressWarnings("unchecked") public Path createNodeConfig(String capabilities, URL hubUrl) throws IOException { Map nodeConfig; + boolean isAppium = capabilities.contains("appium"); // get path to node configuration template String nodeConfigPath = getNodeConfigPath().toString(); // create node configuration from template try (Reader reader = Files.newBufferedReader(getNodeConfigPath())) { nodeConfig = new Json().toType(reader, MAP_TYPE); - Map nodeOption = (Map) nodeConfig.computeIfAbsent("node", k -> new HashMap<>()); - nodeOption.put("hub", hubUrl.toString()); - nodeOption.computeIfAbsent("detect-drivers", k -> false); - nodeOption.computeIfAbsent("driver-configuration", k -> new ArrayList<>()); + Map nodeOptions = (Map) nodeConfig.computeIfAbsent("node", k -> new HashMap<>()); + nodeOptions.put("hub", hubUrl.getProtocol() + "://" + hubUrl.getAuthority() + GridServer.GRID_REGISTER); + nodeOptions.computeIfAbsent("detect-drivers", k -> false); + // if Appium + if (isAppium) { + // create relay configuration template if absent + Map relayOptions = (Map) nodeConfig.computeIfAbsent("relay", k -> new HashMap<>()); + relayOptions.computeIfAbsent("host", k -> GridUtility.getLocalHost()); + relayOptions.computeIfAbsent("port", k -> PortProber.findFreePort()); + relayOptions.computeIfAbsent("configs", k -> new ArrayList<>()); + // otherwise (not Appium) + } else { + // add driver configuration template if absent + nodeOptions.computeIfAbsent("driver-configuration", k -> new ArrayList<>()); + } } catch (IOException e) { throw new ConfigException("Failed reading node configuration template.", e); } catch (ClassCastException e) { @@ -418,14 +469,27 @@ public Path createNodeConfig(String capabilities, URL hubUrl) throws IOException // if assembled path does not exist if (filePath.toFile().createNewFile()) { - Map nodeOption = (Map) nodeConfig.get("node"); - List driverConfiguration = (List) nodeOption.get("driver-configuration"); - capabilitiesList.stream().forEach(theseCaps -> { - Map thisConfig = new HashMap<>(); - thisConfig.put("display-name", GridUtility.getPersonality(theseCaps)); - thisConfig.put("stereotype", theseCaps); - driverConfiguration.add(thisConfig); - }); + // if Appium + if (isAppium) { + // add relay slot specification + Map relayOptions = (Map) nodeConfig.get("relay"); + List configs = (List) relayOptions.get("configs"); + capabilitiesList.stream().forEach(theseCaps -> { + configs.add("1"); + configs.add(toJson(theseCaps)); + }); + // otherwise (not Appium) + } else { + // add driver configuration + Map nodeOptions = (Map) nodeConfig.get("node"); + List driverConfiguration = (List) nodeOptions.get("driver-configuration"); + capabilitiesList.stream().forEach(theseCaps -> { + Map thisConfig = new HashMap<>(); + thisConfig.put("stereotype", theseCaps); + thisConfig.put("display-name", GridUtility.getPersonality(theseCaps)); + driverConfiguration.add(thisConfig); + }); + } try (OutputStream fos = new FileOutputStream(filePath.toFile()); OutputStream out = new BufferedOutputStream(fos)) { out.write(new Json().toJson(nodeConfig).getBytes(StandardCharsets.UTF_8)); diff --git a/src/selenium4/java/com/nordstrom/automation/selenium/core/FoundationSlotMatcher.java b/src/selenium4/java/com/nordstrom/automation/selenium/core/FoundationSlotMatcher.java new file mode 100644 index 00000000..ba6c4121 --- /dev/null +++ b/src/selenium4/java/com/nordstrom/automation/selenium/core/FoundationSlotMatcher.java @@ -0,0 +1,145 @@ +//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 java.io.Serializable; +import java.util.Objects; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.grid.data.SlotMatcher; +import org.openqa.selenium.remote.Browser; + +/** +* Default matching implementation for slots, loosely based on the requirements for capability +* matching from the WebDriver spec. A match is made if the following are all true: +* +*
    +*
  • All non-extension capabilities from the {@code stereotype} match those in the {@link +* Capabilities} being considered. +*
  • If the {@link Capabilities} being considered contain any of: +*
      +*
    • browserName +*
    • browserVersion +*
    • platformName +*
    +* Then the {@code stereotype} must contain the same values. +*
+* +*

One thing to note is that extension capabilities are not considered when matching slots, since +* the matching of these is implementation-specific to each driver. +*/ +@SuppressWarnings("serial") +public class FoundationSlotMatcher implements SlotMatcher, Serializable { + + @Override + public boolean matches(Capabilities stereotype, Capabilities capabilities) { + + if (capabilities.asMap().isEmpty()) { + return false; + } + + if (!initialMatch(stereotype, capabilities)) { + return false; + } + + if (!managedDownloadsEnabled(stereotype, capabilities)) { + return false; + } + + if (!platformVersionMatch(stereotype, capabilities)) { + return false; + } + + if (!extensionCapabilitiesMatch(stereotype, capabilities)) { + return false; + } + + // At the end, a simple browser, browserVersion and platformName match + boolean browserNameMatch = capabilities.getBrowserName() == null + || capabilities.getBrowserName().isEmpty() + || Objects.equals(stereotype.getBrowserName(), capabilities.getBrowserName()); + boolean browserVersionMatch = Browser.HTMLUNIT.is(capabilities) + || capabilities.getBrowserVersion() == null + || capabilities.getBrowserVersion().isEmpty() + || Objects.equals(capabilities.getBrowserVersion(), "stable") + || Objects.equals(stereotype.getBrowserVersion(), capabilities.getBrowserVersion()); + boolean platformNameMatch = Browser.HTMLUNIT.is(capabilities) + || capabilities.getPlatformName() == null + || Objects.equals(stereotype.getPlatformName(), capabilities.getPlatformName()) + || (stereotype.getPlatformName() != null + && stereotype.getPlatformName().is(capabilities.getPlatformName())); + return browserNameMatch && browserVersionMatch && platformNameMatch; + } + + private Boolean initialMatch(Capabilities stereotype, Capabilities capabilities) { + return stereotype.getCapabilityNames().stream() + // Matching of extension capabilities is implementation independent. Skip them + .filter(name -> !name.contains(":")) + // Platform matching is special, we do it later + .filter(name -> !"platformName".equalsIgnoreCase(name)) + .filter(name -> capabilities.getCapability(name) != null).map(name -> { + if (stereotype.getCapability(name) instanceof String + && capabilities.getCapability(name) instanceof String) { + return ((String) stereotype.getCapability(name)) + .equalsIgnoreCase((String) capabilities.getCapability(name)); + } + return Objects.equals(stereotype.getCapability(name), capabilities.getCapability(name)); + }).reduce(Boolean::logicalAnd).orElse(true); + } + + private Boolean managedDownloadsEnabled(Capabilities stereotype, Capabilities capabilities) { + // First lets check if user wanted a Node with managed downloads enabled + Object raw = capabilities.getCapability("se:downloadsEnabled"); + if (raw == null || !Boolean.parseBoolean(raw.toString())) { + // User didn't ask. So lets move on to the next matching criteria + return true; + } + // User wants managed downloads enabled to be done on this Node, let's check the + // stereotype + raw = stereotype.getCapability("se:downloadsEnabled"); + // Try to match what the user requested + return raw != null && Boolean.parseBoolean(raw.toString()); + } + + private Boolean platformVersionMatch(Capabilities stereotype, Capabilities capabilities) { + /* + * This platform version match is not W3C compliant but users can add Appium + * servers as Nodes, so we avoid delaying the match until the Slot, which makes + * the whole matching process faster. + */ + return capabilities.getCapabilityNames().stream().filter(name -> name.contains("platformVersion")) + .map(platformVersionCapName -> Objects.equals(stereotype.getCapability(platformVersionCapName), + capabilities.getCapability(platformVersionCapName))) + .reduce(Boolean::logicalAnd).orElse(true); + } + + private Boolean extensionCapabilitiesMatch(Capabilities stereotype, Capabilities capabilities) { + return stereotype.getCapabilityNames().stream().filter(name -> name.contains(":")) + .filter(name -> capabilities.getCapability(name) != null).map(name -> { + if (stereotype.getCapability(name) instanceof String + && capabilities.getCapability(name) instanceof String) { + return ((String) stereotype.getCapability(name)) + .equalsIgnoreCase((String) capabilities.getCapability(name)); + } + if (capabilities.getCapability(name) instanceof Number + || capabilities.getCapability(name) instanceof Boolean) { + return Objects.equals(stereotype.getCapability(name), capabilities.getCapability(name)); + } + return true; + }).reduce(Boolean::logicalAnd).orElse(true); + } +} diff --git a/src/selenium4/java/com/nordstrom/automation/selenium/core/GridServer.java b/src/selenium4/java/com/nordstrom/automation/selenium/core/GridServer.java index dd4ceb37..91e33a8a 100644 --- a/src/selenium4/java/com/nordstrom/automation/selenium/core/GridServer.java +++ b/src/selenium4/java/com/nordstrom/automation/selenium/core/GridServer.java @@ -29,6 +29,7 @@ public class GridServer { public static final String HUB_BASE = "/wd/hub"; public static final String SERVER_STATUS = "/status"; + public static final String GRID_REGISTER = "/grid/register"; public GridServer(URL url, boolean isHub) { this.isHub = isHub; diff --git a/src/selenium4/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java b/src/selenium4/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java index 0bc72815..4f51bba3 100644 --- a/src/selenium4/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java +++ b/src/selenium4/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java @@ -22,9 +22,11 @@ import com.nordstrom.automation.selenium.DriverPlugin; import com.nordstrom.automation.selenium.SeleniumConfig; import com.nordstrom.automation.selenium.exceptions.GridServerLaunchFailedException; +import com.nordstrom.automation.selenium.plugins.AbstractAppiumPlugin.AppiumGridServer; import com.nordstrom.common.base.UncheckedThrow; import com.nordstrom.common.file.PathUtils; import com.nordstrom.common.jar.JarUtils; +import com.nordstrom.common.uri.UriUtils; /** * This class launches Selenium Grid server instances, each in its own system process. Clients of this class specify @@ -103,7 +105,10 @@ public static void awaitGridReady(GridServer hubServer, Collection n static boolean isGridReady(SeleniumConfig config, GridServer hubServer, Collection nodeServers) { if (!GridServer.isHubActive(hubServer.getUrl())) return false; for (GridServer nodeServer : nodeServers) { - if (!GridServer.isNodeRegistered(config, hubServer.getUrl(), nodeServer.getUrl())) return false; + // if not an Appium Grid server + if (!(nodeServer instanceof AppiumGridServer)) { + if (!GridServer.isNodeRegistered(config, hubServer.getUrl(), nodeServer.getUrl())) return false; + } } return true; } @@ -138,8 +143,21 @@ public static SeleniumGrid create(SeleniumConfig config, final Path hubConfigPat System.setProperty(SeleniumSettings.HUB_PORT.key(), Integer.toString(hubServer.getUrl().getPort())); List nodeServers = new ArrayList<>(); + // iterate over configured driver plugins for (DriverPlugin driverPlugin : GridUtility.getDriverPlugins(config)) { - nodeServers.add(driverPlugin.create(config, launcherClassName, dependencyContexts, hubServer.getUrl(), workingPath)); + // create node server for this driver plugin + LocalGridServer nodeServer = driverPlugin.create(config, launcherClassName, dependencyContexts, + hubServer.getUrl(), workingPath); + // add server to nodes list + nodeServers.add(nodeServer); + // if this is an Appium Grid server + if (nodeServer instanceof AppiumGridServer) { + // get path to relay configuration path from Appium process environment + Path nodeConfigPath = ((AppiumGridServer) nodeServer).getNodeConfigPath(); + // add relay node for Appium Grid server to nodes list + nodeServers.add(create(config, launcherClassName, dependencyContexts, false, 0, nodeConfigPath, + workingPath, GridUtility.getOutputPath(config, null))); + } } return new LocalSeleniumGrid(config, hubServer, nodeServers.toArray(new LocalGridServer[0])); @@ -350,7 +368,7 @@ public boolean shutdown(final boolean localOnly) throws InterruptedException { */ public static URL getServerUrl(String host, Integer port) { try { - return new URL("http://" + host + ":" + port.toString() + GridServer.HUB_BASE); + return UriUtils.makeBasicURI("http", host, port, GridServer.HUB_BASE).toURL(); } catch (MalformedURLException e) { throw UncheckedThrow.throwUnchecked(e); } diff --git a/src/selenium4/java/com/nordstrom/automation/selenium/core/NodeStatus.java b/src/selenium4/java/com/nordstrom/automation/selenium/core/NodeStatus.java index 4a9d1b9b..9577ce3c 100644 --- a/src/selenium4/java/com/nordstrom/automation/selenium/core/NodeStatus.java +++ b/src/selenium4/java/com/nordstrom/automation/selenium/core/NodeStatus.java @@ -11,6 +11,7 @@ import org.openqa.selenium.MutableCapabilities; import org.openqa.selenium.grid.data.Availability; import org.openqa.selenium.grid.data.NodeId; +import org.openqa.selenium.json.Json; import org.openqa.selenium.json.JsonInput; import com.nordstrom.automation.selenium.utility.DataUtils; @@ -86,4 +87,9 @@ public Availability getStatus() { public List getCapabilities() { return capabilities; } + + @Override + public String toString() { + return new Json().toJson(this); + } } diff --git a/src/test/java/com/nordstrom/automation/selenium/core/GridUtilityTest.java b/src/test/java/com/nordstrom/automation/selenium/core/GridUtilityTest.java index bd50a634..df266a19 100644 --- a/src/test/java/com/nordstrom/automation/selenium/core/GridUtilityTest.java +++ b/src/test/java/com/nordstrom/automation/selenium/core/GridUtilityTest.java @@ -10,6 +10,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.util.concurrent.TimeoutException; @@ -41,11 +42,10 @@ public void testHostNullCheck() { } @NoDriver - @Test(expectedExceptions = {NullPointerException.class}, - expectedExceptionsMessageRegExp = "\\[request\\] must be non-null") - public void testRequestNullCheck() throws MalformedURLException { - URL hostUrl = new URL("http://" + GridUtility.getLocalHost()); - GridUtility.isHostActive(hostUrl, null); + @Test + public void testHostWithoutRequest() throws MalformedURLException { + URI hostUri = URI.create("https://github.com"); + assertTrue(GridUtility.isHostActive(hostUri.toURL()), "Failed activity check for: " + hostUri); } @Test diff --git a/uiautomator2Deps.gradle b/uiautomator2Deps.gradle index c71c8a7d..84c02239 100644 --- a/uiautomator2Deps.gradle +++ b/uiautomator2Deps.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/windowsDeps.gradle b/windowsDeps.gradle index 180dbb81..62b1aee5 100644 --- a/windowsDeps.gradle +++ b/windowsDeps.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/xcuitestDeps.gradle b/xcuitestDeps.gradle index 94d476f8..d4771c62 100644 --- a/xcuitestDeps.gradle +++ b/xcuitestDeps.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' } }