diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e5981b475..caf838f59 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -27,4 +27,4 @@ jobs: java-version: ${{ matrix.java }} cache: 'gradle' - name: Build with Gradle - run: ./gradlew clean build -x test -x checkstyleTest + run: ./gradlew clean build -x unitTest -x checkstyleTest diff --git a/build.gradle b/build.gradle index 084b502cb..b0116c43b 100644 --- a/build.gradle +++ b/build.gradle @@ -191,6 +191,15 @@ processResources { ] } +task unitTest( type: Test ) { + useJUnitPlatform() + testLogging.showStandardStreams = true + testLogging.exceptionFormat = 'full' + filter { + includeTestsMatching 'io.appium.java_client.internal.*' + } +} + task xcuiTest( type: Test ) { useJUnitPlatform() testLogging.showStandardStreams = true diff --git a/gradle.properties b/gradle.properties index b11361057..9b086f575 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,4 +7,4 @@ signing.secretKeyRingFile=PathToYourKeyRingFile ossrhUsername=your-jira-id ossrhPassword=your-jira-password -selenium.version=4.4.0 +selenium.version=4.5.0 diff --git a/src/main/java/io/appium/java_client/AppiumClientConfig.java b/src/main/java/io/appium/java_client/AppiumClientConfig.java new file mode 100644 index 000000000..2e6128c03 --- /dev/null +++ b/src/main/java/io/appium/java_client/AppiumClientConfig.java @@ -0,0 +1,191 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client; + +import org.openqa.selenium.Credentials; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.http.AddSeleniumUserAgent; +import org.openqa.selenium.remote.http.ClientConfig; +import org.openqa.selenium.remote.http.Filter; + +import java.net.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.time.Duration; + +/** + * A class to store the appium http client configuration. + */ +public class AppiumClientConfig extends ClientConfig { + private final boolean directConnect; + + private static final Filter DEFAULT_FILTER = new AddSeleniumUserAgent(); + + private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMinutes(10); + + private static final Duration DEFAULT_CONNECTION_TIMEOUT = Duration.ofSeconds(10); + + /** + * Client side configuration. + * + * @param baseUri Base URL the client sends HTTP request to. + * @param connectionTimeout The client connection timeout. + * @param readTimeout The client read timeout. + * @param filters Filters to modify incoming {@link org.openqa.selenium.remote.http.HttpRequest} or outgoing + * {@link org.openqa.selenium.remote.http.HttpResponse}. + * @param proxy The client proxy preference. + * @param credentials Credentials used for authenticating http requests + * @param directConnect If directConnect is enabled. + */ + protected AppiumClientConfig( + URI baseUri, + Duration connectionTimeout, + Duration readTimeout, + Filter filters, + Proxy proxy, + Credentials credentials, + Boolean directConnect) { + super(baseUri, connectionTimeout, readTimeout, filters, proxy, credentials); + + this.directConnect = Require.nonNull("Direct Connect", directConnect); + } + + /** + * Return the instance of {@link AppiumClientConfig} with a default config. + * @return the instance of {@link AppiumClientConfig}. + */ + public static AppiumClientConfig defaultConfig() { + return new AppiumClientConfig( + null, + DEFAULT_CONNECTION_TIMEOUT, + DEFAULT_READ_TIMEOUT, + DEFAULT_FILTER, + null, + null, + false); + } + + /** + * Return the instance of {@link AppiumClientConfig} from the given {@link ClientConfig} parameters. + * @param clientConfig take a look at {@link ClientConfig} + * @return the instance of {@link AppiumClientConfig}. + */ + public static AppiumClientConfig fromClientConfig(ClientConfig clientConfig) { + return new AppiumClientConfig( + clientConfig.baseUri(), + clientConfig.connectionTimeout(), + clientConfig.readTimeout(), + clientConfig.filter(), + clientConfig.proxy(), + clientConfig.credentials(), + false); + } + + private AppiumClientConfig buildAppiumClientConfig(ClientConfig clientConfig, Boolean directConnect) { + return new AppiumClientConfig( + clientConfig.baseUri(), + clientConfig.connectionTimeout(), + clientConfig.readTimeout(), + clientConfig.filter(), + clientConfig.proxy(), + clientConfig.credentials(), + directConnect); + } + + @Override + public AppiumClientConfig baseUri(URI baseUri) { + ClientConfig clientConfig = super.baseUri(baseUri); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + @Override + public AppiumClientConfig baseUrl(URL baseUrl) { + try { + return baseUri(Require.nonNull("Base URL", baseUrl).toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + @Override + public AppiumClientConfig connectionTimeout(Duration timeout) { + ClientConfig clientConfig = super.connectionTimeout(timeout); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + @Override + public AppiumClientConfig readTimeout(Duration timeout) { + ClientConfig clientConfig = super.connectionTimeout(timeout); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + @Override + public AppiumClientConfig withFilter(Filter filter) { + ClientConfig clientConfig = super.withFilter(filter); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + @Override + public AppiumClientConfig withRetries() { + ClientConfig clientConfig = super.withRetries(); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + + @Override + public ClientConfig proxy(Proxy proxy) { + ClientConfig clientConfig = super.proxy(proxy); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + @Override + public AppiumClientConfig authenticateAs(Credentials credentials) { + ClientConfig clientConfig = super.authenticateAs(credentials); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + /** + * Whether enable directConnect feature described in + * + * Connecting Directly to Appium Hosts in Distributed Environments. + * + * @param directConnect if enable the directConnect feature + * @return A new instance of AppiumClientConfig + */ + public AppiumClientConfig directConnect(boolean directConnect) { + // follows ClientConfig's design + return new AppiumClientConfig( + this.baseUri(), + this.connectionTimeout(), + this.readTimeout(), + this.filter(), + this.proxy(), + this.credentials(), + directConnect + ); + } + + /** + * Whether enable directConnect feature is enabled. + * + * @return If the directConnect is enabled. Defaults false. + */ + public boolean isDirectConnectEnabled() { + return directConnect; + } +} diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index 0f109af2c..d814657c6 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -41,7 +41,6 @@ import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.remote.Response; import org.openqa.selenium.remote.html5.RemoteLocationContext; -import org.openqa.selenium.remote.http.ClientConfig; import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.remote.http.HttpMethod; @@ -84,7 +83,7 @@ public AppiumDriver(HttpCommandExecutor executor, Capabilities capabilities) { this.remoteAddress = executor.getAddressOfRemoteServer(); } - public AppiumDriver(ClientConfig clientConfig, Capabilities capabilities) { + public AppiumDriver(AppiumClientConfig clientConfig, Capabilities capabilities) { this(new AppiumCommandExecutor(MobileCommand.commandRepository, clientConfig), capabilities); } diff --git a/src/main/java/io/appium/java_client/android/AndroidDriver.java b/src/main/java/io/appium/java_client/android/AndroidDriver.java index 76810cac3..92ed19370 100644 --- a/src/main/java/io/appium/java_client/android/AndroidDriver.java +++ b/src/main/java/io/appium/java_client/android/AndroidDriver.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap; +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecuteCDPCommand; @@ -203,7 +204,32 @@ public AndroidDriver(HttpClient.Factory httpClientFactory, Capabilities capabili * */ public AndroidDriver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensurePlatformName(capabilities, ANDROID_PLATFORM)); + super(AppiumClientConfig.fromClientConfig(clientConfig), ensurePlatformName(capabilities, + ANDROID_PLATFORM)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * UiAutomator2Options options = new UiAutomator2Options();
+     * AndroidDriver driver = new AndroidDriver(appiumClientConfig, options);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public AndroidDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** diff --git a/src/main/java/io/appium/java_client/gecko/GeckoDriver.java b/src/main/java/io/appium/java_client/gecko/GeckoDriver.java index 43d2072c8..6a7a55cab 100644 --- a/src/main/java/io/appium/java_client/gecko/GeckoDriver.java +++ b/src/main/java/io/appium/java_client/gecko/GeckoDriver.java @@ -16,6 +16,7 @@ package io.appium.java_client.gecko; +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.remote.AutomationName; import io.appium.java_client.service.local.AppiumDriverLocalService; @@ -94,7 +95,31 @@ public GeckoDriver(HttpClient.Factory httpClientFactory, Capabilities capabiliti * */ public GeckoDriver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensureAutomationName(capabilities, AUTOMATION_NAME)); + super(AppiumClientConfig.fromClientConfig(clientConfig), ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * GeckoOptions options = new GeckoOptions();
+     * GeckoDriver driver = new GeckoDriver(options, appiumClientConfig);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public GeckoDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensureAutomationName(capabilities, AUTOMATION_NAME)); } public GeckoDriver(Capabilities capabilities) { diff --git a/src/main/java/io/appium/java_client/ios/IOSDriver.java b/src/main/java/io/appium/java_client/ios/IOSDriver.java index 18ecf3065..00098b14c 100644 --- a/src/main/java/io/appium/java_client/ios/IOSDriver.java +++ b/src/main/java/io/appium/java_client/ios/IOSDriver.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableMap; +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.HasAppStrings; import io.appium.java_client.HasDeviceTime; @@ -192,9 +193,35 @@ public IOSDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities * */ public IOSDriver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensurePlatformName(capabilities, PLATFORM_NAME)); + super(AppiumClientConfig.fromClientConfig(clientConfig), + ensurePlatformName(capabilities, PLATFORM_NAME)); } + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * XCUITestOptions options = new XCUITestOptions();
+     * IOSDriver driver = new IOSDriver(options, appiumClientConfig);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public IOSDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformName(capabilities, PLATFORM_NAME)); + } + + /** * Creates a new instance based on {@code capabilities}. * diff --git a/src/main/java/io/appium/java_client/mac/Mac2Driver.java b/src/main/java/io/appium/java_client/mac/Mac2Driver.java index b76de5943..905cbec77 100644 --- a/src/main/java/io/appium/java_client/mac/Mac2Driver.java +++ b/src/main/java/io/appium/java_client/mac/Mac2Driver.java @@ -16,6 +16,7 @@ package io.appium.java_client.mac; +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.PerformsTouchActions; import io.appium.java_client.remote.AutomationName; @@ -105,7 +106,32 @@ public Mac2Driver(HttpClient.Factory httpClientFactory, Capabilities capabilitie * */ public Mac2Driver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensurePlatformAndAutomationNames( + super(AppiumClientConfig.fromClientConfig(clientConfig), ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * Mac2Options options = new Mac2Options();
+     * Mac2Driver driver = new Mac2Driver(appiumClientConfig, options);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public Mac2Driver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformAndAutomationNames( capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } diff --git a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java index 01c551fb4..ec792852a 100644 --- a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java +++ b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java @@ -24,9 +24,9 @@ import com.google.common.base.Supplier; import com.google.common.base.Throwables; +import io.appium.java_client.AppiumClientConfig; import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.internal.Require; import org.openqa.selenium.remote.Command; import org.openqa.selenium.remote.CommandCodec; import org.openqa.selenium.remote.CommandExecutor; @@ -38,17 +38,18 @@ import org.openqa.selenium.remote.Response; import org.openqa.selenium.remote.ResponseCodec; import org.openqa.selenium.remote.codec.w3c.W3CHttpCommandCodec; -import org.openqa.selenium.remote.http.ClientConfig; import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.remote.http.HttpRequest; import org.openqa.selenium.remote.http.HttpResponse; import org.openqa.selenium.remote.service.DriverService; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import java.lang.reflect.Field; import java.net.ConnectException; +import java.net.MalformedURLException; import java.net.URL; -import java.time.Duration; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -56,49 +57,71 @@ public class AppiumCommandExecutor extends HttpCommandExecutor { // https://github.com/appium/appium-base-driver/pull/400 private static final String IDEMPOTENCY_KEY_HEADER = "X-Idempotency-Key"; - private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMinutes(10); private final Optional serviceOptional; - private AppiumCommandExecutor(Map additionalCommands, DriverService service, - URL addressOfRemoteServer, - HttpClient.Factory httpClientFactory, - ClientConfig clientConfig) { + private final HttpClient.Factory httpClientFactory; + + private final AppiumClientConfig appiumClientConfig; + + /** + * Create an AppiumCommandExecutor instance. + * + * @param additionalCommands is the map of Appium commands + * @param service take a look at {@link DriverService} + * @param httpClientFactory take a look at {@link HttpClient.Factory} + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + */ + public AppiumCommandExecutor( + @Nonnull Map additionalCommands, + @Nullable DriverService service, + @Nullable HttpClient.Factory httpClientFactory, + @Nonnull AppiumClientConfig appiumClientConfig) { super(additionalCommands, - ofNullable(clientConfig).orElse( - ClientConfig.defaultConfig() - .baseUrl(Require.nonNull("Server URL", ofNullable(service) - .map(DriverService::getUrl) - .orElse(addressOfRemoteServer))) - .readTimeout(DEFAULT_READ_TIMEOUT) - ), - ofNullable(httpClientFactory).orElseGet(HttpCommandExecutor::getDefaultClientFactory) + appiumClientConfig, + ofNullable(httpClientFactory).orElseGet(AppiumCommandExecutor::getDefaultClientFactory) ); serviceOptional = ofNullable(service); + + this.httpClientFactory = httpClientFactory; + this.appiumClientConfig = appiumClientConfig; } public AppiumCommandExecutor(Map additionalCommands, DriverService service, HttpClient.Factory httpClientFactory) { - this(additionalCommands, checkNotNull(service), null, httpClientFactory, null); + this(additionalCommands, checkNotNull(service), httpClientFactory, + AppiumClientConfig.defaultConfig().baseUrl(checkNotNull(service).getUrl())); } - public AppiumCommandExecutor(Map additionalCommands, - URL addressOfRemoteServer, HttpClient.Factory httpClientFactory) { - this(additionalCommands, null, checkNotNull(addressOfRemoteServer), httpClientFactory, null); + public AppiumCommandExecutor(Map additionalCommands, URL addressOfRemoteServer, + HttpClient.Factory httpClientFactory) { + this(additionalCommands, null, httpClientFactory, + AppiumClientConfig.defaultConfig().baseUrl(checkNotNull(addressOfRemoteServer))); } - public AppiumCommandExecutor(Map additionalCommands, ClientConfig clientConfig) { - this(additionalCommands, null, checkNotNull(clientConfig.baseUrl()), null, clientConfig); + public AppiumCommandExecutor(Map additionalCommands, AppiumClientConfig appiumClientConfig) { + this(additionalCommands, null, null, appiumClientConfig); } - public AppiumCommandExecutor(Map additionalCommands, - URL addressOfRemoteServer) { - this(additionalCommands, addressOfRemoteServer, HttpClient.Factory.createDefault()); + public AppiumCommandExecutor(Map additionalCommands, URL addressOfRemoteServer) { + this(additionalCommands, null, HttpClient.Factory.createDefault(), + AppiumClientConfig.defaultConfig().baseUrl(checkNotNull(addressOfRemoteServer))); + } + + public AppiumCommandExecutor(Map additionalCommands, URL addressOfRemoteServer, + AppiumClientConfig appiumClientConfig) { + this(additionalCommands, null, HttpClient.Factory.createDefault(), + appiumClientConfig.baseUrl(checkNotNull(addressOfRemoteServer))); + } + + public AppiumCommandExecutor(Map additionalCommands, DriverService service) { + this(additionalCommands, service, HttpClient.Factory.createDefault(), + AppiumClientConfig.defaultConfig().baseUrl(service.getUrl())); } public AppiumCommandExecutor(Map additionalCommands, - DriverService service) { - this(additionalCommands, service, HttpClient.Factory.createDefault()); + DriverService service, AppiumClientConfig appiumClientConfig) { + this(additionalCommands, service, HttpClient.Factory.createDefault(), appiumClientConfig); } @SuppressWarnings("SameParameterValue") @@ -147,6 +170,21 @@ protected HttpClient getClient() { return getPrivateFieldValue(HttpCommandExecutor.class, "client", HttpClient.class); } + /** + * Override the http client in the HttpCommandExecutor class with a new http client instance with the given URL. + * It uses the same http client factory and client config for the new http client instance + * if the constructor got them. + * @param serverUrl A url to override. + */ + protected void overrideServerUrl(URL serverUrl) { + if (this.appiumClientConfig == null) { + return; + } + setPrivateFieldValue(HttpCommandExecutor.class, "client", + ofNullable(this.httpClientFactory).orElseGet(AppiumCommandExecutor::getDefaultClientFactory) + .createClient(this.appiumClientConfig.baseUrl(serverUrl))); + } + private Response createSession(Command command) throws IOException { if (getCommandCodec() != null) { throw new SessionNotCreatedException("Session already exists"); @@ -166,13 +204,45 @@ private Response createSession(Command command) throws IOException { setCommandCodec(new AppiumW3CHttpCommandCodec()); refreshAdditionalCommands(); setResponseCodec(dialect.getResponseCodec()); - return result.createResponse(); + Response response = result.createResponse(); + if (this.appiumClientConfig != null && this.appiumClientConfig.isDirectConnectEnabled()) { + setDirectConnect(response); + } + + return response; } public void refreshAdditionalCommands() { getAdditionalCommands().forEach(this::defineCommand); } + @SuppressWarnings("unchecked") + private void setDirectConnect(Response response) throws SessionNotCreatedException { + Map responseValue = (Map) response.getValue(); + + DirectConnect directConnect = new DirectConnect(responseValue); + + if (!directConnect.isValid()) { + return; + } + + if (!directConnect.getProtocol().equals("https")) { + throw new SessionNotCreatedException( + String.format("The given protocol '%s' as the direct connection url returned by " + + "the remote server is not accurate. Only 'https' is supported.", + directConnect.getProtocol())); + } + + URL newUrl; + try { + newUrl = directConnect.getUrl(); + } catch (MalformedURLException e) { + throw new SessionNotCreatedException(e.getMessage()); + } + + overrideServerUrl(newUrl); + } + @Override public Response execute(Command command) throws WebDriverException { if (DriverCommand.NEW_SESSION.equals(command.getName())) { diff --git a/src/main/java/io/appium/java_client/remote/DirectConnect.java b/src/main/java/io/appium/java_client/remote/DirectConnect.java new file mode 100644 index 000000000..a2049c589 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/DirectConnect.java @@ -0,0 +1,77 @@ +package io.appium.java_client.remote; + +import javax.annotation.Nullable; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +import static io.appium.java_client.internal.CapabilityHelpers.APPIUM_PREFIX; + +// TODO: simplify with lombok as another PR +public class DirectConnect { + private static final String DIRECT_CONNECT_PROTOCOL = "directConnectProtocol"; + private static final String DIRECT_CONNECT_PATH = "directConnectPath"; + private static final String DIRECT_CONNECT_HOST = "directConnectHost"; + private static final String DIRECT_CONNECT_PORT = "directConnectPort"; + + private final String protocol; + + private final String path; + + private final String host; + + private final String port; + + /** + * Create a DirectConnect instance. + * @param responseValue is the response body + */ + public DirectConnect(Map responseValue) { + this.protocol = this.getDirectConnectValue(responseValue, DIRECT_CONNECT_PROTOCOL); + this.path = this.getDirectConnectValue(responseValue, DIRECT_CONNECT_PATH); + this.host = this.getDirectConnectValue(responseValue, DIRECT_CONNECT_HOST); + this.port = this.getDirectConnectValue(responseValue, DIRECT_CONNECT_PORT); + } + + public String getProtocol() { + return protocol; + } + + @Nullable + private String getDirectConnectValue(Map responseValue, String key) { + Object directConnectPath = responseValue.get(APPIUM_PREFIX + key); + if (directConnectPath != null) { + return String.valueOf(directConnectPath); + } + directConnectPath = responseValue.get(key); + return directConnectPath == null ? null : String.valueOf(directConnectPath); + } + + /** + * Returns true if the {@link DirectConnect} instance member has nonnull values. + * @return true if each connection information has a nonnull value + */ + public boolean isValid() { + return Stream.of(this.protocol, this.path, this.host, this.port).noneMatch(Objects::isNull); + } + + /** + * Returns a URL instance built with members in the DirectConnect instance. + * @return A URL object + * @throws MalformedURLException if the built url was invalid + */ + public URL getUrl() throws MalformedURLException { + String newUrlCandidate = String.format("%s://%s:%s%s", this.protocol, this.host, this.port, this.path); + + try { + return new URL(newUrlCandidate); + } catch (MalformedURLException e) { + throw new MalformedURLException( + String.format("The remote server returned an invalid value to build the direct connect URL: %s", + newUrlCandidate) + ); + } + } +} diff --git a/src/main/java/io/appium/java_client/safari/SafariDriver.java b/src/main/java/io/appium/java_client/safari/SafariDriver.java index 0b8a8400d..97d1f96e4 100644 --- a/src/main/java/io/appium/java_client/safari/SafariDriver.java +++ b/src/main/java/io/appium/java_client/safari/SafariDriver.java @@ -16,6 +16,7 @@ package io.appium.java_client.safari; +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.remote.AutomationName; import io.appium.java_client.service.local.AppiumDriverLocalService; @@ -102,7 +103,32 @@ public SafariDriver(HttpClient.Factory httpClientFactory, Capabilities capabilit * */ public SafariDriver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensurePlatformAndAutomationNames( + super(AppiumClientConfig.fromClientConfig(clientConfig), ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * SafariOptions options = new SafariOptions();
+     * SafariDriver driver = new SafariDriver(appiumClientConfig, options);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public SafariDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformAndAutomationNames( capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } diff --git a/src/main/java/io/appium/java_client/windows/WindowsDriver.java b/src/main/java/io/appium/java_client/windows/WindowsDriver.java index 82af6e02d..9a441d68a 100644 --- a/src/main/java/io/appium/java_client/windows/WindowsDriver.java +++ b/src/main/java/io/appium/java_client/windows/WindowsDriver.java @@ -16,6 +16,7 @@ package io.appium.java_client.windows; +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.MobileCommand; import io.appium.java_client.PerformsTouchActions; @@ -45,6 +46,7 @@ public WindowsDriver(HttpCommandExecutor executor, Capabilities capabilities) { super(executor, ensurePlatformAndAutomationNames(capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } + public WindowsDriver(URL remoteAddress, Capabilities capabilities) { super(remoteAddress, ensurePlatformAndAutomationNames( capabilities, PLATFORM_NAME, AUTOMATION_NAME)); @@ -100,7 +102,32 @@ public WindowsDriver(HttpClient.Factory httpClientFactory, Capabilities capabili * */ public WindowsDriver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensurePlatformAndAutomationNames( + super(AppiumClientConfig.fromClientConfig(clientConfig), ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * WindowsOptions options = new WindowsOptions();
+     * WindowsDriver driver = new WindowsDriver(appiumClientConfig, options);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public WindowsDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformAndAutomationNames( capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } diff --git a/src/test/java/io/appium/java_client/events/stubs/EmptyWebDriver.java b/src/test/java/io/appium/java_client/events/stubs/EmptyWebDriver.java index 5316f56e4..01104b988 100644 --- a/src/test/java/io/appium/java_client/events/stubs/EmptyWebDriver.java +++ b/src/test/java/io/appium/java_client/events/stubs/EmptyWebDriver.java @@ -202,10 +202,6 @@ public Timeouts timeouts() { return null; } - public ImeHandler ime() { - return null; - } - public Window window() { return new StubWindow(); } diff --git a/src/test/java/io/appium/java_client/internal/ConfigTest.java b/src/test/java/io/appium/java_client/internal/ConfigTest.java index df8611458..05559de27 100644 --- a/src/test/java/io/appium/java_client/internal/ConfigTest.java +++ b/src/test/java/io/appium/java_client/internal/ConfigTest.java @@ -9,13 +9,14 @@ import org.junit.jupiter.api.Test; class ConfigTest { - private static final String EXISTING_KEY = "selenium.version"; + private static final String SELENIUM_EXISTING_KEY = "selenium.version"; + private static final String MISSING_KEY = "bla"; @Test void verifyGettingExistingValue() { - assertThat(Config.main().getValue(EXISTING_KEY, String.class).length(), greaterThan(0)); - assertTrue(Config.main().getOptionalValue(EXISTING_KEY, String.class).isPresent()); + assertThat(Config.main().getValue(SELENIUM_EXISTING_KEY, String.class).length(), greaterThan(0)); + assertTrue(Config.main().getOptionalValue(SELENIUM_EXISTING_KEY, String.class).isPresent()); } @Test @@ -25,7 +26,7 @@ void verifyGettingNonExistingValue() { @Test void verifyGettingExistingValueWithWrongClass() { - assertThrows(ClassCastException.class, () -> Config.main().getValue(EXISTING_KEY, Integer.class)); + assertThrows(ClassCastException.class, () -> Config.main().getValue(SELENIUM_EXISTING_KEY, Integer.class)); } @Test diff --git a/src/test/java/io/appium/java_client/internal/DirectConnectTest.java b/src/test/java/io/appium/java_client/internal/DirectConnectTest.java new file mode 100644 index 000000000..93b345474 --- /dev/null +++ b/src/test/java/io/appium/java_client/internal/DirectConnectTest.java @@ -0,0 +1,56 @@ +package io.appium.java_client.internal; + +import io.appium.java_client.remote.DirectConnect; +import org.junit.jupiter.api.Test; + +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class DirectConnectTest { + + @Test + void hasValidDirectConnectValuesWithoutAppiumPrefix() throws MalformedURLException { + Map responseValue = new HashMap<>(); + responseValue.put("directConnectProtocol", "https"); + responseValue.put("directConnectPath", "/path/to"); + responseValue.put("directConnectHost", "host"); + responseValue.put("directConnectPort", "8080"); + DirectConnect directConnect = new DirectConnect(responseValue); + assertTrue(directConnect.isValid()); + assertEquals(directConnect.getUrl().toString(), "https://host:8080/path/to"); + } + + @Test + void hasValidDirectConnectValuesWithAppiumPrefix() throws MalformedURLException { + Map responseValue = new HashMap<>(); + responseValue.put("appium:directConnectProtocol", "https"); + responseValue.put("appium:directConnectPath", "/path/to"); + responseValue.put("appium:directConnectHost", "host"); + responseValue.put("appium:directConnectPort", "8080"); + DirectConnect directConnect = new DirectConnect(responseValue); + assertTrue(directConnect.isValid()); + assertEquals(directConnect.getUrl().toString(), "https://host:8080/path/to"); + } + + @Test + void hasValidDirectConnectStringPort() { + Map responseValue = new HashMap<>(); + responseValue.put("appium:directConnectProtocol", "https"); + responseValue.put("appium:directConnectPath", "/path/to"); + responseValue.put("appium:directConnectHost", "host"); + responseValue.put("appium:directConnectPort", "port"); + DirectConnect directConnect = new DirectConnect(responseValue); + assertTrue(directConnect.isValid()); + assertThrowsExactly(MalformedURLException.class, directConnect::getUrl); + } + + @Test + void hasInvalidDirectConnect() { + Map responseValue = new HashMap<>(); + DirectConnect directConnect = new DirectConnect(responseValue); + assertFalse(directConnect.isValid()); + } +} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java index bea245a99..f71d1d8c4 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java @@ -173,11 +173,6 @@ public Timeouts pageLoadTimeout(long time, TimeUnit unit) { }; } - @Override - public ImeHandler ime() { - return null; - } - @Override public Window window() { return null;