From b21f20f8ba1ed8ab42d097b4fd416b69b0f73e0d Mon Sep 17 00:00:00 2001 From: Ramesh Babu Prudhvi Date: Tue, 11 Apr 2023 13:40:31 +0530 Subject: [PATCH] Add support for executing tests on multiple devices simultaneously using custom capabilities (#245) * cleanup caps * Added DevicePool * Fixed minor issue * Code cleanup * Added IOS APP support --- .../selcukes/commons/http/WebClient.java | 30 ++-- .../src/test/resources/selcukes.yaml | 19 ++- .../selcukes/core/driver/AppiumEngine.java | 7 +- .../selcukes/core/driver/AppiumManager.java | 60 ++++--- .../selcukes/core/driver/AppiumOptions.java | 32 ++-- .../selcukes/core/driver/BrowserOptions.java | 43 ++--- .../selcukes/core/driver/CloudOptions.java | 8 +- .../selcukes/core/driver/DesktopManager.java | 10 +- .../selcukes/core/driver/DevicePool.java | 153 ++++++++++++++++++ .../selcukes/core/driver/DriverFactory.java | 80 +++++++++ .../selcukes/core/driver/DriverManager.java | 145 ++++++++++++----- .../selcukes/core/driver/MobileOptions.java | 51 ------ .../selcukes/core/driver/RemoteManager.java | 3 +- .../github/selcukes/core/driver/RunMode.java | 6 +- .../selcukes/core/driver/WebManager.java | 34 ++-- .../selcukes/core/enums/AppiumDriverType.java | 26 --- .../io/github/selcukes/core/page/Pages.java | 13 +- .../io/github/selcukes/core/page/WinPage.java | 9 +- .../core/tests/DriverManagerTest.java | 63 ++++++++ .../selcukes/core/tests/TestDriver.java | 1 - .../tests/mobile/AndroidCalculatorTest.java | 9 +- .../src/test/resources/selcukes.yaml | 1 + .../selcukes/databind/utils/Resources.java | 58 +++++++ .../selcukes/databind/utils/StringHelper.java | 15 ++ 24 files changed, 617 insertions(+), 259 deletions(-) create mode 100644 selcukes-core/src/main/java/io/github/selcukes/core/driver/DevicePool.java create mode 100644 selcukes-core/src/main/java/io/github/selcukes/core/driver/DriverFactory.java delete mode 100644 selcukes-core/src/main/java/io/github/selcukes/core/driver/MobileOptions.java delete mode 100644 selcukes-core/src/main/java/io/github/selcukes/core/enums/AppiumDriverType.java create mode 100644 selcukes-core/src/test/java/io/github/selcukes/core/tests/DriverManagerTest.java diff --git a/selcukes-commons/src/main/java/io/github/selcukes/commons/http/WebClient.java b/selcukes-commons/src/main/java/io/github/selcukes/commons/http/WebClient.java index bb196666e..ecc42437c 100644 --- a/selcukes-commons/src/main/java/io/github/selcukes/commons/http/WebClient.java +++ b/selcukes-commons/src/main/java/io/github/selcukes/commons/http/WebClient.java @@ -16,14 +16,12 @@ package io.github.selcukes.commons.http; +import io.github.selcukes.databind.utils.Resources; import io.github.selcukes.databind.utils.StringHelper; import lombok.SneakyThrows; import java.net.InetSocketAddress; -import java.net.MalformedURLException; import java.net.ProxySelector; -import java.net.URI; -import java.net.URL; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.nio.charset.StandardCharsets; @@ -32,7 +30,6 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Map; -import java.util.Optional; import java.util.UUID; import static java.net.http.HttpRequest.BodyPublisher; @@ -44,11 +41,10 @@ public class WebClient { private HttpRequest.Builder requestBuilder; private BodyPublisher bodyPublisher; - @SneakyThrows - public WebClient(final String url) { + public WebClient(final String uri) { clientBuilder = HttpClient.newBuilder(); requestBuilder = HttpRequest.newBuilder() - .uri(new URI(url)); + .uri(Resources.toURI(uri)); } /** @@ -64,7 +60,7 @@ public WebClient(final String url) { @SneakyThrows public Response post(final Object payload) { contentType("application/json"); - HttpRequest request = requestBuilder.POST(bodyPublisher(payload)).build(); + var request = requestBuilder.POST(bodyPublisher(payload)).build(); return execute(request); } @@ -75,7 +71,7 @@ public Response post(final Object payload) { */ @SneakyThrows public Response post() { - HttpRequest request = requestBuilder.POST(bodyPublisher).build(); + var request = requestBuilder.POST(bodyPublisher).build(); return execute(request); } @@ -85,7 +81,7 @@ public Response post() { * @return A Response object. */ public Response delete() { - HttpRequest request = requestBuilder.DELETE().build(); + var request = requestBuilder.DELETE().build(); return execute(request); } @@ -101,7 +97,7 @@ public Response delete() { * @return A Response object */ public Response put(final Object payload) { - HttpRequest request = requestBuilder.PUT(bodyPublisher(payload)).build(); + var request = requestBuilder.PUT(bodyPublisher(payload)).build(); return execute(request); } @@ -155,18 +151,10 @@ private Response execute(final HttpRequest request) { * @return A Response object. */ public Response get() { - HttpRequest request = requestBuilder.GET().build(); + var request = requestBuilder.GET().build(); return execute(request); } - private Optional getProxyUrl(final String proxy) { - try { - return Optional.of(new URL(proxy)); - } catch (MalformedURLException e) { - return Optional.empty(); - } - } - /** * If the proxy parameter is a valid URL, then set the proxy host and port * to the host and port of the URL @@ -175,7 +163,7 @@ private Optional getProxyUrl(final String proxy) { * @return A WebClient object */ public WebClient proxy(final String proxy) { - Optional url = getProxyUrl(proxy); + var url = Resources.tryURL(proxy); url.ifPresent(u -> clientBuilder = clientBuilder .proxy(ProxySelector.of(new InetSocketAddress(u.getHost(), u.getPort() == -1 ? 80 : u.getPort())))); diff --git a/selcukes-commons/src/test/resources/selcukes.yaml b/selcukes-commons/src/test/resources/selcukes.yaml index 9810bef99..085401932 100644 --- a/selcukes-commons/src/test/resources/selcukes.yaml +++ b/selcukes-commons/src/test/resources/selcukes.yaml @@ -5,9 +5,10 @@ env: Dev proxy: false baseUrl: excel: - runner: - filePath: "" - suiteName: "smoke" + runner: fase + suiteFile: "" + dataFile: "" + suiteName: "Smoke" cucumber: module: google features: src/test/resources/features/${module} @@ -16,13 +17,21 @@ cucumber: plugin: web: remote: false + cloud: browser: CHROME headLess: true - serviceUrl: "http://127.0.0.1:8080" + serviceUrl: "http://127.0.0.1:4444" windows: serviceUrl: "http://127.0.0.1:4723" + app: "C:\\Windows\\System32\\notepad.exe" mobile: + remote: false + cloud: BROWSER_STACK + platform: Android + browser: CHROME + headLess: true serviceUrl: "http://127.0.0.1:4723" + app: "src/test/resources/android-app.apk" reports: emailReport: true htmlReport: true @@ -34,7 +43,7 @@ video: watermark: false notifier: notification: false - type: slack + type: Teams webhookToken: WEBHOOKXXXX apiToken: APIXXXX channel: selcukes diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumEngine.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumEngine.java index 73b7c2c5b..83b3c1050 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumEngine.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumEngine.java @@ -20,20 +20,17 @@ import io.appium.java_client.service.local.AppiumServiceBuilder; import io.appium.java_client.service.local.flags.GeneralServerFlag; import io.github.selcukes.commons.exception.DriverSetupException; +import io.github.selcukes.commons.helper.Singleton; import lombok.CustomLog; import java.net.URL; @CustomLog class AppiumEngine { - private static AppiumEngine appiumEngine; private AppiumDriverLocalService service; public static AppiumEngine getInstance() { - if (appiumEngine == null) { - appiumEngine = new AppiumEngine(); - } - return appiumEngine; + return Singleton.instanceOf(AppiumEngine.class); } URL getServiceUrl() { diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumManager.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumManager.java index ebb84564a..2c11e19b6 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumManager.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumManager.java @@ -17,9 +17,11 @@ package io.github.selcukes.core.driver; import io.appium.java_client.android.AndroidDriver; +import io.appium.java_client.ios.IOSDriver; import io.github.selcukes.commons.config.ConfigFactory; +import io.github.selcukes.databind.utils.Resources; import lombok.CustomLog; -import lombok.SneakyThrows; +import org.openqa.selenium.Capabilities; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.RemoteWebDriver; @@ -31,56 +33,68 @@ import static java.util.Optional.ofNullable; @CustomLog -public class AppiumManager implements RemoteManager { +class AppiumManager implements RemoteManager { @Override - public WebDriver createDriver() { + public WebDriver createDriver(Capabilities customCapabilities) { String target = ConfigFactory.getConfig().getMobile().getBrowser().toUpperCase(); - return target.equals("APP") ? createAppDriver() : createBrowserDriver(target); + return target.equals("APP") ? createAppDriver(customCapabilities) + : createBrowserDriver(customCapabilities, target); } - @SneakyThrows public URL getServiceUrl() { - URL serviceUrl; - + String serviceUrl; if (isLocalAppium()) { - serviceUrl = AppiumEngine.getInstance().getServiceUrl(); + serviceUrl = AppiumEngine.getInstance().getServiceUrl().toString(); } else if (isCloudAppium()) { - serviceUrl = new URL(CloudOptions.browserStackUrl()); + serviceUrl = CloudOptions.browserStackUrl(); } else { - serviceUrl = new URL(ConfigFactory.getConfig().getMobile().getServiceUrl()); + serviceUrl = ConfigFactory.getConfig().getMobile().getServiceUrl(); } - logger.debug(() -> String.format("Using ServiceUrl[%s://%s:%s]", serviceUrl.getProtocol(), serviceUrl.getHost(), - serviceUrl.getPort())); - return serviceUrl; + var url = Resources.toURL(serviceUrl); + logger.debug(() -> String.format("Using ServiceUrl[%s://%s:%s]", url.getProtocol(), url.getHost(), + url.getPort())); + return url; } - public WebDriver createBrowserDriver(String browser) { - logger.debug(() -> "Initiating New Mobile Browser Session..."); - var capabilities = ofNullable(AppiumOptions.getUserOptions()) + public WebDriver createBrowserDriver(Capabilities capabilities, String browser) { + logger.debug(() -> "Creating New Mobile Browser Session..."); + var options = ofNullable(capabilities) .orElseGet(() -> { String platform = ConfigFactory.getConfig().getMobile().getPlatform(); var driverOptions = BrowserOptions.getBrowserOptions(BrowserOptions.valueOf(browser), platform); return isCloudAppium() ? driverOptions.merge(CloudOptions.getBrowserStackOptions(false)) : driverOptions; }); - return new RemoteWebDriver(getServiceUrl(), capabilities); + return new RemoteWebDriver(getServiceUrl(), options); + } + + public WebDriver createAppDriver(Capabilities capabilities) { + var platform = ConfigFactory.getConfig().getMobile().getPlatform(); + if (platform.equalsIgnoreCase("IOS")) { + logger.debug(() -> "Creating New IOS App Session..."); + var options = getAppiumAppOptions(capabilities, true); + return new IOSDriver(getServiceUrl(), options); + } else { + logger.debug(() -> "Creating New ANDROID App Session..."); + var options = getAppiumAppOptions(capabilities, false); + return new AndroidDriver(getServiceUrl(), options); + } } - public WebDriver createAppDriver() { - logger.debug(() -> "Initiating New Mobile App Session..."); - var capabilities = ofNullable(AppiumOptions.getUserOptions()) + private Capabilities getAppiumAppOptions(Capabilities capabilities, boolean isIOS) { + return ofNullable(capabilities) .orElseGet(() -> { if (isCloudAppium()) { return CloudOptions.getBrowserStackOptions(true); } else { var app = ConfigFactory.getConfig().getMobile().getApp(); String appPath = Path.of(app).toAbsolutePath().toString(); - logger.info(() -> "Using APP: " + appPath); - return AppiumOptions.getAndroidOptions(appPath); + logger.debug(() -> "Using APP: " + appPath); + return isIOS ? AppiumOptions.getIOSOptions(appPath) + : AppiumOptions.getAndroidOptions(appPath); } }); - return new AndroidDriver(getServiceUrl(), capabilities); } } diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumOptions.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumOptions.java index 1da30b177..b5c24a9b3 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumOptions.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/AppiumOptions.java @@ -17,6 +17,7 @@ package io.github.selcukes.core.driver; import io.appium.java_client.android.options.UiAutomator2Options; +import io.appium.java_client.ios.options.XCUITestOptions; import io.appium.java_client.windows.options.WindowsOptions; import lombok.experimental.UtilityClass; import org.openqa.selenium.Capabilities; @@ -24,8 +25,6 @@ @UtilityClass public class AppiumOptions { - Capabilities caps; - public Capabilities setAppTopLevelWindow(String windowId) { return setCapability("appTopLevelWindow", windowId); } @@ -35,33 +34,34 @@ public Capabilities appRoot() { } public MutableCapabilities getWinAppOptions(String app) { - WindowsOptions windowsOptions = new WindowsOptions(); - windowsOptions.setApp(app); - return merge(windowsOptions); + var options = new WindowsOptions(); + options.setApp(app); + return merge(options); } public MutableCapabilities getAndroidOptions(String app) { - UiAutomator2Options uiAutomator2Options = new UiAutomator2Options(); - uiAutomator2Options.setApp(app); - return merge(uiAutomator2Options); + var options = new UiAutomator2Options(); + options.setApp(app); + return merge(options); } public MutableCapabilities setCapability(String capabilityName, String value) { - MutableCapabilities capabilities = new MutableCapabilities(); - capabilities.setCapability(capabilityName, value); + var capabilities = newCapabilities(); + newCapabilities().setCapability(capabilityName, value); return capabilities; } private MutableCapabilities merge(Capabilities capabilities) { - return new MutableCapabilities().merge(capabilities); + return newCapabilities().merge(capabilities); } - public Capabilities getUserOptions() { - return caps; + private MutableCapabilities newCapabilities() { + return new MutableCapabilities(); } - public void setUserOptions(Capabilities capabilities) { - caps = capabilities; + public MutableCapabilities getIOSOptions(final String app) { + var options = new XCUITestOptions(); + options.setApp(app); + return merge(options); } - } diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/BrowserOptions.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/BrowserOptions.java index 791542ae9..755083782 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/driver/BrowserOptions.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/BrowserOptions.java @@ -16,68 +16,55 @@ package io.github.selcukes.core.driver; -import io.github.selcukes.databind.utils.StringHelper; import lombok.experimental.UtilityClass; import org.openqa.selenium.Capabilities; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.edge.EdgeOptions; import org.openqa.selenium.firefox.FirefoxOptions; -import org.openqa.selenium.ie.InternetExplorerDriver; -import org.openqa.selenium.ie.InternetExplorerOptions; import org.openqa.selenium.remote.Browser; +import static io.github.selcukes.databind.utils.StringHelper.isNonEmpty; import static org.openqa.selenium.remote.Browser.CHROME; import static org.openqa.selenium.remote.Browser.EDGE; import static org.openqa.selenium.remote.Browser.FIREFOX; -import static org.openqa.selenium.remote.Browser.IE; @UtilityClass public class BrowserOptions { - public static final String HEADLESS = "--headless"; + private static final String HEADLESS = "--headless"; + private static final String NEW_HEADLESS = "--headless=new"; public static Capabilities getBrowserOptions(Browser browser, String platform) { boolean isHeadless = RunMode.isHeadless(); if (EDGE.equals(browser)) { - EdgeOptions edgeOptions = new EdgeOptions(); + var edgeOptions = new EdgeOptions(); if (isHeadless) { edgeOptions.addArguments(HEADLESS); } - if (!StringHelper.isNullOrEmpty(platform)) { + if (isNonEmpty(platform)) { edgeOptions.setPlatformName(platform); } return edgeOptions; } else if (FIREFOX.equals(browser)) { - FirefoxOptions firefoxOptions = new FirefoxOptions(); + var firefoxOptions = new FirefoxOptions(); if (isHeadless) { firefoxOptions.addArguments(HEADLESS); } return firefoxOptions; - } else if (IE.equals(browser)) { - InternetExplorerOptions ieOptions = new InternetExplorerOptions().requireWindowFocus(); - ieOptions.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS, true); - ieOptions.setCapability(InternetExplorerDriver.IGNORE_ZOOM_SETTING, true); - ieOptions.setCapability("ignoreProtectedModeSettings", true); - ieOptions.setCapability("disable-popup-blocking", true); - ieOptions.setCapability("enablePersistentHover", true); - return ieOptions; - } - ChromeOptions chromeOptions = new ChromeOptions(); - chromeOptions.addArguments("--remote-allow-origins=*"); - if (isHeadless) { - chromeOptions.addArguments(HEADLESS); - } - if (!StringHelper.isNullOrEmpty(platform)) { - chromeOptions.setPlatformName(platform); + } else { + var chromeOptions = new ChromeOptions(); + if (isHeadless) { + chromeOptions.addArguments(NEW_HEADLESS); + } + if (isNonEmpty(platform)) { + chromeOptions.setPlatformName(platform); + } + return chromeOptions; } - return chromeOptions; - } public Browser valueOf(String browserName) { if (browserName.equalsIgnoreCase("MicroSoftEdge")) { return EDGE; - } else if (browserName.equalsIgnoreCase("internet explorer")) { - return IE; } else if (browserName.equalsIgnoreCase("Firefox")) { return FIREFOX; } else { diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/CloudOptions.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/CloudOptions.java index b218909e8..c8aa0e002 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/driver/CloudOptions.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/CloudOptions.java @@ -63,9 +63,9 @@ public String browserStackUrl() { @Data @DataFile - static class BrowserStack { - String url; - Map capabilities; - Map environments; + private static class BrowserStack { + private String url; + private Map capabilities; + private Map environments; } } diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/DesktopManager.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/DesktopManager.java index 45114981e..5904c8539 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/driver/DesktopManager.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/DesktopManager.java @@ -29,17 +29,17 @@ import static java.util.Optional.ofNullable; @CustomLog -public class DesktopManager extends AppiumManager { +class DesktopManager extends AppiumManager { @Override - public synchronized WebDriver createDriver() { + public synchronized WebDriver createDriver(Capabilities customCapabilities) { logger.debug(() -> "Initiating New Desktop Session..."); String app = ConfigFactory.getConfig().getWindows().getApp(); - URL serviceUrl = Objects.requireNonNull(getServiceUrl()); - Capabilities capabilities = ofNullable(AppiumOptions.getUserOptions()) + var serviceUrl = Objects.requireNonNull(getServiceUrl()); + var options = ofNullable(customCapabilities) .orElse(AppiumOptions.getWinAppOptions(app)); - return new WindowsDriver(serviceUrl, capabilities); + return new WindowsDriver(serviceUrl, options); } @SneakyThrows diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/DevicePool.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/DevicePool.java new file mode 100644 index 000000000..d3eec86a5 --- /dev/null +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/DevicePool.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) Ramesh Babu Prudhvi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.github.selcukes.core.driver; + +import io.github.selcukes.core.enums.DeviceType; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * The DevicePool class represents a collection of devices grouped by their + * device type. + *

+ * Each device is represented as an {@code Object}, which can be a + * {@code WebDriver} instance or any other type of device object. The pool + * maintains a mapping between the device objects and their device types, and + * allows devices to be added, removed, and retrieved based on their device type + * and index. + *

+ *

+ * The DevicePool class is thread-safe, allowing multiple threads to access and + * modify the pool concurrently. + *

+ */ +public class DevicePool { + /** + * A map that associates each device type with a list of devices of that + * type. The list of devices is stored as a {@code CopyOnWriteArrayList}, + * which is thread-safe and allows concurrent reads and writes without the + * need for explicit synchronization. + */ + private final Map> devicesByType; + /** + * A map that associates each device object with its corresponding device + * type. The map is stored as a {@code ConcurrentHashMap}, which is + * thread-safe and allows concurrent reads and writes without the need for + * explicit synchronization. + */ + private final Map devicesByObject; + + /** + * Creates a new instance of the {@code DevicePool} class. Initializes the + * {@code devicesByType} and {@code devicesByObject} maps with thread-safe + * implementations. + */ + public DevicePool() { + devicesByType = new ConcurrentHashMap<>(); + devicesByObject = new ConcurrentHashMap<>(); + } + + /** + * Adds a device to the device pool for the specified device type. + *

+ * The device is represented as an {@code Object}, which can be a + * {@code WebDriver} instance or any other type of device object. The device + * is added to the end of the list of devices for the specified device type. + * + * @param deviceType the device type of the device to be added + * @param device the device object to be added + */ + public synchronized void addDevice(DeviceType deviceType, Object device) { + devicesByType.computeIfAbsent(deviceType, k -> new CopyOnWriteArrayList<>()).add(device); + devicesByObject.put(device, deviceType); + } + + /** + * Returns a list of devices of the given type in the pool. + * + * @param deviceType the type of devices being retrieved + * @return a list of devices of the given type in the pool, or an + * empty list if there are no devices of that type + */ + public synchronized List getDevices(DeviceType deviceType) { + return devicesByType.getOrDefault(deviceType, Collections.emptyList()); + } + + /** + * Returns the device object at the specified index in the list of devices + * of the given type. If the index is out of range, an + * {@code IndexOutOfBoundsException} is thrown. + * + * @param deviceType the type of device being retrieved + * @param index the index of the device in the list of + * devices + * @return the device object at the specified + * index + * @throws IndexOutOfBoundsException if the index is out of range + */ + public synchronized Object getDevice(DeviceType deviceType, int index) { + var deviceList = getDevices(deviceType); + if (index >= 0 && index < deviceList.size()) { + return deviceList.get(index); + } else { + throw new IndexOutOfBoundsException( + String.format("Invalid device index %d for device type %s", index, deviceType)); + } + } + + /** + * Returns a map of all device types to their corresponding lists of devices + * in the pool. + * + * @return a map of all device types to their corresponding lists of devices + * in the pool + */ + public synchronized Map> getAllDevices() { + return Collections.unmodifiableMap(devicesByType); + } + + /** + * Removes a device from the pool. + * + * @param device the device being removed + */ + public synchronized void removeDevice(Object device) { + var deviceType = devicesByObject.get(device); + if (deviceType != null) { + devicesByType.get(deviceType).remove(device); + devicesByObject.remove(device); + } + } + + /** + * Checks if the pool has a device of the given type and with the given + * reference. + * + * @param deviceType the type of the device being checked + * @param device the device being checked + * @return true if the pool has a device of the given type and + * with the given reference, false otherwise + */ + public boolean hasDevice(final DeviceType deviceType, final Object device) { + return getDevices(deviceType).contains(device); + } + +} diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/DriverFactory.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/DriverFactory.java new file mode 100644 index 000000000..0521f38dd --- /dev/null +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/DriverFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) Ramesh Babu Prudhvi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.github.selcukes.core.driver; + +import io.appium.java_client.windows.WindowsDriver; +import io.github.selcukes.commons.exception.DriverSetupException; +import io.github.selcukes.core.enums.DeviceType; +import io.github.selcukes.core.listener.EventCapture; +import lombok.CustomLog; +import lombok.experimental.UtilityClass; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.events.EventFiringDecorator; + +import java.util.Map; +import java.util.function.Supplier; + +import static java.lang.String.format; +import static java.util.Optional.ofNullable; + +/** + * A factory class for creating instances of the WebDriver. This class uses a + * map of device types to driver managers to determine which driver manager to + * use for creating the appropriate WebDriver. + */ +@CustomLog +@UtilityClass +public class DriverFactory { + + /** + * A map of device types to driver managers. + */ + private static final Map> DRIVER_MANAGER_MAP = Map.of( + DeviceType.BROWSER, WebManager::new, + DeviceType.DESKTOP, DesktopManager::new, + DeviceType.MOBILE, AppiumManager::new); + + /** + * Creates a new instance of the Selenium WebDriver for the given device + * type and capabilities. + * + * @param deviceType the type of device to create the driver for + * @param capabilities the custom capabilities for the driver + * @return the created WebDriver instance + * @throws DriverSetupException if a driver session cannot be created for + * the given device type + */ + public synchronized WebDriver create(DeviceType deviceType, Capabilities capabilities) { + // Log the creation of a new session + logger.info(() -> format("Creating new %s session...", deviceType)); + + // Get the appropriate driver manager for the given device type + var remoteManager = ofNullable(DRIVER_MANAGER_MAP.get(deviceType)) + .map(Supplier::get) + .orElseThrow(() -> new DriverSetupException( + "Unable to create new driver session for Driver Type[" + deviceType + "]")); + + // Create the WebDriver using the selected driver manager + var webDriver = remoteManager.createDriver(capabilities); + + // Decorate the WebDriver with the EventFiringDecorator and EventCapture + // if it's not a WindowsDriver + return (webDriver instanceof WindowsDriver ? webDriver + : new EventFiringDecorator<>(new EventCapture()).decorate(webDriver)); + } +} diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/DriverManager.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/DriverManager.java index afa84e97b..4fb29c4a9 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/driver/DriverManager.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/DriverManager.java @@ -16,98 +16,169 @@ package io.github.selcukes.core.driver; -import io.appium.java_client.windows.WindowsDriver; -import io.github.selcukes.commons.exception.DriverSetupException; import io.github.selcukes.commons.fixture.DriverFixture; +import io.github.selcukes.commons.helper.SingletonContext; import io.github.selcukes.core.enums.DeviceType; -import io.github.selcukes.core.listener.EventCapture; import lombok.CustomLog; import lombok.experimental.UtilityClass; import org.openqa.selenium.Capabilities; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WrapsDriver; -import org.openqa.selenium.support.events.EventFiringDecorator; import java.util.Arrays; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; +import java.util.List; +import java.util.stream.Stream; import static java.lang.String.format; -import static java.util.Optional.ofNullable; +/** + * The DriverManager class manages a pool of WebDriver instances and provides + * methods for creating, switching, and removing WebDriver instances for + * different devices and capabilities. + */ @CustomLog @UtilityClass public class DriverManager { private static final ThreadLocal DRIVER_THREAD = new InheritableThreadLocal<>(); - private static final Map STORED_DRIVER = new ConcurrentHashMap<>(); + private static final SingletonContext DEVICE_POOL = SingletonContext.with(DevicePool::new); + /** + * Creates a new driver instance for the specified device type and + * capabilities, adds it to the DevicePool, sets it as the current driver + * for the current thread, and returns it. + * + * @param deviceType the device type for which the driver instance should + * be created + * @param capabilities the capabilities for the driver instance + * @return the newly created driver instance + */ public synchronized D createDriver(DeviceType deviceType, Capabilities... capabilities) { - Arrays.stream(capabilities).findAny().ifPresent(AppiumOptions::setUserOptions); - if (getDriver() == null) { - logger.info(() -> format("Creating new %s session...", deviceType)); - Map> driverManagerMap = Map.of( - DeviceType.BROWSER, WebManager::new, - DeviceType.DESKTOP, DesktopManager::new, - DeviceType.MOBILE, AppiumManager::new); - var remoteManager = ofNullable(driverManagerMap.get(deviceType)) - .map(Supplier::get) - .orElseThrow( - () -> new DriverSetupException( - "Unable to create new driver session for Driver Type[" + deviceType + "]")); - var webDriver = remoteManager.createDriver(); - setDriver(webDriver instanceof WindowsDriver ? webDriver - : new EventFiringDecorator<>(new EventCapture()).decorate(webDriver)); - } + createDevice(deviceType, capabilities); + setDriver(DEVICE_POOL.get().getDevice(deviceType, 0)); return getDriver(); } + /** + * Returns the device pool. + * + * @return the device pool + */ + public synchronized DevicePool getDevicePool() { + return DEVICE_POOL.get(); + } + + /** + * Creates a new device with the specified device type and capabilities, and + * adds it to the device pool if it does not already exist. + * + * @param deviceType the type of device to create + * @param capabilities the capabilities to use when creating the device + */ + private synchronized void createDevice(DeviceType deviceType, Capabilities... capabilities) { + Stream.ofNullable(capabilities) + .flatMap(Arrays::stream) + .map(options -> DriverFactory.create(deviceType, options)) + .forEach(device -> getDevicePool().addDevice(deviceType, device)); + if (getDevicePool().getDevices(deviceType).isEmpty()) { + getDevicePool().addDevice(deviceType, DriverFactory.create(deviceType, null)); + } + } + + /** + * Returns the WebDriver instance for the current thread. + * + * @param the type of WebDriver to return + * @return the WebDriver instance for the current thread + */ @SuppressWarnings("unchecked") - public static D getDriver() { + public synchronized D getDriver() { return (D) DRIVER_THREAD.get(); } - public static void setDriver(D driver) { + /** + * Sets the WebDriver instance for the specified device type and index as + * the current driver for the current thread. + * + * @param deviceType the device type for which the driver instance should be + * switched + * @param index the zero-based index of the driver instance to be + * switched + */ + public synchronized void switchDriver(DeviceType deviceType, int index) { + setDriver(getDevicePool().getDevice(deviceType, index)); + } + + /** + * Sets the specified WebDriver instance as the current driver for the + * current thread. + * + * @param driver the WebDriver instance to be set as the current driver + */ + public synchronized void setDriver(Object driver) { DRIVER_THREAD.set(driver); DriverFixture.setDriverFixture(driver); - STORED_DRIVER.putIfAbsent(driver.hashCode(), driver); } - public static WebDriver getWrappedDriver() { + /** + * Returns the underlying WebDriver instance wrapped by any + * {@link WrapsDriver} instance currently set as the driver for the current + * thread. If the current driver does not implement the {@link WrapsDriver} + * interface, the current driver instance is returned. + * + * @return the underlying WebDriver instance wrapped by any + * {@link WrapsDriver} instance currently set as the driver for the + * current thread, or the current driver instance if it does not + * implement {@link WrapsDriver}. + */ + public synchronized WebDriver getWrappedDriver() { if (getDriver() instanceof WrapsDriver) { return ((WrapsDriver) getDriver()).getWrappedDriver(); } return getDriver(); } - public static synchronized void removeDriver() { + /** + * Removes the current driver instance from the current thread, closing any + * associated WebDriver instances and removing the device from the device + * pool. + */ + public synchronized void removeDriver() { try { if (getDriver() != null) { - STORED_DRIVER.remove(getDriver().hashCode()); getDriver().quit(); + getDevicePool().removeDevice(getDriver()); } } finally { DRIVER_THREAD.remove(); } } - public static synchronized void removeAllDrivers() { - logger.debug(() -> format("Closing [%d] stored drivers..", STORED_DRIVER.size())); - - STORED_DRIVER.values().stream() + /** + * Removes all WebDriver instances from the DevicePool and quits each + * driver. + *

+ * If a driver fails to quit, a warning message is logged with details of + * the driver and the exception that occurred. + *

+ */ + public synchronized void removeAllDrivers() { + getDevicePool().getAllDevices().values().stream() + .flatMap(List::stream) .filter(WebDriver.class::isInstance) .map(WebDriver.class::cast) .forEach(webDriver -> { try { webDriver.quit(); + getDevicePool().removeDevice(webDriver); + logger.debug( + () -> format("Closed driver %d and removed from DevicePool.", webDriver.hashCode())); } catch (Exception e) { logger.warn( () -> format("Failed to close driver %d: %s", webDriver.hashCode(), e.getMessage())); } }); - - STORED_DRIVER.clear(); DRIVER_THREAD.remove(); } + } diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/MobileOptions.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/MobileOptions.java deleted file mode 100644 index d66e7fba4..000000000 --- a/selcukes-core/src/main/java/io/github/selcukes/core/driver/MobileOptions.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) Ramesh Babu Prudhvi. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.github.selcukes.core.driver; - -import io.appium.java_client.android.options.EspressoOptions; -import io.appium.java_client.android.options.UiAutomator2Options; -import io.appium.java_client.gecko.options.GeckoOptions; -import io.appium.java_client.mac.options.Mac2Options; -import io.appium.java_client.safari.options.SafariOptions; -import io.appium.java_client.windows.options.WindowsOptions; -import io.github.selcukes.commons.exception.DriverSetupException; -import io.github.selcukes.core.enums.AppiumDriverType; -import org.openqa.selenium.Capabilities; - -public class MobileOptions { - - public Capabilities getAppiumOptions(AppiumDriverType appiumDriverType) { - switch (appiumDriverType) { - case SAFARI: - return new SafariOptions(); - case GECKO: - return new GeckoOptions(); - case MAC: - return new Mac2Options(); - case WINDOWS: - return new WindowsOptions(); - case ESPRESSO: - return new EspressoOptions(); - case UIAUTOMATOR: - return new UiAutomator2Options(); - default: - throw new DriverSetupException("Not Supported option" + appiumDriverType); - - } - - } -} diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/RemoteManager.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/RemoteManager.java index 3a817ff7d..a649d4d44 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/driver/RemoteManager.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/RemoteManager.java @@ -16,8 +16,9 @@ package io.github.selcukes.core.driver; +import org.openqa.selenium.Capabilities; import org.openqa.selenium.WebDriver; public interface RemoteManager { - WebDriver createDriver(); + WebDriver createDriver(Capabilities capabilities); } diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/RunMode.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/RunMode.java index 7539ce9fb..fb9930d19 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/driver/RunMode.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/RunMode.java @@ -20,13 +20,13 @@ import io.github.selcukes.commons.os.Platform; import lombok.experimental.UtilityClass; -import static io.github.selcukes.databind.utils.StringHelper.isNullOrEmpty; +import static io.github.selcukes.databind.utils.StringHelper.isNonEmpty; @UtilityClass public class RunMode { static boolean isCloudAppium() { String cloud = ConfigFactory.getConfig().getMobile().getCloud(); - return !isNullOrEmpty(cloud); + return isNonEmpty(cloud); } static boolean isLocalAppium() { @@ -39,7 +39,7 @@ static boolean isLocalBrowser() { static boolean isCloudBrowser() { String cloud = ConfigFactory.getConfig().getWeb().getCloud(); - return !isNullOrEmpty(cloud); + return isNonEmpty(cloud); } boolean isHeadless() { diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/driver/WebManager.java b/selcukes-core/src/main/java/io/github/selcukes/core/driver/WebManager.java index 59b25c4f0..77732451a 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/driver/WebManager.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/driver/WebManager.java @@ -17,8 +17,9 @@ package io.github.selcukes.core.driver; import io.github.selcukes.commons.config.ConfigFactory; +import io.github.selcukes.databind.utils.Resources; import lombok.CustomLog; -import lombok.SneakyThrows; +import org.openqa.selenium.Capabilities; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.RemoteWebDriver; @@ -30,13 +31,13 @@ import static java.util.Optional.ofNullable; @CustomLog -public class WebManager implements RemoteManager { +class WebManager implements RemoteManager { - public synchronized WebDriver createDriver() { + public synchronized WebDriver createDriver(Capabilities customCapabilities) { String browser = ConfigFactory.getConfig().getWeb().getBrowser().toUpperCase(); logger.debug(() -> "Initiating New Browser Session..."); - var capabilities = ofNullable(AppiumOptions.getUserOptions()) + var capabilities = ofNullable(customCapabilities) .orElseGet(() -> { var driverOptions = BrowserOptions .getBrowserOptions(BrowserOptions.valueOf(browser), ""); @@ -55,21 +56,20 @@ public synchronized WebDriver createDriver() { return driverBuilder.build(); } - @SneakyThrows public URL getServiceUrl() { - URL serviceUrl; - if (isCloudBrowser()) { - serviceUrl = new URL(CloudOptions.browserStackUrl()); + var serviceUrl = isCloudBrowser() + ? CloudOptions.browserStackUrl() + : ConfigFactory.getConfig().getWeb().getServiceUrl(); + + if (isSeleniumServerNotRunning()) { + logger.warn(() -> "The Selenium server is not running.\n" + + "Please use the 'GridRunner.startSelenium()' method to start it automatically.\n" + + "If you have started it manually or are executing in the cloud, you can ignore this message."); } else { - serviceUrl = new URL(ConfigFactory.getConfig().getWeb().getServiceUrl()); - if (isSeleniumServerNotRunning()) { - logger.warn(() -> "Selenium server not started...\n" + - "Please use 'GridRunner.startSeleniumServer' method to start automatically.\n" + - " Ignore this message if you have started manually or executing in Cloud..."); - } else { - return GridRunner.getLocalServiceUrl(); - } + return GridRunner.getLocalServiceUrl(); } - return serviceUrl; + + return Resources.toURL(serviceUrl); } + } diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/enums/AppiumDriverType.java b/selcukes-core/src/main/java/io/github/selcukes/core/enums/AppiumDriverType.java deleted file mode 100644 index 4394297e4..000000000 --- a/selcukes-core/src/main/java/io/github/selcukes/core/enums/AppiumDriverType.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) Ramesh Babu Prudhvi. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.github.selcukes.core.enums; - -public enum AppiumDriverType { - SAFARI, - GECKO, - MAC, - WINDOWS, - ESPRESSO, - UIAUTOMATOR -} diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/page/Pages.java b/selcukes-core/src/main/java/io/github/selcukes/core/page/Pages.java index cd3028bb6..21356537a 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/page/Pages.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/page/Pages.java @@ -20,22 +20,23 @@ import io.github.selcukes.core.driver.DriverManager; import io.github.selcukes.core.enums.DeviceType; import lombok.experimental.UtilityClass; +import org.openqa.selenium.Capabilities; import org.openqa.selenium.WebDriver; @UtilityClass public class Pages { - public synchronized WebPage webPage() { - WebDriver driver = DriverManager.createDriver(DeviceType.BROWSER); + public synchronized WebPage webPage(Capabilities... capabilities) { + WebDriver driver = DriverManager.createDriver(DeviceType.BROWSER, capabilities); return new WebPage(driver); } - public synchronized WinPage winPage() { - WindowsDriver driver = DriverManager.createDriver(DeviceType.DESKTOP); + public synchronized WinPage winPage(Capabilities... capabilities) { + WindowsDriver driver = DriverManager.createDriver(DeviceType.DESKTOP, capabilities); return new WinPage(driver); } - public synchronized MobilePage mobilePage() { - WebDriver driver = DriverManager.createDriver(DeviceType.MOBILE); + public synchronized MobilePage mobilePage(Capabilities... capabilities) { + WebDriver driver = DriverManager.createDriver(DeviceType.MOBILE, capabilities); return new MobilePage(driver); } diff --git a/selcukes-core/src/main/java/io/github/selcukes/core/page/WinPage.java b/selcukes-core/src/main/java/io/github/selcukes/core/page/WinPage.java index 847c18cf9..cf7f57f39 100644 --- a/selcukes-core/src/main/java/io/github/selcukes/core/page/WinPage.java +++ b/selcukes-core/src/main/java/io/github/selcukes/core/page/WinPage.java @@ -21,6 +21,7 @@ import io.github.selcukes.core.driver.AppiumOptions; import io.github.selcukes.core.driver.DriverManager; import io.github.selcukes.core.enums.DeviceType; +import io.github.selcukes.databind.utils.StringHelper; import lombok.CustomLog; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; @@ -38,13 +39,13 @@ public WebDriver getDriver() { return driver; } - public WinPage switchToWindow(Object locator) { + public WinPage switchToWindowBy(Object locator) { WebElement newWindowElement = find(locator); String appTopLevelWindow = newWindowElement.getAttribute("NativeWindowHandle"); - Preconditions.checkArgument(appTopLevelWindow.isEmpty(), + Preconditions.checkArgument(!appTopLevelWindow.isEmpty(), "The found window does not have NativeWindowHandle property"); - String windowIdToHex = Integer.toHexString(Integer.parseInt(appTopLevelWindow)); - logger.info(() -> "Window Id: " + appTopLevelWindow + "After: " + windowIdToHex); + String windowIdToHex = StringHelper.toHex(appTopLevelWindow); + logger.debug(() -> "Window Id: " + appTopLevelWindow + "After: " + windowIdToHex); driver = DriverManager.createDriver(DeviceType.DESKTOP, AppiumOptions.setAppTopLevelWindow(windowIdToHex)); driver.switchTo().activeElement(); return this; diff --git a/selcukes-core/src/test/java/io/github/selcukes/core/tests/DriverManagerTest.java b/selcukes-core/src/test/java/io/github/selcukes/core/tests/DriverManagerTest.java new file mode 100644 index 000000000..64082d673 --- /dev/null +++ b/selcukes-core/src/test/java/io/github/selcukes/core/tests/DriverManagerTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) Ramesh Babu Prudhvi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.github.selcukes.core.tests; + +import io.github.selcukes.commons.config.ConfigFactory; +import io.github.selcukes.core.driver.BrowserOptions; +import io.github.selcukes.core.driver.DevicePool; +import io.github.selcukes.core.driver.DriverManager; +import io.github.selcukes.core.enums.DeviceType; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.edge.EdgeDriver; +import org.openqa.selenium.edge.EdgeOptions; +import org.openqa.selenium.remote.Browser; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class DriverManagerTest { + @Test + public void testCustomOptions() { + ConfigFactory.getConfig().getWeb().setRemote(false); + var chromeOptions = BrowserOptions.getBrowserOptions(Browser.CHROME, ""); + var edgeOptions = BrowserOptions.getBrowserOptions(Browser.EDGE, ""); + DriverManager.createDriver(DeviceType.BROWSER, chromeOptions, edgeOptions); + assertTrue(DriverManager.getWrappedDriver() instanceof ChromeDriver); + DriverManager.switchDriver(DeviceType.BROWSER, 1); + assertTrue(DriverManager.getWrappedDriver() instanceof EdgeDriver); + } + + @Test(enabled = false) + public void devicePoolWithTwoDifferentOptionsTest() { + DevicePool pool = new DevicePool(); + pool.addDevice(DeviceType.BROWSER, new ChromeOptions()); + pool.addDevice(DeviceType.BROWSER, new EdgeOptions()); + assertEquals(pool.getAllDevices().get(DeviceType.BROWSER).size(), 2); + } + + @Test(enabled = false) + public void devicePoolWithTwoSameOptionsTest() { + DevicePool pool = new DevicePool(); + pool.addDevice(DeviceType.BROWSER, new ChromeOptions()); + pool.addDevice(DeviceType.BROWSER, new ChromeOptions()); + pool.addDevice(DeviceType.BROWSER, new ChromeOptions()); + assertEquals(pool.getAllDevices().get(DeviceType.BROWSER).size(), 1); + } + +} diff --git a/selcukes-core/src/test/java/io/github/selcukes/core/tests/TestDriver.java b/selcukes-core/src/test/java/io/github/selcukes/core/tests/TestDriver.java index f62139dc4..095f84fa0 100644 --- a/selcukes-core/src/test/java/io/github/selcukes/core/tests/TestDriver.java +++ b/selcukes-core/src/test/java/io/github/selcukes/core/tests/TestDriver.java @@ -26,7 +26,6 @@ public class TestDriver { public static WebDriver getChromeDriver() { var options = new ChromeOptions(); options.addArguments("--headless"); - options.addArguments("--remote-allow-origins=*"); return new ChromeDriver(options); } } diff --git a/selcukes-core/src/test/java/io/github/selcukes/core/tests/mobile/AndroidCalculatorTest.java b/selcukes-core/src/test/java/io/github/selcukes/core/tests/mobile/AndroidCalculatorTest.java index d79067663..3410d0652 100644 --- a/selcukes-core/src/test/java/io/github/selcukes/core/tests/mobile/AndroidCalculatorTest.java +++ b/selcukes-core/src/test/java/io/github/selcukes/core/tests/mobile/AndroidCalculatorTest.java @@ -18,11 +18,9 @@ import io.appium.java_client.android.options.UiAutomator2Options; import io.github.selcukes.commons.annotation.Lifecycle; -import io.github.selcukes.core.driver.DriverManager; -import io.github.selcukes.core.enums.DeviceType; import io.github.selcukes.core.page.MobilePage; +import io.github.selcukes.core.page.Pages; import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -34,13 +32,12 @@ public class AndroidCalculatorTest { @BeforeMethod void beforeTest() { - UiAutomator2Options options = new UiAutomator2Options(); + var options = new UiAutomator2Options(); options.setAppPackage("com.android.calculator2"); options.setAppActivity("com.android.calculator2.Calculator"); options.setNewCommandTimeout(Duration.ofSeconds(11)); options.setFullReset(false); - WebDriver driver = DriverManager.createDriver(DeviceType.MOBILE, options); - page = new MobilePage(driver); + page = Pages.mobilePage(options); } @Test(enabled = false) diff --git a/selcukes-core/src/test/resources/selcukes.yaml b/selcukes-core/src/test/resources/selcukes.yaml index 1a2dd83ac..34f09e7df 100644 --- a/selcukes-core/src/test/resources/selcukes.yaml +++ b/selcukes-core/src/test/resources/selcukes.yaml @@ -18,5 +18,6 @@ mobile: cloud: BROWSER_STACK platform: Android browser: CHROME + headLess: true serviceUrl: "http://127.0.0.1:4723" app: "src/test/resources/android-app.apk" diff --git a/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/Resources.java b/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/Resources.java index 7916be9e4..4a1919d09 100644 --- a/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/Resources.java +++ b/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/Resources.java @@ -24,9 +24,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; +import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Stream; @@ -206,4 +211,57 @@ public static InputStream newFileStream(String filePath) { public static OutputStream newOutputStream(Path filePath) { return Files.newOutputStream(filePath); } + + /** + * Returns a new URL object by parsing the given URL string. + * + * @param urlStr the URL string to be parsed into a URL + * object + * @return the URL object representing the parsed + * URL string + * @throws IllegalArgumentException if the URL string is invalid and cannot + * be parsed + */ + public URL toURL(String urlStr) { + try { + return new URL(urlStr); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid URL string: " + urlStr, e); + } + } + + /** + * Returns an Optional containing a new URL object by parsing the given URL + * string, or an empty Optional if the URL string is invalid. + * + * @param urlStr the URL string to be parsed into a URL object + * @return an Optional containing the URL object representing the + * parsed URL string, or an empty Optional if the URL string + * is invalid + */ + public Optional tryURL(String urlStr) { + try { + return Optional.of(new URL(urlStr)); + } catch (MalformedURLException e) { + return Optional.empty(); + } + } + + /** + * Returns a new URI object by parsing the given URI string. + * + * @param uriStr the URI string to be parsed into a URI + * object + * @return the URI object representing the parsed + * URI string + * @throws IllegalArgumentException if the URI string is invalid and cannot + * be parsed + */ + public URI toURI(String uriStr) { + try { + return new URI(uriStr); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URI string: " + uriStr, e); + } + } } diff --git a/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/StringHelper.java b/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/StringHelper.java index 1a51b2c34..a0dacc237 100644 --- a/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/StringHelper.java +++ b/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/StringHelper.java @@ -267,4 +267,19 @@ public boolean containsWord(String label, List words) { .anyMatch(word -> label.toLowerCase().contains(word.toLowerCase())); } + /** + * Converts the given string representation of an integer to its hexadecimal + * string representation. + * + * @param decimalString the string representation of an integer to + * convert + * @return the hexadecimal string representation of + * the given integer + * @throws NumberFormatException if the input string is not a valid integer + */ + public String toHex(String decimalString) { + int decimal = Integer.parseInt(decimalString); + return Integer.toHexString(decimal); + } + }