Skip to content

Commit

Permalink
Add Appium support for Selenium 4 Grid (#274)
Browse files Browse the repository at this point in the history
  • Loading branch information
sbabcoc authored Sep 30, 2024
1 parent de09dc2 commit b216a3d
Show file tree
Hide file tree
Showing 34 changed files with 703 additions and 310 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions espressoDeps.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
1 change: 1 addition & 0 deletions mac2Deps.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
18 changes: 9 additions & 9 deletions selenium4Deps.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ ext.libsDir = new File(buildRoot, 'libs')

java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
languageVersion = JavaLanguageVersion.of(17)
}
}

Expand All @@ -25,25 +25,25 @@ 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'
api 'com.codeborne:phantomjsdriver:1.5.0'
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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <b>Selenium Grid</b> hub server.
*
* name: <b>selenium.slot.matcher</b><br>
* default: <b>com.nordstrom.automation.selenium.core.FoundationSlotMatcher</b>
*/
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 <b>Selenium Grid</b> hub server.
Expand Down Expand Up @@ -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();

/**
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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[] {};
}
Expand Down Expand Up @@ -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}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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()));
}

/**
Expand All @@ -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);
}
Expand Down Expand Up @@ -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: <ul>
* <li>{@code true} = hub</li>
* <li>{@code false} = node</li>
* <li>{@code null} = relay</li>
* </ul>
* @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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
* <b>The {@code SeleniumGrid} Object</b>
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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<String, String>() {{
put("package", components[2]);
put("appActivity", components[3]);
}});
} else {
driver.get(url);
}
Expand Down
Loading

0 comments on commit b216a3d

Please sign in to comment.