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)); + } +}