Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions Assets/AltTester/Runtime/AltDriver/AltAppiumHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
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 <https://www.gnu.org/licenses/>.
*/

#if !UNITY_EDITOR
using System;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Support.UI;
using SeleniumExtras.WaitHelpers;

namespace AltTester.AltTesterSDK.Driver
{
/// <summary>
/// API to interact with the native popup dialog (implemented by AltTester) using Appium
/// </summary>
public class AltAppiumHelper
{
/// <summary>
/// Sets connection data in the native popup dialog
/// </summary>
/// <param name="appiumDriver">The Appium driver instance</param>
/// <param name="platform">The platform (Android or iOS)</param>
/// <param name="host">The host value to set. If not provided, the host field won't be updated</param>
/// <param name="port">The port value to set. If not provided, the port field won't be updated</param>
/// <param name="appName">The app name value to set. If not provided, the app name field won't be updated</param>
/// <param name="timeout">Timeout in seconds for waiting for elements (default: 60)</param>
/// <exception cref="ArgumentNullException">Thrown when appiumDriver is null</exception>
/// <exception cref="ArgumentException">Thrown when no fields are provided for update</exception>
/// <exception cref="ArgumentException">Thrown when host or port is invalid</exception>
/// <exception cref="AppiumHelperException">Thrown when an error occurs while interacting with the popup</exception>
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);
}
}

/// <summary>
/// Sets connection data in the native popup dialog with integer port
/// </summary>
/// <param name="appiumDriver">The Appium driver instance</param>
/// <param name="platform">The platform (Android or iOS)</param>
/// <param name="host">The host value to set. If not provided, the host field won't be updated</param>
/// <param name="port">The port value to set. If not provided, the port field won't be updated</param>
/// <param name="appName">The app name value to set. If not provided, the app name field won't be updated</param>
/// <param name="timeout">Timeout in seconds for waiting for elements (default: 60)</param>
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);
}
}

/// <summary>
/// Exception thrown when an error occurs while using AltAppiumHelper
/// </summary>
public class AppiumHelperException : Exception
{
public AppiumHelperException(string message) : base(message)
{
}

public AppiumHelperException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
#endif
2 changes: 2 additions & 0 deletions Bindings~/dotnet/AltDriver/AltDriver.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="4.7.9" />
<PackageReference Include="AltWebSocketSharp" Version="1.0.9" />
<PackageReference Include="Appium.WebDriver" Version="8.0.1" />
<PackageReference Include="DotNetSeleniumExtras.WaitHelpers" Version="3.11.0" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions Bindings~/python/alttester/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
124 changes: 124 additions & 0 deletions Bindings~/python/alttester/appium_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""
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 <https://www.gnu.org/licenses/>.
"""

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 the native popup dialog (implemented by AltTester) 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}(?<!-)(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-))*\.?$'
return re.match(hostname_pattern, host) is not None

@staticmethod
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, 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:
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:
raise ValueError("Appium driver cannot be None")

# 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 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_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
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)))

# 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)

# 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)

# 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:
raise Exception(f"Error while setting connection data on {platform} platform: {str(ex)}") from ex
1 change: 0 additions & 1 deletion Bindings~/python/alttester/reverse_port_forwarding.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading