Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP for mobile support #147

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
9 changes: 7 additions & 2 deletions java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -94,6 +94,11 @@
<version>1.25</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.appium</groupId>
<artifactId>java-client</artifactId>
<version>7.3.0</version>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.Set;

public enum Browser {
NONE(""),
CHROME("chrome"),
INTERNET_EXPLORER("internet explorer"),
EDGE("MicrosoftEdge"),
Expand Down
20 changes: 17 additions & 3 deletions java/src/main/java/com/saucelabs/saucebindings/DataCenter.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,30 @@
import lombok.Getter;

public enum DataCenter {
US_WEST("ondemand.us-west-1.saucelabs.com"),
US_EAST("ondemand.us-east-1.saucelabs.com"),
EU_CENTRAL("ondemand.eu-central-1.saucelabs.com");
US_LEGACY("https://ondemand.saucelabs.com/wd/hub"),
US_WEST("https://ondemand.us-west-1.saucelabs.com/wd/hub"),
US_EAST("https://ondemand.us-east-1.saucelabs.com/wd/hub"),
HEADLESS("https://ondemand.us-east-1.saucelabs.com/wd/hub"),
EU_CENTRAL("https://ondemand.eu-central-1.saucelabs.com/wd/hub"),
US_MOBILE("https://us1.appium.testobject.com/wd/hub"),
EU_MOBILE("https://eu1.appium.testobject.com/wd/hub"),
US_TEST_OBJECT("https://us1-manual.app.testobject.com/wd/hub"),
EU_TEST_OBJECT("https://appium.testobject.com/wd/hub");

@Getter private final String value;

DataCenter(String value) {
this.value = value;
}

public boolean supportsW3C() {
return ("https://ondemand.saucelabs.com/wd/hub".equals(value));
}

public boolean isTestObject() {
return value.contains("testobject");
}

public String toString() {
return value;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.saucelabs.saucebindings;

import lombok.Getter;

public enum DeviceOrientation {
PORTRAIT("portrait"),
LANDSCAPE("landscape");

@Getter private final String value;

DeviceOrientation(String value) {
this.value = value;
}

public String toString() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.saucelabs.saucebindings;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.openqa.selenium.MutableCapabilities;

@Accessors(chain = true)
@Setter @Getter
public class SauceAndroidOptions extends SauceOptions {

// minimal configuration
private DeviceOrientation deviceOrientation;
private String deviceName;
private String platformVersion;

public SauceAndroidOptions(String deviceName, String platformVersion, DeviceOrientation deviceOrientation){
super();
this.platformName = SaucePlatform.ANDROID;
this.browserName = Browser.NONE;
this.browserVersion = "";
this.deviceName = deviceName;
this.platformVersion = platformVersion;
this.deviceOrientation = deviceOrientation;
}

public SauceAndroidOptions(MutableCapabilities capabilities){
super(capabilities);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.saucelabs.saucebindings;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.openqa.selenium.MutableCapabilities;

@Accessors(chain = true)
@Setter @Getter
public class SauceIOSOptions extends SauceOptions {

// minimal configuration
private DeviceOrientation deviceOrientation;
private String deviceName;
private String platformVersion;

public SauceIOSOptions(String deviceName, String platformVersion, DeviceOrientation deviceOrientation){
super();
this.platformName = SaucePlatform.IOS;
this.browserName = Browser.NONE;
this.platformVersion = "";
this.deviceName = deviceName;
this.platformVersion = platformVersion;
this.deviceOrientation = deviceOrientation;
}

public SauceIOSOptions(MutableCapabilities capabilities){
super(capabilities);
}
}
175 changes: 175 additions & 0 deletions java/src/main/java/com/saucelabs/saucebindings/SauceMobileOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.saucelabs.saucebindings;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.openqa.selenium.MutableCapabilities;

import java.util.List;

@Accessors(chain = true)
@Setter @Getter
public class SauceMobileOptions extends SauceOptions {
@Setter(AccessLevel.NONE) private MutableCapabilities appiumCapabilities;

// Defined in W3C
private Browser browserName = Browser.CHROME;
private SaucePlatform platformName = SaucePlatform.ANDROID;

// Defined in Appium
// These are the only values that are handled by Sauce Labs by default
// Additional values will be populated by Appium's IOSOptions & AndroidOptions class instances
private String app;
private String deviceName;
private String deviceOrientation = "portrait";
private String platformVersion = "10";
private String automationName;

// Supported by Sauce
private String appiumVersion = "1.15.0";
private String deviceType; // "table" or "phone"

// Supported by Sauce for Real Devices
private String testobject_app_id;
private String privateDeviceOnly;
private String tabletOnly;
private String phoneOnly;
private String carrierConnectivityOnly;
private String recordDeviceVitals;
private String cacheId;
private String testobject_test_live_view_url;
private String testobject_test_report_url;
private String testobject_test_report_api_url;
private String testobject_session_creation_timeout;
private String commandTimeouts;
private String crosswalkApplication;
private String autoGrantPermissions;
private String enableAnimations;
private String name;

public static final List<String> mobileW3COptions = List.of(
"browserName",
"platformName");

public static final List<String> mobileSauceDefinedOptions = List.of(
"appiumVersion",
"deviceType");

public static final List<String> appiumDefinedOptions = List.of(
"app",
"automationName",
"deviceName",
"platformVersion",
"deviceOrientation");

public static final List<String> realDeviceSauceDefinedOptions = List.of(
"testobject_app_id",
"privateDeviceOnly",
"tabletOnly",
"phoneOnly",
"carrierConnectivityOnly",
"recordDeviceVitals",
"cacheId",
"testobject_test_live_view_url",
"testobject_test_report_url",
"testobject_test_report_api_url",
"testobject_session_creation_timeout",
"commandTimeouts",
"crosswalkApplication",
"autoGrantPermissions",
"enableAnimations",
"name");

public SauceMobileOptions() {
this(new MutableCapabilities());
}

// TODO: require users to work with Appium's MobileOptions class similar to Selenium
public SauceMobileOptions(MutableCapabilities options) {
appiumCapabilities = new MutableCapabilities(options.asMap());
}

public MutableCapabilities toCapabilities(DataCenter dataCenter) {
if (app != null) {
browserName = null;
}

mobileW3COptions.forEach((capability) -> {
addCapabilityIfDefined(appiumCapabilities, capability);
});

if (dataCenter.supportsW3C()) {
useW3cCapabilities();
} else {
useJwpCapabilities(dataCenter.isTestObject());
}
return appiumCapabilities;
}

private void useW3cCapabilities() {
MutableCapabilities sauceCapabilities = new MutableCapabilities();
useSaucePlatform(sauceCapabilities);

appiumDefinedOptions.forEach((capability) -> {
addAppiumCapabilityIfDefined(appiumCapabilities, capability);
});

mobileSauceDefinedOptions.forEach((capability) -> {
addCapabilityIfDefined(sauceCapabilities, capability);
});

sauceDefinedOptions.forEach((capability) -> {
addCapabilityIfDefined(appiumCapabilities, capability);
});

appiumCapabilities.setCapability("sauce:options", sauceCapabilities);
}

private void useJwpCapabilities(boolean to) {
if (to) {
appiumCapabilities.setCapability("testobject_api_key", getTestObjectKey());
if (deviceName == null) {
this.deviceName = "Google Pixel 2";
}
} else {
useSaucePlatform(appiumCapabilities);
}

appiumDefinedOptions.forEach((capability) -> {
addCapabilityIfDefined(appiumCapabilities, capability);
});

mobileSauceDefinedOptions.forEach((capability) -> {
addCapabilityIfDefined(appiumCapabilities, capability);
});

realDeviceSauceDefinedOptions.forEach((capability) -> {
addCapabilityIfDefined(appiumCapabilities, capability);
});
}

private void useSaucePlatform(MutableCapabilities capabilities) {
if (deviceName == null) {
this.deviceName = "Android GoogleAPI Emulator";
}
addAuthentication(capabilities);
}

private void addAppiumCapabilityIfDefined(MutableCapabilities capabilities, String capability) {
Object value = getCapability(capability);
if (value != null) {
capabilities.setCapability("appium:" + capability, value);
}
}

protected String getTestObjectKey() {
if (getSystemProperty("TEST_OBJECT_KEY") != null) {
return getSystemProperty("TEST_OBJECT_KEY");
} else if (getEnvironmentVariable("TEST_OBJECT_KEY") != null) {
return getEnvironmentVariable("TEST_OBJECT_KEY");
} else {
throw new SauceEnvironmentVariablesNotSetException("Test Object API Key was not provided");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.saucelabs.saucebindings;

import io.appium.java_client.AppiumDriver;
import lombok.Getter;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.InvalidArgumentException;
import org.openqa.selenium.MutableCapabilities;

import java.net.MalformedURLException;
import java.net.URL;

public class SauceMobileSession extends SauceSession {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how I feel about Dividing SauceSession and SauceMobileSession. Can we combine the logic here into SauceSession and have a single session concept?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need some way of differentiating Appium Driver from Selenium Driver type. I started to look into doing generics and interfaces, but I can't tell if that's actually better for users.

@Getter private final SauceMobileOptions sauceOptions;
@Getter private AppiumDriver driver;

public SauceMobileSession() {
this(new SauceMobileOptions());
}

public SauceMobileSession(SauceMobileOptions options) {
sauceOptions = options;
}

public AppiumDriver start() {
MutableCapabilities capabilities = sauceOptions.toCapabilities(getDataCenter());

URL url;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (dataCenter.supportsW3C() || dataCenter.isTestObject()) {
url = getSauceUrl();
} else {
String username = (String) capabilities.getCapability("username");
String key = (String) capabilities.getCapability("accessKey");
url = getSauceUrl(username, key);
}

//driver = createRemoteWebDriver(url, capabilities);
return driver;
}

public AppiumDriver createAppiumDriver(URL url, Capabilities caps) {
return new AppiumDriver<>(url, caps);
}

public URL getSauceUrl(String username, String key) {
String url = dataCenter.getValue().replace("://", "://" + username + ":" + key + "@");
try {
return new URL(url);
} catch (MalformedURLException e) {
throw new InvalidArgumentException("Invalid URL");
}
}
}