From a054753f846e5fa05042134a38bf3ba1077f0fa4 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 13 Dec 2024 22:27:49 +0100 Subject: [PATCH 1/8] feat: Implement HasBiDi interface support in AppiumDriver --- .../io/appium/java_client/AppiumDriver.java | 201 +++++++++++------- .../remote/AppiumCommandExecutor.java | 5 +- 2 files changed, 122 insertions(+), 84 deletions(-) diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index a8757843c..717cbd53e 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -30,6 +30,8 @@ import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.UnsupportedCommandException; import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.bidi.BiDi; +import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.remote.CapabilityType; import org.openqa.selenium.remote.DriverCommand; import org.openqa.selenium.remote.ErrorHandler; @@ -42,6 +44,8 @@ import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.remote.http.HttpMethod; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; import java.util.Collections; @@ -66,7 +70,8 @@ public class AppiumDriver extends RemoteWebDriver implements LogsEvents, HasBrowserCheck, CanRememberExtensionPresence, - HasSettings { + HasSettings, + HasBiDi { private static final ErrorHandler ERROR_HANDLER = new ErrorHandler(new ErrorCodesMobile(), true); // frequently used command parameters @@ -76,6 +81,8 @@ public class AppiumDriver extends RemoteWebDriver implements protected final RemoteLocationContext locationContext; private final ExecuteMethod executeMethod; private final Set absentExtensionNames = new HashSet<>(); + protected URI biDiUri; + protected BiDi biDi; /** * Creates a new instance based on command {@code executor} and {@code capabilities}. @@ -171,54 +178,6 @@ public AppiumDriver(URL remoteSessionAddress, String platformName, String automa setSessionId(sessionAddress.getId()); } - /** - * Changes platform name if it is not set and returns merged capabilities. - * - * @param originalCapabilities the given {@link Capabilities}. - * @param defaultName a platformName value which has to be set up - * @return {@link Capabilities} with changed platform name value or the original capabilities - */ - protected static Capabilities ensurePlatformName( - Capabilities originalCapabilities, String defaultName) { - return originalCapabilities.getPlatformName() == null - ? originalCapabilities.merge(new ImmutableCapabilities(PLATFORM_NAME, defaultName)) - : originalCapabilities; - } - - /** - * Changes automation name if it is not set and returns merged capabilities. - * - * @param originalCapabilities the given {@link Capabilities}. - * @param defaultName a platformName value which has to be set up - * @return {@link Capabilities} with changed mobile automation name value or the original capabilities - */ - protected static Capabilities ensureAutomationName( - Capabilities originalCapabilities, String defaultName) { - String currentAutomationName = CapabilityHelpers.getCapability( - originalCapabilities, AUTOMATION_NAME_OPTION, String.class); - if (isNullOrEmpty(currentAutomationName)) { - String capabilityName = originalCapabilities.getCapabilityNames() - .contains(AUTOMATION_NAME_OPTION) ? AUTOMATION_NAME_OPTION : APPIUM_PREFIX + AUTOMATION_NAME_OPTION; - return originalCapabilities.merge(new ImmutableCapabilities(capabilityName, defaultName)); - } - return originalCapabilities; - } - - /** - * Changes platform and automation names if they are not set - * and returns merged capabilities. - * - * @param originalCapabilities the given {@link Capabilities}. - * @param defaultPlatformName a platformName value which has to be set up - * @param defaultAutomationName The default automation name to set up for this class - * @return {@link Capabilities} with changed platform/automation name value or the original capabilities - */ - protected static Capabilities ensurePlatformAndAutomationNames( - Capabilities originalCapabilities, String defaultPlatformName, String defaultAutomationName) { - Capabilities capsWithPlatformFixed = ensurePlatformName(originalCapabilities, defaultPlatformName); - return ensureAutomationName(capsWithPlatformFixed, defaultAutomationName); - } - @Override public ExecuteMethod getExecuteMethod() { return executeMethod; @@ -260,39 +219,6 @@ public void addCommand(HttpMethod httpMethod, String url, String methodName) { ((AppiumCommandExecutor) getCommandExecutor()).refreshAdditionalCommands(); } - @Override - protected void startSession(Capabilities capabilities) { - var response = Optional.ofNullable( - execute(DriverCommand.NEW_SESSION(singleton(capabilities))) - ).orElseThrow(() -> new SessionNotCreatedException( - "The underlying command executor returned a null response." - )); - - var rawCapabilities = Optional.ofNullable(response.getValue()) - .map(value -> { - if (!(value instanceof Map)) { - throw new SessionNotCreatedException(String.format( - "The underlying command executor returned a response " - + "with a non well formed payload: %s", response) - ); - } - //noinspection unchecked - return (Map) value; - }) - .orElseThrow(() -> new SessionNotCreatedException( - "The underlying command executor returned a response without payload: " + response) - ); - - // TODO: remove this workaround for Selenium API enforcing some legacy capability values in major version - rawCapabilities.remove("platform"); - if (rawCapabilities.containsKey(CapabilityType.BROWSER_NAME) - && isNullOrEmpty((String) rawCapabilities.get(CapabilityType.BROWSER_NAME))) { - rawCapabilities.remove(CapabilityType.BROWSER_NAME); - } - this.capabilities = new BaseOptions<>(rawCapabilities); - setSessionId(response.getSessionId()); - } - @Override public Response execute(String driverCommand, Map parameters) { return super.execute(driverCommand, parameters); @@ -337,7 +263,118 @@ public AppiumDriver markExtensionAbsence(String extName) { return this; } + @Override + public Optional maybeGetBiDi() { + return Optional.ofNullable(this.biDi); + } + protected HttpClient getHttpClient() { return ((HttpCommandExecutor) getCommandExecutor()).client; } + + @Override + protected void startSession(Capabilities capabilities) { + var response = Optional.ofNullable( + execute(DriverCommand.NEW_SESSION(singleton(capabilities))) + ).orElseThrow(() -> new SessionNotCreatedException( + "The underlying command executor returned a null response." + )); + + var rawCapabilities = Optional.ofNullable(response.getValue()) + .map(value -> { + if (!(value instanceof Map)) { + throw new SessionNotCreatedException(String.format( + "The underlying command executor returned a response " + + "with a non well formed payload: %s", response) + ); + } + //noinspection unchecked + return (Map) value; + }) + .orElseThrow(() -> new SessionNotCreatedException( + "The underlying command executor returned a response without payload: " + response) + ); + + // TODO: remove this workaround for Selenium API enforcing some legacy capability values in major version + rawCapabilities.remove("platform"); + if (rawCapabilities.containsKey(CapabilityType.BROWSER_NAME) + && isNullOrEmpty((String) rawCapabilities.get(CapabilityType.BROWSER_NAME))) { + rawCapabilities.remove(CapabilityType.BROWSER_NAME); + } + this.capabilities = new BaseOptions<>(rawCapabilities); + this.initBiDi(capabilities); + setSessionId(response.getSessionId()); + } + + /** + * Changes platform name if it is not set and returns merged capabilities. + * + * @param originalCapabilities the given {@link Capabilities}. + * @param defaultName a platformName value which has to be set up + * @return {@link Capabilities} with changed platform name value or the original capabilities + */ + protected static Capabilities ensurePlatformName( + Capabilities originalCapabilities, String defaultName) { + return originalCapabilities.getPlatformName() == null + ? originalCapabilities.merge(new ImmutableCapabilities(PLATFORM_NAME, defaultName)) + : originalCapabilities; + } + + /** + * Changes automation name if it is not set and returns merged capabilities. + * + * @param originalCapabilities the given {@link Capabilities}. + * @param defaultName a platformName value which has to be set up + * @return {@link Capabilities} with changed mobile automation name value or the original capabilities + */ + protected static Capabilities ensureAutomationName( + Capabilities originalCapabilities, String defaultName) { + String currentAutomationName = CapabilityHelpers.getCapability( + originalCapabilities, AUTOMATION_NAME_OPTION, String.class); + if (isNullOrEmpty(currentAutomationName)) { + String capabilityName = originalCapabilities.getCapabilityNames() + .contains(AUTOMATION_NAME_OPTION) ? AUTOMATION_NAME_OPTION : APPIUM_PREFIX + AUTOMATION_NAME_OPTION; + return originalCapabilities.merge(new ImmutableCapabilities(capabilityName, defaultName)); + } + return originalCapabilities; + } + + /** + * Changes platform and automation names if they are not set + * and returns merged capabilities. + * + * @param originalCapabilities the given {@link Capabilities}. + * @param defaultPlatformName a platformName value which has to be set up + * @param defaultAutomationName The default automation name to set up for this class + * @return {@link Capabilities} with changed platform/automation name value or the original capabilities + */ + protected static Capabilities ensurePlatformAndAutomationNames( + Capabilities originalCapabilities, String defaultPlatformName, String defaultAutomationName) { + Capabilities capsWithPlatformFixed = ensurePlatformName(originalCapabilities, defaultPlatformName); + return ensureAutomationName(capsWithPlatformFixed, defaultAutomationName); + } + + private void initBiDi(Capabilities responseCaps) { + var webSocketUrl = CapabilityHelpers.getCapability(responseCaps, "webSocketUrl", String.class); + if (webSocketUrl == null) { + return; + } + try { + this.biDiUri = new URI(webSocketUrl); + } catch (URISyntaxException e) { + // no valid url -> no BiDi + return; + } + var executor = getCommandExecutor(); + final HttpClient wsClient; + if (executor instanceof AppiumCommandExecutor) { + var wsConfig = ((AppiumCommandExecutor) executor).getAppiumClientConfig().baseUri(biDiUri); + wsClient = ((AppiumCommandExecutor) executor).getHttpClientFactory().createClient(wsConfig); + } else { + var wsConfig = AppiumClientConfig.defaultConfig().baseUri(biDiUri); + wsClient = HttpClient.Factory.createDefault().createClient(wsConfig); + } + var biDiConnection = new org.openqa.selenium.bidi.Connection(wsClient, biDiUri.toString()); + this.biDi = new BiDi(biDiConnection); + } } 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 a689e7d33..4f9de33fe 100644 --- a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java +++ b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java @@ -19,6 +19,7 @@ import com.google.common.base.Throwables; import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.internal.ReflectionHelpers; +import lombok.Getter; import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.Command; @@ -54,9 +55,9 @@ public class AppiumCommandExecutor extends HttpCommandExecutor { private final Optional serviceOptional; - + @Getter private final HttpClient.Factory httpClientFactory; - + @Getter private final AppiumClientConfig appiumClientConfig; /** From 7b3ffc615a1a85f558228bd6ed7725d887fd24aa Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 13 Dec 2024 22:34:16 +0100 Subject: [PATCH 2/8] moar --- .../io/appium/java_client/remote/AppiumCommandExecutor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4f9de33fe..d664b2eab 100644 --- a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java +++ b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java @@ -134,7 +134,7 @@ protected void setPrivateFieldValue( ReflectionHelpers.setPrivateFieldValue(cls, this, fieldName, newValue); } - protected Map getAdditionalCommands() { + public Map getAdditionalCommands() { //noinspection unchecked return getPrivateFieldValue(HttpCommandExecutor.class, "additionalCommands", Map.class); } From e2c20eef3da2209e491f8d3a0fd57d882b319951 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 14 Dec 2024 21:30:23 +0100 Subject: [PATCH 3/8] add a test --- .../io/appium/java_client/ios/AppIOSTest.java | 1 + .../java_client/ios/BaseIOSWebViewTest.java | 1 - .../appium/java_client/ios/IOSBiDiTest.java | 41 ++++++++++ .../io/appium/java_client/AppiumDriver.java | 81 ++++++++++++++----- .../remote/options/BaseOptions.java | 3 +- .../options/SupportsWebSocketUrlOption.java | 54 +++++++++++++ 6 files changed, 160 insertions(+), 21 deletions(-) create mode 100644 src/e2eIosTest/java/io/appium/java_client/ios/IOSBiDiTest.java create mode 100644 src/main/java/io/appium/java_client/remote/options/SupportsWebSocketUrlOption.java diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/AppIOSTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/AppIOSTest.java index 25574d727..595114978 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/AppIOSTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/AppIOSTest.java @@ -21,6 +21,7 @@ public static void beforeClass() { .setDeviceName(DEVICE_NAME) .setCommandTimeouts(Duration.ofSeconds(240)) .setApp(TEST_APP_ZIP) + .enableBiDi() .setWdaLaunchTimeout(WDA_LAUNCH_TIMEOUT); try { driver = new IOSDriver(service.getUrl(), options); diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java index 752a0c539..2ffe6c79c 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java @@ -39,7 +39,6 @@ public static void beforeClass() { .setDeviceName(DEVICE_NAME) .setWdaLaunchTimeout(WDA_LAUNCH_TIMEOUT) .setCommandTimeouts(Duration.ofSeconds(240)) - .setShowIosLog(true) .setApp(VODQA_ZIP); Supplier createDriver = () -> new IOSDriver(service.getUrl(), options); try { diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSBiDiTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSBiDiTest.java new file mode 100644 index 000000000..c86374dba --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSBiDiTest.java @@ -0,0 +1,41 @@ +/* + * 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.ios; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.bidi.log.LogEntry; +import org.openqa.selenium.bidi.module.LogInspector; + +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class IOSBiDiTest extends AppIOSTest { + + @Test + @Disabled("Need to resolve compatibility issues") + public void listenForIosLogs() { + var logs = new CopyOnWriteArrayList(); + try (var logInspector = new LogInspector(driver)) { + logInspector.onLog(logs::add); + driver.getPageSource(); + } + assertFalse(logs.isEmpty()); + } + +} diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index 717cbd53e..c2cb482e5 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -21,6 +21,7 @@ import io.appium.java_client.remote.AppiumCommandExecutor; import io.appium.java_client.remote.AppiumW3CHttpCommandCodec; import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.SupportsWebSocketUrlOption; import io.appium.java_client.service.local.AppiumDriverLocalService; import io.appium.java_client.service.local.AppiumServiceBuilder; import lombok.Getter; @@ -31,6 +32,7 @@ import org.openqa.selenium.UnsupportedCommandException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.bidi.BiDi; +import org.openqa.selenium.bidi.BiDiException; import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.remote.CapabilityType; import org.openqa.selenium.remote.DriverCommand; @@ -44,6 +46,7 @@ import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.remote.http.HttpMethod; +import javax.annotation.Nonnull; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -152,8 +155,8 @@ public AppiumDriver(Capabilities capabilities) { * !!! This API is supposed to be used for **debugging purposes only**. * * @param remoteSessionAddress The address of the **running** session including the session identifier. - * @param platformName The name of the target platform. - * @param automationName The name of the target automation. + * @param platformName The name of the target platform. + * @param automationName The name of the target automation. */ public AppiumDriver(URL remoteSessionAddress, String platformName, String automationName) { super(); @@ -268,19 +271,46 @@ public Optional maybeGetBiDi() { return Optional.ofNullable(this.biDi); } + @Override + @Nonnull + public BiDi getBiDi() { + var webSocketUrl = ((BaseOptions) this.capabilities).getWebSocketUrl().orElseThrow( + () -> new BiDiException( + String.format( + "BiDi is not enabled for this driver session. " + + "Did you set %s to true?", SupportsWebSocketUrlOption.WEB_SOCKET_URL + ) + ) + ); + if (this.biDiUri == null) { + throw new BiDiException( + String.format( + "BiDi is not enabled for this driver session. " + + "Is the %s '%s' received from the create session response valid?", + SupportsWebSocketUrlOption.WEB_SOCKET_URL, webSocketUrl + ) + ); + } + if (this.biDi == null) { + // This should not happen + throw new IllegalStateException(); + } + return this.biDi; + } + protected HttpClient getHttpClient() { return ((HttpCommandExecutor) getCommandExecutor()).client; } @Override - protected void startSession(Capabilities capabilities) { + protected void startSession(Capabilities requestCapabilities) { var response = Optional.ofNullable( - execute(DriverCommand.NEW_SESSION(singleton(capabilities))) + execute(DriverCommand.NEW_SESSION(singleton(requestCapabilities))) ).orElseThrow(() -> new SessionNotCreatedException( "The underlying command executor returned a null response." )); - var rawCapabilities = Optional.ofNullable(response.getValue()) + var rawResponseCapabilities = Optional.ofNullable(response.getValue()) .map(value -> { if (!(value instanceof Map)) { throw new SessionNotCreatedException(String.format( @@ -296,13 +326,15 @@ protected void startSession(Capabilities capabilities) { ); // TODO: remove this workaround for Selenium API enforcing some legacy capability values in major version - rawCapabilities.remove("platform"); - if (rawCapabilities.containsKey(CapabilityType.BROWSER_NAME) - && isNullOrEmpty((String) rawCapabilities.get(CapabilityType.BROWSER_NAME))) { - rawCapabilities.remove(CapabilityType.BROWSER_NAME); + rawResponseCapabilities.remove("platform"); + if (rawResponseCapabilities.containsKey(CapabilityType.BROWSER_NAME) + && isNullOrEmpty((String) rawResponseCapabilities.get(CapabilityType.BROWSER_NAME))) { + rawResponseCapabilities.remove(CapabilityType.BROWSER_NAME); + } + this.capabilities = new BaseOptions<>(rawResponseCapabilities); + if (Boolean.TRUE.equals(requestCapabilities.getCapability(SupportsWebSocketUrlOption.WEB_SOCKET_URL))) { + this.initBiDi((BaseOptions) capabilities); } - this.capabilities = new BaseOptions<>(rawCapabilities); - this.initBiDi(capabilities); setSessionId(response.getSessionId()); } @@ -343,8 +375,8 @@ protected static Capabilities ensureAutomationName( * Changes platform and automation names if they are not set * and returns merged capabilities. * - * @param originalCapabilities the given {@link Capabilities}. - * @param defaultPlatformName a platformName value which has to be set up + * @param originalCapabilities the given {@link Capabilities}. + * @param defaultPlatformName a platformName value which has to be set up * @param defaultAutomationName The default automation name to set up for this class * @return {@link Capabilities} with changed platform/automation name value or the original capabilities */ @@ -354,16 +386,27 @@ protected static Capabilities ensurePlatformAndAutomationNames( return ensureAutomationName(capsWithPlatformFixed, defaultAutomationName); } - private void initBiDi(Capabilities responseCaps) { - var webSocketUrl = CapabilityHelpers.getCapability(responseCaps, "webSocketUrl", String.class); - if (webSocketUrl == null) { + private void initBiDi(BaseOptions responseCaps) { + var webSocketUrl = responseCaps.getWebSocketUrl(); + if (webSocketUrl.isEmpty()) { return; } + URISyntaxException uriSyntaxError = null; try { - this.biDiUri = new URI(webSocketUrl); + this.biDiUri = new URI(String.valueOf(webSocketUrl.get())); } catch (URISyntaxException e) { - // no valid url -> no BiDi - return; + uriSyntaxError = e; + } + if (uriSyntaxError != null || this.biDiUri.getScheme() == null) { + var message = String.format( + "BiDi cannot be enabled for this driver session. " + + "Is the %s '%s' received from the create session response valid?", + SupportsWebSocketUrlOption.WEB_SOCKET_URL, webSocketUrl.get() + ); + if (uriSyntaxError == null) { + throw new BiDiException(message); + } + throw new BiDiException(message, uriSyntaxError); } var executor = getCommandExecutor(); final HttpClient wsClient; diff --git a/src/main/java/io/appium/java_client/remote/options/BaseOptions.java b/src/main/java/io/appium/java_client/remote/options/BaseOptions.java index dff4f5c44..7e3ade21f 100644 --- a/src/main/java/io/appium/java_client/remote/options/BaseOptions.java +++ b/src/main/java/io/appium/java_client/remote/options/BaseOptions.java @@ -49,7 +49,8 @@ public class BaseOptions> extends MutableCapabilities i SupportsFullResetOption, SupportsNewCommandTimeoutOption, SupportsBrowserNameOption, - SupportsPlatformVersionOption { + SupportsPlatformVersionOption, + SupportsWebSocketUrlOption { /** * Creates new instance with no preset capabilities. diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsWebSocketUrlOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsWebSocketUrlOption.java new file mode 100644 index 000000000..1e14174cc --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsWebSocketUrlOption.java @@ -0,0 +1,54 @@ +/* + * 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.remote.options; + +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +public interface SupportsWebSocketUrlOption> extends + Capabilities, CanSetCapability { + String WEB_SOCKET_URL = "webSocketUrl"; + + /** + * Enable BiDi session support. + * + * @return self instance for chaining. + */ + default T enableBiDi() { + return amend(WEB_SOCKET_URL, true); + } + + /** + * Whether to enable BiDi session support. + * + * @return self instance for chaining. + */ + default T setWebSocketUrl(boolean value) { + return amend(WEB_SOCKET_URL, value); + } + + /** + * For input capabilities: whether enable BiDi session support is enabled. + * For session creation response capabilities: BiDi web socket URL. + * + * @return If called on request capabilities if BiDi support is enabled for the driver session + */ + default Optional getWebSocketUrl() { + return Optional.ofNullable(getCapability(WEB_SOCKET_URL)); + } +} From e4769536b4c55b9f80381cbf017ac14a55be1317 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 14 Dec 2024 21:40:40 +0100 Subject: [PATCH 4/8] Add constant --- .../io/appium/java_client/android/AndroidContextTest.java | 7 ++++--- .../io/appium/java_client/android/AndroidFunctionTest.java | 3 ++- .../java/io/appium/java_client/ios/IOSBiDiTest.java | 3 ++- .../java/io/appium/java_client/ios/IOSContextTest.java | 5 +++-- src/main/java/io/appium/java_client/HasBrowserCheck.java | 4 +++- .../pagefactory/utils/WebDriverUnpackUtility.java | 5 ++--- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java index fdc47664b..1a9a5657d 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static io.appium.java_client.HasBrowserCheck.NATIVE_CONTEXT; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -31,7 +32,7 @@ public class AndroidContextTest extends BaseAndroidTest { } @Test public void testGetContext() { - assertEquals("NATIVE_APP", driver.getContext()); + assertEquals(NATIVE_CONTEXT, driver.getContext()); } @Test public void testGetContextHandles() { @@ -42,8 +43,8 @@ public class AndroidContextTest extends BaseAndroidTest { driver.getContextHandles(); driver.context("WEBVIEW_io.appium.android.apis"); assertEquals(driver.getContext(), "WEBVIEW_io.appium.android.apis"); - driver.context("NATIVE_APP"); - assertEquals(driver.getContext(), "NATIVE_APP"); + driver.context(NATIVE_CONTEXT); + assertEquals(driver.getContext(), NATIVE_CONTEXT); } @Test public void testContextError() { diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java index 79d327ae1..0db6f2647 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java @@ -18,6 +18,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static io.appium.java_client.HasBrowserCheck.NATIVE_CONTEXT; import static java.time.Duration.ofMillis; import static java.time.Duration.ofSeconds; import static org.hamcrest.MatcherAssert.assertThat; @@ -75,7 +76,7 @@ public static void startWebViewActivity() { @BeforeEach public void setUp() { - driver.context("NATIVE_APP"); + driver.context(NATIVE_CONTEXT); } @Test diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSBiDiTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSBiDiTest.java index c86374dba..e25d3f515 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/IOSBiDiTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSBiDiTest.java @@ -23,6 +23,7 @@ import java.util.concurrent.CopyOnWriteArrayList; +import static io.appium.java_client.HasBrowserCheck.NATIVE_CONTEXT; import static org.junit.jupiter.api.Assertions.assertFalse; public class IOSBiDiTest extends AppIOSTest { @@ -31,7 +32,7 @@ public class IOSBiDiTest extends AppIOSTest { @Disabled("Need to resolve compatibility issues") public void listenForIosLogs() { var logs = new CopyOnWriteArrayList(); - try (var logInspector = new LogInspector(driver)) { + try (var logInspector = new LogInspector(NATIVE_CONTEXT, driver)) { logInspector.onLog(logs::add); driver.getPageSource(); } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSContextTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSContextTest.java index f2ac548b1..c7e0af42f 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/IOSContextTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSContextTest.java @@ -19,6 +19,7 @@ import io.appium.java_client.NoSuchContextException; import org.junit.jupiter.api.Test; +import static io.appium.java_client.HasBrowserCheck.NATIVE_CONTEXT; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.StringContains.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -27,7 +28,7 @@ public class IOSContextTest extends BaseIOSWebViewTest { @Test public void testGetContext() { - assertEquals("NATIVE_APP", driver.getContext()); + assertEquals(NATIVE_CONTEXT, driver.getContext()); } @Test public void testGetContextHandles() { @@ -38,7 +39,7 @@ public class IOSContextTest extends BaseIOSWebViewTest { driver.getContextHandles(); findAndSwitchToWebView(); assertThat(driver.getContext(), containsString("WEBVIEW")); - driver.context("NATIVE_APP"); + driver.context(NATIVE_CONTEXT); } @Test public void testContextError() { diff --git a/src/main/java/io/appium/java_client/HasBrowserCheck.java b/src/main/java/io/appium/java_client/HasBrowserCheck.java index 76094b5ca..ebce4a3c5 100644 --- a/src/main/java/io/appium/java_client/HasBrowserCheck.java +++ b/src/main/java/io/appium/java_client/HasBrowserCheck.java @@ -10,6 +10,8 @@ import static java.util.Objects.requireNonNull; public interface HasBrowserCheck extends ExecutesMethod, HasCapabilities { + String NATIVE_CONTEXT = "NATIVE_APP"; + /** * Validates if the driver is currently in a web browser context. * @@ -32,7 +34,7 @@ default boolean isBrowser() { } try { var context = ((ContextAware) this).getContext(); - return context != null && !context.toUpperCase().contains("NATIVE_APP"); + return context != null && !context.toUpperCase().contains(NATIVE_CONTEXT); } catch (WebDriverException e) { return false; } diff --git a/src/main/java/io/appium/java_client/pagefactory/utils/WebDriverUnpackUtility.java b/src/main/java/io/appium/java_client/pagefactory/utils/WebDriverUnpackUtility.java index 190f9c4ae..8b59d7ba6 100644 --- a/src/main/java/io/appium/java_client/pagefactory/utils/WebDriverUnpackUtility.java +++ b/src/main/java/io/appium/java_client/pagefactory/utils/WebDriverUnpackUtility.java @@ -28,12 +28,11 @@ import java.util.Optional; +import static io.appium.java_client.HasBrowserCheck.NATIVE_CONTEXT; import static io.appium.java_client.pagefactory.bys.ContentType.HTML_OR_DEFAULT; import static io.appium.java_client.pagefactory.bys.ContentType.NATIVE_MOBILE_SPECIFIC; public final class WebDriverUnpackUtility { - private static final String NATIVE_APP_PATTERN = "NATIVE_APP"; - private WebDriverUnpackUtility() { } @@ -109,7 +108,7 @@ public static ContentType getCurrentContentType(SearchContext context) { var contextAware = unpackObjectFromSearchContext(context, ContextAware.class); if (contextAware.map(ContextAware::getContext) - .filter(c -> c.toUpperCase().contains(NATIVE_APP_PATTERN)).isPresent()) { + .filter(c -> c.toUpperCase().contains(NATIVE_CONTEXT)).isPresent()) { return NATIVE_MOBILE_SPECIFIC; } From 2d10f1252c7990ec5fe58f7d3f41d51fe847708b Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 14 Dec 2024 22:34:08 +0100 Subject: [PATCH 5/8] fix checkstyle --- .../java/io/appium/java_client/AppiumDriver.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index c2cb482e5..5d6f90f61 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -277,16 +277,16 @@ public BiDi getBiDi() { var webSocketUrl = ((BaseOptions) this.capabilities).getWebSocketUrl().orElseThrow( () -> new BiDiException( String.format( - "BiDi is not enabled for this driver session. " + - "Did you set %s to true?", SupportsWebSocketUrlOption.WEB_SOCKET_URL + "BiDi is not enabled for this driver session. " + + "Did you set %s to true?", SupportsWebSocketUrlOption.WEB_SOCKET_URL ) ) ); if (this.biDiUri == null) { throw new BiDiException( String.format( - "BiDi is not enabled for this driver session. " + - "Is the %s '%s' received from the create session response valid?", + "BiDi is not enabled for this driver session. " + + "Is the %s '%s' received from the create session response valid?", SupportsWebSocketUrlOption.WEB_SOCKET_URL, webSocketUrl ) ); @@ -399,8 +399,8 @@ private void initBiDi(BaseOptions responseCaps) { } if (uriSyntaxError != null || this.biDiUri.getScheme() == null) { var message = String.format( - "BiDi cannot be enabled for this driver session. " + - "Is the %s '%s' received from the create session response valid?", + "BiDi cannot be enabled for this driver session. " + + "Is the %s '%s' received from the create session response valid?", SupportsWebSocketUrlOption.WEB_SOCKET_URL, webSocketUrl.get() ); if (uriSyntaxError == null) { From a7eae5e5c6b69a8baf9faaf777f4469329fe697b Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 15 Dec 2024 08:55:52 +0100 Subject: [PATCH 6/8] Tune error message --- .../io/appium/java_client/AppiumDriver.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index 5d6f90f61..ad44f481b 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -86,6 +86,7 @@ public class AppiumDriver extends RemoteWebDriver implements private final Set absentExtensionNames = new HashSet<>(); protected URI biDiUri; protected BiDi biDi; + private boolean wasBiDiRequested = false; /** * Creates a new instance based on command {@code executor} and {@code capabilities}. @@ -275,12 +276,14 @@ public Optional maybeGetBiDi() { @Nonnull public BiDi getBiDi() { var webSocketUrl = ((BaseOptions) this.capabilities).getWebSocketUrl().orElseThrow( - () -> new BiDiException( - String.format( - "BiDi is not enabled for this driver session. " - + "Did you set %s to true?", SupportsWebSocketUrlOption.WEB_SOCKET_URL - ) - ) + () -> { + var suffix = wasBiDiRequested + ? "Do both the server and the driver declare BiDi support?" + : String.format("Did you set %s to true?", SupportsWebSocketUrlOption.WEB_SOCKET_URL); + return new BiDiException(String.format( + "BiDi is not enabled for this driver session. %s", suffix + )); + } ); if (this.biDiUri == null) { throw new BiDiException( @@ -332,7 +335,10 @@ && isNullOrEmpty((String) rawResponseCapabilities.get(CapabilityType.BROWSER_NAM rawResponseCapabilities.remove(CapabilityType.BROWSER_NAME); } this.capabilities = new BaseOptions<>(rawResponseCapabilities); - if (Boolean.TRUE.equals(requestCapabilities.getCapability(SupportsWebSocketUrlOption.WEB_SOCKET_URL))) { + this.wasBiDiRequested = Boolean.TRUE.equals( + requestCapabilities.getCapability(SupportsWebSocketUrlOption.WEB_SOCKET_URL) + ); + if (wasBiDiRequested) { this.initBiDi((BaseOptions) capabilities); } setSessionId(response.getSessionId()); From 5b285d2e3a68e8a44957c95b359a26ed965f8c6d Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 15 Dec 2024 10:20:00 +0100 Subject: [PATCH 7/8] make private --- src/main/java/io/appium/java_client/AppiumDriver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index ad44f481b..f7eba8020 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -84,8 +84,8 @@ public class AppiumDriver extends RemoteWebDriver implements protected final RemoteLocationContext locationContext; private final ExecuteMethod executeMethod; private final Set absentExtensionNames = new HashSet<>(); - protected URI biDiUri; - protected BiDi biDi; + private URI biDiUri; + private BiDi biDi; private boolean wasBiDiRequested = false; /** From 6585b17836ecb74a0f2d9139a4d4eab96dc31a3b Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 15 Dec 2024 19:04:54 +0100 Subject: [PATCH 8/8] Assume --- .github/workflows/gradle.yml | 1 + .../java/io/appium/java_client/ios/IOSWebViewTest.java | 5 +++++ src/test/java/io/appium/java_client/TestUtils.java | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 252710957..cd70a8203 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -19,6 +19,7 @@ concurrency: cancel-in-progress: true env: + CI: true ANDROID_SDK_VERSION: "28" ANDROID_EMU_NAME: test ANDROID_EMU_TARGET: default diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java index 60943342e..da68eeecb 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java @@ -1,6 +1,8 @@ package io.appium.java_client.ios; import io.appium.java_client.AppiumBy; +import io.appium.java_client.TestUtils; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.openqa.selenium.By; import org.openqa.selenium.support.ui.ExpectedConditions; @@ -15,6 +17,9 @@ public class IOSWebViewTest extends BaseIOSWebViewTest { @Test public void webViewPageTestCase() throws InterruptedException { + // this test is not stable in the CI env + Assumptions.assumeFalse(TestUtils.isCiEnv()); + new WebDriverWait(driver, LOOKUP_TIMEOUT) .until(ExpectedConditions.presenceOfElementLocated(By.id("login"))) .click(); diff --git a/src/test/java/io/appium/java_client/TestUtils.java b/src/test/java/io/appium/java_client/TestUtils.java index 1d650777c..73d104469 100644 --- a/src/test/java/io/appium/java_client/TestUtils.java +++ b/src/test/java/io/appium/java_client/TestUtils.java @@ -78,4 +78,8 @@ public static Point getCenter(WebElement webElement, @Nullable Point location) { } return new Point(location.x + dim.width / 2, location.y + dim.height / 2); } + + public static boolean isCiEnv() { + return System.getenv("CI") != null; + } }