From 1b3104af041d96dc0c6bf6f963c273dd88709ec9 Mon Sep 17 00:00:00 2001 From: "andrei.on1ca" Date: Thu, 5 Feb 2026 11:34:01 +0200 Subject: [PATCH 1/4] Implemented AltAppiumHelper class --- Bindings~/python/alttester/__init__.py | 1 + Bindings~/python/alttester/appium_helper.py | 116 ++++++++++++++++++ .../alttester/reverse_port_forwarding.py | 1 - 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 Bindings~/python/alttester/appium_helper.py diff --git a/Bindings~/python/alttester/__init__.py b/Bindings~/python/alttester/__init__.py index bda18de49..4805e44be 100644 --- a/Bindings~/python/alttester/__init__.py +++ b/Bindings~/python/alttester/__init__.py @@ -23,3 +23,4 @@ from alttester.logging import AltLogLevel, AltLogger from alttester.exceptions import * from alttester.reverse_port_forwarding import * +from alttester.appium_helper import * diff --git a/Bindings~/python/alttester/appium_helper.py b/Bindings~/python/alttester/appium_helper.py new file mode 100644 index 000000000..96572bd84 --- /dev/null +++ b/Bindings~/python/alttester/appium_helper.py @@ -0,0 +1,116 @@ +""" + Copyright(C) 2025 Altom Consulting + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +import re +import ipaddress + +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class AltAppiumHelper: + """API to interact with native popups using Appium""" + + @staticmethod + def _is_valid_host(host): + if not host or not isinstance(host, str): + return False + + # Try to parse as IP address (IPv4 or IPv6) + try: + ipaddress.ip_address(host) + return True + except ValueError: + pass + + # Validate as hostname + # Hostname pattern: labels separated by dots, each label contains alphanumeric and hyphens + # Label cannot start or end with hyphen, max 63 characters per label + hostname_pattern = r'^(?!-)[a-zA-Z0-9-]{1,63}(? 65535: + raise ValueError(f"Invalid port: {port_int}. The port number should be between 1 and 65535.") + + try: + # Set XPath based on platform + if platform.lower() == 'ios': + host_xpath = '//XCUIElementTypeTextField[@value="Host"]' + port_xpath = '//XCUIElementTypeTextField[@value="Port"]' + app_name_xpath = '//XCUIElementTypeTextField[@value="App Name"]' + ok_button_xpath = '//XCUIElementTypeButton[@name="OK"]' + elif platform.lower() == 'android': + host_xpath = '//android.widget.EditText[@text="Host"]' + port_xpath = '//android.widget.EditText[@text="Port"]' + app_name_xpath = '//android.widget.EditText[@text="App Name"]' + ok_button_xpath = '//android.widget.Button[@resource-id="android:id/button1"]' + else: + raise ValueError(f"Unsupported platform: {platform}. Supported platforms are 'android' and 'ios'.") + + # Wait for the connection dialog to be present + wait = WebDriverWait(appium_driver, timeout) + wait.until(EC.presence_of_element_located((AppiumBy.XPATH, host_xpath))) + + # Find elements + host_field = appium_driver.find_element(AppiumBy.XPATH, host_xpath) + port_field = appium_driver.find_element(AppiumBy.XPATH, port_xpath) + app_name_field = appium_driver.find_element(AppiumBy.XPATH, app_name_xpath) + ok_button = appium_driver.find_element(AppiumBy.XPATH, ok_button_xpath) + + # Set values + host_field.clear() + host_field.send_keys(host) + + port_field.clear() + port_field.send_keys(port_str) + + app_name_field.clear() + app_name_field.send_keys(app_name) + + # Press OK button + ok_button.click() + + except Exception as ex: + raise Exception(f"Error while setting connection data on {platform} platform: {str(ex)}") from ex diff --git a/Bindings~/python/alttester/reverse_port_forwarding.py b/Bindings~/python/alttester/reverse_port_forwarding.py index 315bf340b..7b842fa8a 100644 --- a/Bindings~/python/alttester/reverse_port_forwarding.py +++ b/Bindings~/python/alttester/reverse_port_forwarding.py @@ -1,6 +1,5 @@ """ Copyright(C) 2025 Altom Consulting - Copyright(C) 2025 Altom Consulting This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by From 07dfac6d9fb208ff747b0aabc83a4ced2d7a6601 Mon Sep 17 00:00:00 2001 From: "andrei.on1ca" Date: Thu, 5 Feb 2026 12:55:43 +0200 Subject: [PATCH 2/4] Make host, port and app_name optional parameters --- Bindings~/python/alttester/appium_helper.py | 53 ++++++++++++--------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/Bindings~/python/alttester/appium_helper.py b/Bindings~/python/alttester/appium_helper.py index 96572bd84..2a3d2796d 100644 --- a/Bindings~/python/alttester/appium_helper.py +++ b/Bindings~/python/alttester/appium_helper.py @@ -45,16 +45,16 @@ def _is_valid_host(host): return re.match(hostname_pattern, host) is not None @staticmethod - def set_connection_data(appium_driver, platform, host, port, app_name, timeout=60): + def set_connection_data(appium_driver, platform, host=None, port=None, app_name=None, timeout=60): """ Sets connection data in the native popup dialog Args: appium_driver: The Appium driver instance platform (str): The platform ('android' or 'ios') - host (str): The host value to set - port (str or int): The port value to set - app_name (str): The app name value to set + host (str, optional): The host value to set. If not provided, the host field won't be updated + port (str or int, optional): The port value to set. If not provided, the port field won't be updated + app_name (str, optional): The app name value to set. If not provided, the app name field won't be updated timeout (int): Timeout in seconds for waiting for elements (default: 60) Raises: @@ -63,16 +63,20 @@ def set_connection_data(appium_driver, platform, host, port, app_name, timeout=6 if appium_driver is None: raise ValueError("Appium driver cannot be None") - # Convert port to string if it's an integer - port_str = str(port) if isinstance(port, int) else port + # Check that at least one field is provided + if host is None and port is None and app_name is None: + raise ValueError("At least one of 'host', 'port', or 'app_name' must be provided") # Validate connection data - if not AltAppiumHelper._is_valid_host(host): + if host is not None and not AltAppiumHelper._is_valid_host(host): raise ValueError(f"Invalid host: {host}. The host should be a valid host.") - port_int = int(port_str) - if port_int <= 0 or port_int > 65535: - raise ValueError(f"Invalid port: {port_int}. The port number should be between 1 and 65535.") + port_str = None + if port is not None: + port_str = str(port) if isinstance(port, int) else port + port_int = int(port_str) + if port_int <= 0 or port_int > 65535: + raise ValueError(f"Invalid port: {port_int}. The port number should be between 1 and 65535.") try: # Set XPath based on platform @@ -93,23 +97,26 @@ def set_connection_data(appium_driver, platform, host, port, app_name, timeout=6 wait = WebDriverWait(appium_driver, timeout) wait.until(EC.presence_of_element_located((AppiumBy.XPATH, host_xpath))) - # Find elements - host_field = appium_driver.find_element(AppiumBy.XPATH, host_xpath) - port_field = appium_driver.find_element(AppiumBy.XPATH, port_xpath) - app_name_field = appium_driver.find_element(AppiumBy.XPATH, app_name_xpath) - ok_button = appium_driver.find_element(AppiumBy.XPATH, ok_button_xpath) - - # Set values - host_field.clear() - host_field.send_keys(host) + # Update host if provided + if host is not None: + host_field = appium_driver.find_element(AppiumBy.XPATH, host_xpath) + host_field.clear() + host_field.send_keys(host) - port_field.clear() - port_field.send_keys(port_str) + # Update port if provided + if port is not None: + port_field = appium_driver.find_element(AppiumBy.XPATH, port_xpath) + port_field.clear() + port_field.send_keys(port_str) - app_name_field.clear() - app_name_field.send_keys(app_name) + # Update app_name if provided + if app_name is not None: + app_name_field = appium_driver.find_element(AppiumBy.XPATH, app_name_xpath) + app_name_field.clear() + app_name_field.send_keys(app_name) # Press OK button + ok_button = appium_driver.find_element(AppiumBy.XPATH, ok_button_xpath) ok_button.click() except Exception as ex: From 72f280790b7396d3708e6c815cfe26aa661447d9 Mon Sep 17 00:00:00 2001 From: "andrei.on1ca" Date: Thu, 5 Feb 2026 13:40:19 +0200 Subject: [PATCH 3/4] Implemented AltAppiumHelper.cs --- .../Runtime/AltDriver/AltAppiumHelper.cs | 166 ++++++++++++++++++ Bindings~/dotnet/AltDriver/AltDriver.csproj | 2 + Bindings~/python/alttester/appium_helper.py | 3 +- 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 Assets/AltTester/Runtime/AltDriver/AltAppiumHelper.cs diff --git a/Assets/AltTester/Runtime/AltDriver/AltAppiumHelper.cs b/Assets/AltTester/Runtime/AltDriver/AltAppiumHelper.cs new file mode 100644 index 000000000..8d07dde2a --- /dev/null +++ b/Assets/AltTester/Runtime/AltDriver/AltAppiumHelper.cs @@ -0,0 +1,166 @@ +/* + Copyright(C) 2025 Altom Consulting + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +using System; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Support.UI; +using SeleniumExtras.WaitHelpers; + +namespace AltTester.AltTesterSDK.Driver +{ + /// + /// API to interact with the native popup dialog (implemented by AltTester) using Appium + /// + public class AltAppiumHelper + { + /// + /// Sets connection data in the native popup dialog + /// + /// The Appium driver instance + /// The platform (Android or iOS) + /// The host value to set. If not provided, the host field won't be updated + /// The port value to set. If not provided, the port field won't be updated + /// The app name value to set. If not provided, the app name field won't be updated + /// Timeout in seconds for waiting for elements (default: 60) + /// Thrown when appiumDriver is null + /// Thrown when no fields are provided for update + /// Thrown when host or port is invalid + /// Thrown when an error occurs while interacting with the popup + public static void SetConnectionData(AppiumDriver appiumDriver, string platform, string host = null, string port = null, string appName = null, int timeout = 60) + { + if (appiumDriver == null) + { + throw new ArgumentNullException(nameof(appiumDriver), "Appium driver cannot be null"); + } + + // Check that at least one field is provided + if (string.IsNullOrEmpty(host) && string.IsNullOrEmpty(port) && string.IsNullOrEmpty(appName)) + { + throw new ArgumentException("At least one of 'host', 'port', or 'appName' must be provided"); + } + + // Validate connection data + if (!string.IsNullOrEmpty(host) && Uri.CheckHostName(host) == UriHostNameType.Unknown) + { + throw new ArgumentException($"Invalid host: {host}. The host should be a valid host."); + } + + if (!string.IsNullOrEmpty(port)) + { + if (!int.TryParse(port, out int portInt) || portInt <= 0 || portInt > 65535) + { + throw new ArgumentException($"Invalid port: {port}. The port number should be between 1 and 65535."); + } + } + + if (!platform.Equals("Android", StringComparison.OrdinalIgnoreCase) && + !platform.Equals("iOS", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"Unsupported platform: {platform}. Supported platforms are 'Android' and 'iOS'."); + } + + try + { + // Set XPath based on platform + string hostXPath = platform.Equals("iOS", StringComparison.OrdinalIgnoreCase) + ? "//XCUIElementTypeTextField[@value=\"Host\"]" + : "//android.widget.EditText[@text=\"Host\"]"; + + string portXPath = platform.Equals("iOS", StringComparison.OrdinalIgnoreCase) + ? "//XCUIElementTypeTextField[@value=\"Port\"]" + : "//android.widget.EditText[@text=\"Port\"]"; + + string appNameXPath = platform.Equals("iOS", StringComparison.OrdinalIgnoreCase) + ? "//XCUIElementTypeTextField[@value=\"App Name\"]" + : "//android.widget.EditText[@text=\"App Name\"]"; + + string okButtonXPath = platform.Equals("iOS", StringComparison.OrdinalIgnoreCase) + ? "//XCUIElementTypeButton[@name=\"OK\"]" + : "//android.widget.Button[@resource-id=\"android:id/button1\"]"; + + // Wait for the connection dialog to be present + var wait = new WebDriverWait(appiumDriver, TimeSpan.FromSeconds(timeout)); + wait.Until(ExpectedConditions.ElementExists(OpenQA.Selenium.By.XPath(hostXPath))); + + // Update host if provided + if (!string.IsNullOrEmpty(host)) + { + var hostField = appiumDriver.FindElement(OpenQA.Selenium.By.XPath(hostXPath)); + hostField.Clear(); + hostField.SendKeys(host); + } + + // Update port if provided + if (!string.IsNullOrEmpty(port)) + { + var portField = appiumDriver.FindElement(OpenQA.Selenium.By.XPath(portXPath)); + portField.Clear(); + portField.SendKeys(port); + } + + // Update app name if provided + if (!string.IsNullOrEmpty(appName)) + { + var appNameField = appiumDriver.FindElement(OpenQA.Selenium.By.XPath(appNameXPath)); + appNameField.Clear(); + appNameField.SendKeys(appName); + } + + // Press OK button + var okButton = appiumDriver.FindElement(OpenQA.Selenium.By.XPath(okButtonXPath)); + okButton.Click(); + } + catch (ArgumentException) + { + throw; + } + catch (Exception ex) + { + throw new AppiumHelperException($"Error while setting connection data on {platform} platform: {ex.Message}", ex); + } + } + + /// + /// Sets connection data in the native popup dialog with integer port + /// + /// The Appium driver instance + /// The platform (Android or iOS) + /// The host value to set. If not provided, the host field won't be updated + /// The port value to set. If not provided, the port field won't be updated + /// The app name value to set. If not provided, the app name field won't be updated + /// Timeout in seconds for waiting for elements (default: 60) + public static void SetConnectionData(AppiumDriver appiumDriver, string platform, string host = null, int? port = null, string appName = null, int timeout = 60) + { + SetConnectionData(appiumDriver, platform, host, port?.ToString(), appName, timeout); + } + } + + /// + /// Exception thrown when an error occurs while using AltAppiumHelper + /// + public class AppiumHelperException : Exception + { + public AppiumHelperException(string message) : base(message) + { + } + + public AppiumHelperException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/Bindings~/dotnet/AltDriver/AltDriver.csproj b/Bindings~/dotnet/AltDriver/AltDriver.csproj index 05f9cdba3..02a4760f4 100644 --- a/Bindings~/dotnet/AltDriver/AltDriver.csproj +++ b/Bindings~/dotnet/AltDriver/AltDriver.csproj @@ -31,5 +31,7 @@ + + diff --git a/Bindings~/python/alttester/appium_helper.py b/Bindings~/python/alttester/appium_helper.py index 2a3d2796d..c611ba8b7 100644 --- a/Bindings~/python/alttester/appium_helper.py +++ b/Bindings~/python/alttester/appium_helper.py @@ -24,7 +24,7 @@ class AltAppiumHelper: - """API to interact with native popups using Appium""" + """API to interact with the native popup dialog (implemented by AltTester) using Appium""" @staticmethod def _is_valid_host(host): @@ -58,6 +58,7 @@ def set_connection_data(appium_driver, platform, host=None, port=None, app_name= timeout (int): Timeout in seconds for waiting for elements (default: 60) Raises: + ValueError: If appium_driver is None, if none of the parameters are provided, if host is invalid, if port is out of range, or if platform is unsupported Exception: When an error occurs while interacting with the popup """ if appium_driver is None: From 373a5bbf8877848a929edd780b849afb88ce7efb Mon Sep 17 00:00:00 2001 From: "andrei.on1ca" Date: Fri, 6 Feb 2026 09:32:22 +0200 Subject: [PATCH 4/4] Wraped class to work outside Unity Editor --- Assets/AltTester/Runtime/AltDriver/AltAppiumHelper.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Assets/AltTester/Runtime/AltDriver/AltAppiumHelper.cs b/Assets/AltTester/Runtime/AltDriver/AltAppiumHelper.cs index 8d07dde2a..a0bda59cb 100644 --- a/Assets/AltTester/Runtime/AltDriver/AltAppiumHelper.cs +++ b/Assets/AltTester/Runtime/AltDriver/AltAppiumHelper.cs @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +#if !UNITY_EDITOR using System; using OpenQA.Selenium; using OpenQA.Selenium.Appium; @@ -164,3 +165,4 @@ public AppiumHelperException(string message, Exception innerException) : base(me } } } +#endif