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
+ * + * 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