Mobile Automation Patterns
This comprehensive guide covers mobile application automation using Ranorex, including native apps, hybrid applications, mobile web, and cross-platform scenarios for both iOS and Android.
📱 Mobile Platform Coverage
| Platform | Description | Key Features |
|---|---|---|
| Android Native | Native Android applications | Activities, Views, Intents |
| iOS Native | Native iOS applications | ViewControllers, UI elements |
| Hybrid Apps | Cordova, PhoneGap, Ionic | WebView + native components |
| Mobile Web | Browser-based mobile apps | Responsive design, touch events |
| Cross-Platform | React Native, Xamarin, Flutter | Shared codebase automation |
Android Native
Basic Android App Automation
using Ranorex;
using Ranorex.Plugin.Mobile;
using Ranorex.Core.Remoting;
using System;
using System.Collections.Generic;
using System.Linq;
public class AndroidNativeAutomation
{
private AndroidApp _currentApp;
private string _appPackageName;
private IRemoteEndpoint _endpoint;
public void AutomateAndroidApp(string appPackageName)
{
try
{
_appPackageName = appPackageName;
// Connect to Android device and application
ConnectToAndroidApp();
// Perform Android-specific interactions
PerformAndroidInteractions();
// Handle Android system features
HandleAndroidFeatures();
Report.Success("Android Automation", "Successfully automated Android application");
}
catch (Exception ex)
{
Report.Failure("Android Automation", $"Failed: {ex.Message}");
throw;
}
finally
{
CleanupAndroidApp();
}
}
private void ConnectToAndroidApp()
{
try
{
// Discover Android devices
var discoveredDevices = DiscoverAndroidDevices();
if (discoveredDevices == null || !discoveredDevices.Any())
{
throw new Exception("No Android devices found");
}
// Connect to first available device
var deviceInfo = discoveredDevices.First();
_endpoint = ConnectToDevice(deviceInfo);
// Start the application
_endpoint.StartApplication(_appPackageName, true);
// Connect to the Android app using mobile app path
string appPath = $"/mobileapp[@title='{_appPackageName}']";
_currentApp = AndroidApp.FromPath(appPath);
if (_currentApp != null && _currentApp.Element.Valid)
{
var deviceInfo = _currentApp.GetDeviceInfo();
Report.Info($"Connected to: {deviceInfo.Manufacturer} {deviceInfo.Brand} running Android {deviceInfo.AndroidVersionName}");
Report.Success("App Connection", $"Successfully connected to Android app: {_appPackageName}");
}
else
{
throw new Exception($"Could not connect to Android application: {_appPackageName}");
}
}
catch (Exception ex)
{
throw new Exception($"Failed to connect to Android app: {ex.Message}");
}
}
private IList<IRemoteEndpointInformation> DiscoverAndroidDevices()
{
try
{
var endpointService = RemoteServiceLocator.Service;
var discoveryFactory = endpointService.Resolve<IDeviceDiscoveryServiceFactory>();
var discoveryService = discoveryFactory.Create(RemotePlatform.Android, RemoteConnectionType.USB);
return discoveryService.Discover(new Duration(15000));
}
catch (Exception ex)
{
Report.Warn($"Device discovery failed: {ex.Message}");
return null;
}
}
private IRemoteEndpoint ConnectToDevice(IRemoteEndpointInformation deviceInfo)
{
var endpointService = RemoteServiceLocator.Service;
var deviceSerial = deviceInfo.DisplayName;
// Clean device name if needed
int parenthesisIndex = deviceSerial.IndexOf(" (", StringComparison.Ordinal);
if (parenthesisIndex > 0)
{
deviceSerial = deviceSerial.Substring(0, parenthesisIndex).Trim();
}
var endpoint = endpointService.AddDevice($"AndroidDevice-{deviceSerial}",
RemotePlatform.Android, RemoteConnectionType.USB, deviceSerial);
// Wait for device connection
int maxWaitChecks = 10;
for (int waitCheck = 0; waitCheck < maxWaitChecks; waitCheck++)
{
System.Threading.Thread.Sleep(1000);
if (endpoint.Status == ChannelState.DeviceConnected || endpoint.Status == ChannelState.Connected)
{
return endpoint;
}
else if (endpoint.Status == ChannelState.Error)
{
throw new Exception("Failed to connect to Android device");
}
}
throw new Exception("Timeout waiting for Android device connection");
}
private void PerformAndroidInteractions()
{
// Handle text input
HandleAndroidTextInput();
// Handle button interactions
HandleAndroidButtons();
// Handle list interactions with swipe gestures
HandleAndroidListInteractions();
// Handle navigation
HandleAndroidNavigation();
}
private void HandleAndroidTextInput()
{
try
{
// Find Android text fields using mobile-specific selectors
var textFields = _currentApp.Find<AndroidElement>("//androidelement[@class='android.widget.EditText']");
foreach (var textField in textFields.Take(2)) // Take first 2 text fields
{
if (textField.Valid && textField.Visible)
{
textField.Click();
Delay.Milliseconds(500);
// Clear existing text and enter new text
textField.PressKeys("{Control down}a{Control up}");
textField.PressKeys("test input");
Report.Info("Text Input", "Successfully filled Android text field");
}
}
// Hide virtual keyboard by pressing back
_currentApp.PressBackKey();
}
catch (Exception ex)
{
Report.Debug("Text Input", $"Text input handling: {ex.Message}");
}
}
private void HandleAndroidButtons()
{
try
{
// Find buttons by content description
var buttons = _currentApp.Find<AndroidElement>("//androidelement[@class='android.widget.Button']");
if (buttons.Any())
{
var firstButton = buttons.First();
if (firstButton.Valid && firstButton.Visible)
{
firstButton.Click();
Delay.Milliseconds(1000);
Report.Success("Button Interaction", "Successfully clicked Android button");
}
}
// Try to find buttons by content description
var menuButton = _currentApp.FindSingle<AndroidElement>("//androidelement[@contentdescription='Menu']", new Duration(3000));
if (menuButton.Valid)
{
menuButton.Click();
Report.Success("Menu Button", "Successfully clicked menu button");
Delay.Milliseconds(1000);
// Press back to close menu
_currentApp.PressBackKey();
}
}
catch (Exception ex)
{
Report.Debug("Button Interaction", $"Button handling: {ex.Message}");
}
}
private void HandleAndroidListInteractions()
{
try
{
// Find scrollable list elements
var scrollableElements = _currentApp.Find<AndroidElement>(
"//androidelement[@contentdescription='Scrollview manages views in given screen size' or @class='androidx.recyclerview.widget.RecyclerView' or @class='android.widget.ListView']");
foreach (var listElement in scrollableElements)
{
if (listElement.Valid && listElement.Visible)
{
Report.Info("List Interaction", "Found scrollable list element");
// Perform swipe gestures using mobile-specific API
listElement.Swipe(Ranorex.Core.Recorder.Touch.GestureDirection.Up, new Distance(200));
Delay.Milliseconds(500);
listElement.Swipe(Ranorex.Core.Recorder.Touch.GestureDirection.Down, new Distance(200));
Delay.Milliseconds(500);
Report.Success("List Interaction", "Successfully performed swipe gestures on list");
break; // Only interact with first found list
}
}
}
catch (Exception ex)
{
Report.Debug("List Interaction", $"List interaction: {ex.Message}");
}
}
private void HandleAndroidNavigation()
{
try
{
// Use Android hardware back key
_currentApp.PressBackKey();
Report.Success("Navigation", "Used Android hardware back key");
Delay.Milliseconds(1000);
// Find and use navigation drawer if available
var drawerToggle = _currentApp.FindSingle<AndroidElement>(
"//androidelement[@contentdescription='Navigate up' or @contentdescription='Open navigation drawer']",
new Duration(3000));
if (drawerToggle.Valid)
{
drawerToggle.Click();
Delay.Milliseconds(1000);
// Check if drawer opened
var drawerMenu = _currentApp.FindSingle<AndroidElement>("//androidelement[@rid='drawerMenu']", new Duration(3000));
if (drawerMenu.Valid && drawerMenu.Visible)
{
Report.Success("Navigation Drawer", "Successfully opened navigation drawer");
// Close drawer by pressing back
_currentApp.PressBackKey();
}
}
}
catch (Exception ex)
{
Report.Debug("Navigation", $"Navigation handling: {ex.Message}");
}
}
private void HandleAndroidFeatures()
{
// Handle Android-specific permissions
HandleAndroidPermissions();
// Handle Android notifications
HandleAndroidNotifications();
// Handle Android intents and system dialogs
HandleAndroidSystemDialogs();
}
private void HandleAndroidPermissions()
{
try
{
// Look for common permission dialogs
var allowButton = _currentApp.FindSingle<AndroidElement>(
"//androidelement[@text='Allow' or @text='ALLOW' or @text='Grant']", new Duration(3000));
if (allowButton.Valid && allowButton.Visible)
{
allowButton.Click();
Report.Success("Permissions", "Granted Android app permission");
Delay.Milliseconds(1000);
}
var denyButton = _currentApp.FindSingle<AndroidElement>(
"//androidelement[@text='Deny' or @text='DENY']", new Duration(2000));
if (denyButton.Valid && denyButton.Visible)
{
Report.Info("Permissions", "Permission denial option available");
}
}
catch (Exception ex)
{
Report.Debug("Permissions", $"No permission dialogs found: {ex.Message}");
}
}
private void HandleAndroidNotifications()
{
try
{
// Try to access notification panel (this would typically require system-level access)
Report.Info("Notifications", "Android notification handling would require system-level automation");
// Look for in-app notifications
var notifications = _currentApp.Find<AndroidElement>("//androidelement[contains(@text,'notification') or contains(@contentdescription,'notification')]");
if (notifications.Any())
{
var firstNotification = notifications.First();
if (firstNotification.Valid && firstNotification.Visible)
{
firstNotification.Click();
Report.Success("Notifications", "Interacted with in-app notification");
}
}
}
catch (Exception ex)
{
Report.Debug("Notifications", $"Notification handling: {ex.Message}");
}
}
private void HandleAndroidSystemDialogs()
{
try
{
// Handle system dialogs like "App not responding"
var dialogButtons = _currentApp.Find<AndroidElement>("//androidelement[@class='android.widget.Button' and (@text='OK' or @text='Cancel' or @text='Wait')]");
foreach (var button in dialogButtons)
{
if (button.Valid && button.Visible)
{
var buttonText = button.GetAttributeValueText("text");
Report.Info("System Dialog", $"Found system dialog button: {buttonText}");
// Handle as appropriate for your test scenario
if (buttonText == "OK" || buttonText == "Wait")
{
button.Click();
Report.Info("System Dialog", $"Clicked {buttonText} button");
}
}
}
}
catch (Exception ex)
{
Report.Debug("System Dialogs", $"System dialog handling: {ex.Message}");
}
}
private void CleanupAndroidApp()
{
try
{
if (_currentApp != null)
{
_currentApp.CloseApplication();
Report.Info("Cleanup", "Android application closed");
}
if (_endpoint != null)
{
_endpoint.DisconnectAndDisable();
Report.Info("Cleanup", "Android device endpoint disconnected");
}
}
catch (Exception ex)
{
Report.Warn("Cleanup", $"Cleanup issues: {ex.Message}");
}
}
}
📱 App Instrumentation: For mobile automation, you can use pre-instrumented applications to avoid manual instrumentation. Learn more about instrumenting apps for seamless automation setup.
iOS Native
iOS Application Automation
using Ranorex;
using Ranorex.Plugin.Mobile;
using System;
using System.Collections.Generic;
public class iOSNativeAutomation
{
private MobileApp _currentApp;
private string _appTitle;
private static readonly TestCredential[] TestCredentials = new[]
{
new TestCredential
{
Title = "WordPressDemo",
Username = "testuser1",
Password = "password123",
URL = "https://wordpress.com"
},
new TestCredential
{
Title = "GitHubTest",
Username = "developer",
Password = "secure456",
URL = "https://github.com"
}
};
public void AutomateiOSApp(string appTitle)
{
try
{
_appTitle = appTitle;
// Connect to iOS application
ConnectToiOSApp();
// Perform iOS automation test scenarios
RuniOSTestScenarios();
Report.Success("iOS Automation", "Successfully automated iOS application");
}
catch (Exception ex)
{
Report.Failure("iOS Automation", $"Failed: {ex.Message}");
throw;
}
finally
{
CleanupApplication();
}
}
private void ConnectToiOSApp()
{
try
{
Report.Info($"Connecting to iOS app: {_appTitle}");
// Connect to the iOS app using mobile app path
string appPath = $"/mobileapp[@title='{_appTitle}']";
_currentApp = MobileApp.FromPath(appPath);
if (_currentApp != null && _currentApp.Element.Valid)
{
// Handle initial permissions and setup
HandleInitialSetup();
Report.Success("iOS Connection", $"Successfully connected to iOS app: {_appTitle}");
}
else
{
throw new Exception($"Could not connect to iOS application: {_appTitle}");
}
}
catch (Exception ex)
{
throw new Exception($"Failed to connect to iOS app: {ex.Message}");
}
}
private void HandleInitialSetup()
{
try
{
// Handle iOS permission dialogs
HandleiOSPermissions();
// Wait for app initialization
Delay.Milliseconds(1000);
Report.Info("iOS Setup", "Initial setup completed");
}
catch (Exception ex)
{
Report.Warn("iOS Setup", $"Setup issues: {ex.Message}");
}
}
private void HandleiOSPermissions()
{
try
{
// Handle location permission
var allowLocationButton = _currentApp.FindSingle<Button>("//*[@name='Allow While Using App']", new Duration(3000));
if (allowLocationButton.Valid)
{
allowLocationButton.Click();
Report.Info("iOS Permissions", "Granted location permission");
Delay.Milliseconds(1000);
}
// Handle notification permission
var allowNotificationsButton = _currentApp.FindSingle<Button>("//*[@name='Allow']", new Duration(3000));
if (allowNotificationsButton.Valid)
{
allowNotificationsButton.Click();
Report.Info("iOS Permissions", "Granted notification permission");
Delay.Milliseconds(1000);
}
// Handle additional permissions (camera, microphone, etc.)
var okButton = _currentApp.FindSingle<Button>("//*[@name='OK']", new Duration(2000));
if (okButton.Valid)
{
okButton.Click();
Report.Info("iOS Permissions", "Handled additional permission");
}
}
catch (Exception ex)
{
Report.Debug("iOS Permissions", $"No permission dialogs found: {ex.Message}");
}
}
private void RuniOSTestScenarios()
{
// Run comprehensive iOS test scenarios similar to the KeePass example
RunCreateNewDatabaseTest();
RunAddNewEntryTests();
RunAddNewGroupTests();
}
private void RunCreateNewDatabaseTest()
{
Report.Info("=== iOS Create Database Test ===");
try
{
// Simulate login or database creation process
SimulateLoginProcess();
Report.Success("Create Database", "iOS database creation test completed");
}
catch (Exception ex)
{
Report.Error($"Create Database test failed: {ex.Message}");
throw;
}
}
private void RunAddNewEntryTests()
{
Report.Info("=== iOS Add Entry Tests ===");
try
{
foreach (var credential in TestCredentials)
{
// Add entry using iOS-specific interactions
AddEntryiOS(credential);
// Validate entry was added
ValidateEntryiOS(credential.Title);
// Clean up entry
DeleteEntryiOS(credential.Title);
}
Report.Success("Add Entry", "iOS entry tests completed");
}
catch (Exception ex)
{
Report.Error($"Add Entry tests failed: {ex.Message}");
throw;
}
}
private void RunAddNewGroupTests()
{
Report.Info("=== iOS Add Group Tests ===");
try
{
string testGroup = "TestGroup";
// Add group using iOS-specific interactions
AddGroupiOS(testGroup);
// Validate group was added
ValidateGroupiOS(testGroup);
// Clean up group
DeleteGroupiOS(testGroup);
Report.Success("Add Group", "iOS group tests completed");
}
catch (Exception ex)
{
Report.Error($"Add Group tests failed: {ex.Message}");
throw;
}
}
private void SimulateLoginProcess()
{
try
{
Report.Info("Performing iOS login process");
// Find and interact with login elements using iOS-specific selectors
var usernameField = _currentApp.FindSingle<TextField>("//textfield[@name='username' or @placeholder='Username']", new Duration(5000));
if (usernameField.Valid)
{
usernameField.Click();
Delay.Milliseconds(500);
usernameField.PressKeys("testuser");
}
var passwordField = _currentApp.FindSingle<TextField>("//textfield[@name='password' or @placeholder='Password']", new Duration(3000));
if (passwordField.Valid)
{
passwordField.Click();
Delay.Milliseconds(500);
passwordField.PressKeys("password123");
}
// Dismiss keyboard by tapping outside
DismissiOSKeyboard();
var loginButton = _currentApp.FindSingle<Button>("//button[@name='Login' or @name='Sign In']", new Duration(3000));
if (loginButton.Valid)
{
loginButton.Click();
Delay.Milliseconds(2000);
}
Report.Info("iOS Login", "Login process completed");
}
catch (Exception ex)
{
Report.Debug("iOS Login", $"Login simulation: {ex.Message}");
}
}
private void AddEntryiOS(TestCredential credential)
{
Report.Info($"Adding iOS entry: {credential.Title}");
try
{
// Navigate to add entry screen
NavigateToAddEntryiOS();
// Fill entry details using iOS UI elements
FillEntryDetailsiOS(credential);
// Save entry
SaveEntryiOS();
Report.Success("Add Entry", $"iOS entry '{credential.Title}' added successfully");
}
catch (Exception ex)
{
Report.Error($"Failed to add iOS entry: {ex.Message}");
throw;
}
}
private void NavigateToAddEntryiOS()
{
try
{
// Look for Add/Plus button in navigation bar
var addButton = _currentApp.FindSingle<Button>("//button[@name='Add' or @name='+' or contains(@name,'add')]", new Duration(5000));
if (addButton.Valid)
{
addButton.Click();
Delay.Milliseconds(500);
}
// Select Entry option if multiple options available
var newEntryOption = _currentApp.FindSingle<Button>("//button[@name='New Entry' or @name='Entry']", new Duration(3000));
if (newEntryOption.Valid)
{
newEntryOption.Click();
Delay.Milliseconds(500);
}
Report.Info("iOS Navigation", "Navigated to add entry screen");
}
catch (Exception ex)
{
Report.Debug("iOS Navigation", $"Navigation: {ex.Message}");
}
}
private void FillEntryDetailsiOS(TestCredential credential)
{
try
{
// Fill title field
var titleField = _currentApp.FindSingle<TextField>("//textfield[@name='Title' or @placeholder='Title']", new Duration(3000));
if (titleField.Valid)
{
titleField.Click();
titleField.PressKeys(credential.Title);
Delay.Milliseconds(300);
}
// Fill username field
var usernameField = _currentApp.FindSingle<TextField>("//textfield[@name='Username' or @placeholder='Username']", new Duration(3000));
if (usernameField.Valid)
{
usernameField.Click();
usernameField.PressKeys(credential.Username);
Delay.Milliseconds(300);
}
// Fill password field
var passwordField = _currentApp.FindSingle<TextField>("//textfield[@name='Password' or @placeholder='Password']", new Duration(3000));
if (passwordField.Valid)
{
passwordField.Click();
passwordField.PressKeys(credential.Password);
Delay.Milliseconds(300);
}
// Fill URL field if available
var urlField = _currentApp.FindSingle<TextField>("//textfield[@name='URL' or @placeholder='URL']", new Duration(2000));
if (urlField.Valid)
{
urlField.Click();
urlField.PressKeys(credential.URL);
Delay.Milliseconds(300);
}
// Dismiss keyboard
DismissiOSKeyboard();
Report.Info("iOS Form", "Entry details filled");
}
catch (Exception ex)
{
Report.Debug("iOS Form", $"Form filling: {ex.Message}");
}
}
private void SaveEntryiOS()
{
try
{
// Look for Done/Save button
var saveButton = _currentApp.FindSingle<Button>("//button[@name='Done' or @name='Save']", new Duration(3000));
if (saveButton.Valid)
{
saveButton.Click();
Delay.Milliseconds(1000);
Report.Info("iOS Save", "Entry saved successfully");
}
else
{
Report.Warn("iOS Save", "Save button not found");
}
}
catch (Exception ex)
{
Report.Debug("iOS Save", $"Save operation: {ex.Message}");
}
}
private void ValidateEntryiOS(string title)
{
Report.Info($"Validating iOS entry: {title}");
try
{
// Search for the entry in the list
if (FindEntryInListiOS(title))
{
Report.Success("Validation", $"iOS entry '{title}' found and validated");
}
else
{
throw new Exception($"iOS entry '{title}' not found during validation");
}
}
catch (Exception ex)
{
Report.Error($"iOS entry validation failed: {ex.Message}");
throw;
}
}
private bool FindEntryInListiOS(string title)
{
try
{
// Look for the entry in table view or collection view
var entryElement = _currentApp.FindSingle<Cell>($"//cell[contains(@name,'{title}')]", new Duration(5000));
if (entryElement.Valid)
{
Report.Info("iOS Search", $"Found entry '{title}' in list");
return true;
}
// Try alternative search methods
var textElement = _currentApp.FindSingle<Text>($"//text[contains(@name,'{title}')]", new Duration(3000));
if (textElement.Valid)
{
Report.Info("iOS Search", $"Found entry '{title}' as text element");
return true;
}
return false;
}
catch (Exception ex)
{
Report.Debug("iOS Search", $"Search error: {ex.Message}");
return false;
}
}
private void DeleteEntryiOS(string title)
{
Report.Info($"Deleting iOS entry: {title}");
try
{
if (FindEntryInListiOS(title))
{
// Long press or swipe to reveal delete option
var entryElement = _currentApp.FindSingle<Cell>($"//cell[contains(@name,'{title}')]", new Duration(5000));
if (entryElement.Valid)
{
// Try swipe to delete (iOS pattern)
entryElement.Swipe(Ranorex.Core.Recorder.Touch.GestureDirection.Left, new Distance(100));
Delay.Milliseconds(500);
// Look for delete button
var deleteButton = _currentApp.FindSingle<Button>("//button[@name='Delete' or contains(@name,'delete')]", new Duration(3000));
if (deleteButton.Valid)
{
deleteButton.Click();
// Confirm deletion if needed
var confirmButton = _currentApp.FindSingle<Button>("//button[@name='Delete' or @name='Confirm']", new Duration(2000));
if (confirmButton.Valid)
{
confirmButton.Click();
}
Delay.Milliseconds(1000);
Report.Success("Delete Entry", $"iOS entry '{title}' deleted successfully");
}
}
}
}
catch (Exception ex)
{
Report.Error($"Failed to delete iOS entry: {ex.Message}");
throw;
}
}
private void AddGroupiOS(string groupName)
{
Report.Info($"Adding iOS group: {groupName}");
try
{
// Navigate to add group
var addButton = _currentApp.FindSingle<Button>("//button[@name='Add' or @name='+']", new Duration(5000));
if (addButton.Valid)
{
addButton.Click();
Delay.Milliseconds(500);
}
// Select Group option
var newGroupOption = _currentApp.FindSingle<Button>("//button[@name='New Group' or @name='Group']", new Duration(3000));
if (newGroupOption.Valid)
{
newGroupOption.Click();
Delay.Milliseconds(500);
}
// Enter group name
var groupNameField = _currentApp.FindSingle<TextField>("//textfield[@placeholder='Group Name' or @name='Name']", new Duration(3000));
if (groupNameField.Valid)
{
groupNameField.Click();
groupNameField.PressKeys(groupName);
Delay.Milliseconds(500);
}
// Save group
var saveButton = _currentApp.FindSingle<Button>("//button[@name='Done' or @name='Save']", new Duration(3000));
if (saveButton.Valid)
{
saveButton.Click();
Delay.Milliseconds(1000);
}
// Dismiss keyboard if needed
DismissiOSKeyboard();
Report.Success("Add Group", $"iOS group '{groupName}' added successfully");
}
catch (Exception ex)
{
Report.Error($"Failed to add iOS group: {ex.Message}");
throw;
}
}
private void ValidateGroupiOS(string groupName)
{
Report.Info($"Validating iOS group: {groupName}");
try
{
if (FindGroupInListiOS(groupName))
{
Report.Success("Validation", $"iOS group '{groupName}' found and validated");
}
else
{
throw new Exception($"iOS group '{groupName}' not found during validation");
}
}
catch (Exception ex)
{
Report.Error($"iOS group validation failed: {ex.Message}");
throw;
}
}
private bool FindGroupInListiOS(string groupName)
{
try
{
// Look for the group in list
var groupElement = _currentApp.FindSingle<Cell>($"//cell[contains(@name,'{groupName}')]", new Duration(5000));
if (groupElement.Valid)
{
Report.Info("iOS Search", $"Found group '{groupName}' in list");
return true;
}
return false;
}
catch (Exception ex)
{
Report.Debug("iOS Search", $"Group search error: {ex.Message}");
return false;
}
}
private void DeleteGroupiOS(string groupName)
{
Report.Info($"Deleting iOS group: {groupName}");
try
{
if (FindGroupInListiOS(groupName))
{
// Similar to entry deletion process
var groupElement = _currentApp.FindSingle<Cell>($"//cell[contains(@name,'{groupName}')]", new Duration(5000));
if (groupElement.Valid)
{
groupElement.Swipe(Ranorex.Core.Recorder.Touch.GestureDirection.Left, new Distance(100));
Delay.Milliseconds(500);
var deleteButton = _currentApp.FindSingle<Button>("//button[@name='Delete']", new Duration(3000));
if (deleteButton.Valid)
{
deleteButton.Click();
Delay.Milliseconds(1000);
Report.Success("Delete Group", $"iOS group '{groupName}' deleted successfully");
}
}
}
}
catch (Exception ex)
{
Report.Error($"Failed to delete iOS group: {ex.Message}");
throw;
}
}
private void DismissiOSKeyboard()
{
try
{
// Tap outside text fields to dismiss keyboard
var appElement = _currentApp.Element;
if (appElement.Valid)
{
appElement.Click(new Location(100, 100)); // Tap in empty area
Delay.Milliseconds(500);
}
}
catch (Exception ex)
{
Report.Debug("iOS Keyboard", $"Keyboard dismissal: {ex.Message}");
}
}
private void CleanupApplication()
{
try
{
if (_currentApp != null)
{
Report.Info("iOS Cleanup", "Closing iOS application");
// Clean up would typically involve closing the app gracefully
Delay.Milliseconds(500);
Report.Info("Cleanup", "iOS application cleanup completed");
}
}
catch (Exception ex)
{
Report.Warn("Cleanup", $"iOS cleanup issues: {ex.Message}");
}
}
// Data structure for test credentials
public class TestCredential
{
public string Title { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string URL { get; set; }
}
}
📱 App Instrumentation: For mobile automation, you can use pre-instrumented applications to avoid manual instrumentation. Learn more about instrumenting apps for seamless automation setup.
Hybrid Apps
Hybrid Application Automation
using Ranorex;
using System;
using System.Threading;
public class HybridAppAutomation
{
private string _appTitle;
private string _processName;
public void AutomateHybridApp(string appTitle, string processName = null)
{
Host.Initialize();
try
{
_appTitle = appTitle;
_processName = processName ?? "HybridApp";
// Verify touch support
VerifyTouchSupport();
// Connect to hybrid application
ConnectToHybridApp();
// Handle native context
HandleNativeContext();
// Switch to web context
SwitchToWebContext();
// Handle web content
HandleWebContext();
// Switch back to native context
SwitchToNativeContext();
Report.Success("Hybrid App", "Successfully automated hybrid application");
}
catch (Exception ex)
{
Report.Failure("Hybrid App", $"Failed: {ex.Message}");
TakeScreenshot("HybridApp_Error");
throw;
}
finally
{
Host.Shutdown();
}
}
private void VerifyTouchSupport()
{
if (!Touch.IsSupported)
{
throw new InvalidOperationException("Touch input is not supported for hybrid app automation");
}
Report.Success("Touch Support", "Touch input is available");
}
private void ConnectToHybridApp()
{
try
{
// Wait for hybrid application window/form to be available
var appWindow = Host.Local.FindSingle<Form>($"/form[@processname='{_processName}']", 30000);
if (!appWindow.Exists)
{
appWindow = Host.Local.FindSingle<Form>($"//form[contains(@title,'{_appTitle}')]", 10000);
}
if (appWindow.Exists)
{
appWindow.Focus();
Thread.Sleep(1000);
// Wait for hybrid app to initialize
WaitForHybridAppReady(TimeSpan.FromSeconds(15));
Report.Success("Hybrid Connection", $"Connected to hybrid app: {_appTitle}");
}
else
{
throw new Exception($"Could not connect to hybrid application: {_appTitle}");
}
}
catch (Exception ex)
{
throw new Exception($"Failed to connect to hybrid app: {ex.Message}");
}
}
private void WaitForHybridAppReady(TimeSpan timeout)
{
// Wait for WebView to be available in hybrid app
var webView = Host.Local.FindSingle<WebDocument>("//webdocument | //webview", (int)timeout.TotalMilliseconds);
if (webView.Exists)
{
Report.Info("Hybrid Ready", "WebView detected - hybrid app ready");
}
else
{
Report.Warn("Hybrid Ready", "WebView not detected - app may be purely native");
}
}
private void HandleNativeContext()
{
try
{
// Interact with native elements first
var nativeButton = Host.Local.FindSingle<Button>("//*[@text='Open WebView' or @name='Open WebView']", 5000);
if (nativeButton.Exists)
{
nativeButton.Click();
// Wait for the webview to appear
Host.Local.FindSingle<WebDocument>("//webview", 5000);
}
// Handle native navigation elements
var toolbar = Host.Local.FindSingle<Container>("//*[@class='android.widget.Toolbar']", 3000);
if (toolbar.Exists)
{
Report.Info("Native Context", "Found native toolbar");
}
Report.Success("Native Context", "Successfully handled native elements");
}
catch (Exception ex)
{
Report.Debug("Native Context", $"Limited native elements: {ex.Message}");
}
}
private void SwitchToWebContext()
{
try
{
// Find the WebView container
var webView = Host.Local.FindSingle<WebDocument>("//*[@class='android.webkit.WebView']", 10000);
if (webView.Exists)
{
// Switch focus to web context
webView.Focus();
// Wait for web content to load
WaitForWebContentReady(webView);
Report.Success("Context Switch", "Successfully switched to web context");
}
else
{
throw new Exception("WebView not found");
}
}
catch (Exception ex)
{
throw new Exception($"Failed to switch to web context: {ex.Message}");
}
}
private void WaitForWebContentReady(WebDocument webView)
{
// Wait for JavaScript to be ready
var jsReady = WaitHelper.WaitForCondition(() =>
{
try
{
var result = webView.ExecuteScript("return document.readyState");
return result?.ToString() == "complete";
}
catch
{
return false;
}
}, "web content to load", TimeSpan.FromSeconds(20));
if (!jsReady)
{
throw new TimeoutException("Web content did not load in time");
}
}
private void HandleWebContext()
{
try
{
var webView = Host.Local.FindSingle<WebDocument>("//*[@class='android.webkit.WebView']");
// Handle web form elements
HandleWebForm(webView);
// Handle web navigation
HandleWebNavigation(webView);
// Interact with hybrid elements
HandleHybridElements(webView);
Report.Success("Web Context", "Successfully handled web content");
}
catch (Exception ex)
{
Report.Failure("Web Context", $"Failed: {ex.Message}");
throw;
}
}
private void HandleWebForm(WebDocument webView)
{
try
{
// Fill web form elements
var emailField = webView.FindSingle<InputTag>("//input[@type='email']");
emailField.Click();
emailField.PressKeys("test@example.com");
var passwordField = webView.FindSingle<InputTag>("//input[@type='password']");
passwordField.Click();
passwordField.PressKeys("password123");
// Submit form
var submitButton = webView.FindSingle<Button>("//button[@type='submit']");
submitButton.Click();
Report.Success("Web Form", "Successfully filled web form");
}
catch (Exception ex)
{
Report.Debug("Web Form", $"No web form found: {ex.Message}");
}
}
private void HandleWebNavigation(WebDocument webView)
{
try
{
// Click web navigation links
var navLink = webView.FindSingle<ATag>("//a[contains(@href, 'dashboard')]");
navLink.Click();
// Wait for the page to navigate
Host.Local.FindSingle<WebDocument>("/dom", 5000);
// Handle JavaScript navigation
webView.ExecuteScript("history.back()");
// Wait for the page to navigate back
Host.Local.FindSingle<WebDocument>("/dom", 5000);
Report.Success("Web Navigation", "Successfully handled web navigation");
}
catch (Exception ex)
{
Report.Debug("Web Navigation", $"No web navigation: {ex.Message}");
}
}
private void HandleHybridElements(WebDocument webView)
{
try
{
// Trigger native functionality from web
webView.ExecuteScript("window.cordova.plugins.camera.getPicture();");
// Wait for the native camera view to appear
// The path should be adapted to the application's UI structure.
Host.Local.FindSingle<Adapter>("/form", 5000);
Report.Info("Hybrid Elements", "Triggered native functionality from web");
}
catch (Exception ex)
{
Report.Debug("Hybrid Elements", $"No hybrid plugins available: {ex.Message}");
}
}
private void SwitchToNativeContext()
{
try
{
// Click outside WebView to return to native context
var appWindow = Host.Local.FindSingle<Form>("/form");
if (appWindow.Exists)
{
var bounds = appWindow.ScreenRectangle;
Touch.Tap(appWindow, new Location(bounds.Width / 2, 100));
Thread.Sleep(1000);
// Verify we're back in native context by looking for native elements
var nativeElement = Host.Local.FindSingle<Button>("//button", 3000);
if (nativeElement.Exists)
{
Report.Success("Context Switch", "Successfully returned to native context");
}
}
}
catch (Exception ex)
{
Report.Debug("Context Switch", $"Already in native context: {ex.Message}");
}
}
// Helper methods for hybrid automation
private void TakeScreenshot(string name)
{
try
{
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var adapter = Host.Local.FindSingle<Adapter>("/form");
Report.Screenshot(adapter, $"{name}_{timestamp}");
}
catch (Exception ex)
{
Report.Warn("Screenshot", $"Could not take screenshot: {ex.Message}");
}
}
}
Mobile Web
Mobile Browser Automation
using Ranorex;
using System;
using System.Threading;
public class MobileWebAutomation
{
private string _url;
private WebDocument _webDocument;
private string _browserProcess;
public void AutomateMobileWeb(string url, string browserProcess = "chrome")
{
Host.Initialize();
try
{
_url = url;
_browserProcess = browserProcess;
// Verify touch support
VerifyTouchSupport();
// Launch mobile browser
LaunchMobileBrowser();
// Handle mobile-specific web features
HandleMobileWebFeatures();
// Test responsive design
TestResponsiveDesign();
// Handle touch gestures on web
HandleMobileTouchGestures();
Report.Success("Mobile Web", "Successfully automated mobile web application");
}
catch (Exception ex)
{
Report.Failure("Mobile Web", $"Failed: {ex.Message}");
TakeScreenshot("MobileWeb_Error");
throw;
}
finally
{
Host.Shutdown();
}
}
private void VerifyTouchSupport()
{
if (!Touch.IsSupported)
{
throw new InvalidOperationException("Touch input is not supported for mobile web automation");
}
Report.Success("Touch Support", "Touch input is available");
}
private void LaunchMobileBrowser()
{
try
{
// Connect to browser process or launch manually
var browserWindow = Host.Local.FindSingle<Form>($"/form[@processname='{_browserProcess}']", 10000);
if (!browserWindow.Exists)
{
Report.Info("Browser Launch", $"Browser not found. Please manually launch {_browserProcess} and navigate to {_url}");
// Wait for browser to be available
browserWindow = Host.Local.FindSingle<Form>($"/form[@processname='{_browserProcess}']", 30000);
}
if (browserWindow.Exists)
{
browserWindow.Focus();
Thread.Sleep(2000);
// Try to navigate to URL if address bar is available
var addressBar = FindElementWithFallback(new[]
{
"//text[@name='Address and search bar']",
"//text[contains(@name,'address')]",
"//combobox[@name='Address and search bar']"
});
if (addressBar != null)
{
Touch.Tap(addressBar, Location.Center);
Thread.Sleep(500);
addressBar.PressKeys("{Control down}a{Control up}");
addressBar.PressKeys(_url);
addressBar.PressKeys("{Return}");
Thread.Sleep(3000);
}
// Wait for page to load and get WebDocument
_webDocument = Host.Local.FindSingle<WebDocument>("/dom", 15000);
if (_webDocument.Exists)
{
Report.Success("Mobile Browser", "Successfully connected to mobile browser");
}
else
{
throw new Exception("Could not find web document");
}
}
else
{
throw new Exception($"Could not connect to browser: {_browserProcess}");
}
}
catch (Exception ex)
{
throw new Exception($"Failed to launch mobile browser: {ex.Message}");
}
}
private void HandleMobileWebFeatures()
{
try
{
// Handle mobile viewport
HandleMobileViewport();
// Handle touch-optimized elements
HandleTouchOptimizedElements();
// Handle mobile-specific UI patterns
HandleMobileUIPatterns();
}
catch (Exception ex)
{
Report.Failure("Mobile Web Features", $"Failed: {ex.Message}");
throw;
}
}
private void HandleMobileViewport()
{
try
{
// Check viewport meta tag
var viewportMeta = _webDocument.ExecuteScript(
"return document.querySelector('meta[name=\"viewport\"]')?.content || 'none'");
Report.Info("Viewport", $"Viewport meta: {viewportMeta}");
// Check if page is mobile-optimized
var isMobileOptimized = _webDocument.ExecuteScript(
"return window.innerWidth <= 768 && document.querySelector('meta[name=\"viewport\"]') !== null");
if (isMobileOptimized != null && (bool)isMobileOptimized)
{
Report.Success("Mobile Optimization", "Page is mobile-optimized");
}
else
{
Report.Info("Mobile Optimization", "Page may not be fully mobile-optimized");
}
}
catch (Exception ex)
{
Report.Debug("Viewport", $"Could not check viewport: {ex.Message}");
}
}
private void HandleTouchOptimizedElements()
{
try
{
// Find and interact with touch-friendly buttons
var buttons = _webDocument.Find<Button>("//button | //input[@type='button'] | //a[contains(@class, 'btn')]");
foreach (var button in buttons)
{
// Check button size (should be at least 44px for good touch targets)
var buttonSize = _webDocument.ExecuteScript($@"
var btn = arguments[0];
var rect = btn.getBoundingClientRect();
return {{ width: rect.width, height: rect.height }};
", button);
Report.Info("Touch Target", $"Button size: {buttonSize}");
}
// Test hamburger menu (common mobile pattern)
var hamburgerMenu = _webDocument.FindSingle<Button>("//button[contains(@class, 'menu') or contains(@class, 'hamburger')]", 5000);
if (hamburgerMenu.Exists)
{
hamburgerMenu.Click();
// Wait for the menu to appear, for example by waiting for a menu item
_webDocument.FindSingle<DivTag>("//div[contains(@class, 'mobile-menu-item')]", 5000);
// Close menu
hamburgerMenu.Click();
Report.Success("Mobile Menu", "Successfully toggled mobile menu");
}
}
catch (Exception ex)
{
Report.Debug("Touch Elements", $"No touch-optimized elements found: {ex.Message}");
}
}
private void HandleMobileUIPatterns()
{
try
{
// Handle pull-to-refresh
HandlePullToRefresh();
// Handle infinite scroll
HandleInfiniteScroll();
// Handle swipe navigation
HandleSwipeNavigation();
}
catch (Exception ex)
{
Report.Debug("Mobile UI Patterns", $"Could not test all patterns: {ex.Message}");
}
}
private void HandlePullToRefresh()
{
try
{
var browserWindow = Host.Local.FindSingle<Form>($"/form[@processname='{_browserProcess}']");
if (browserWindow.Exists)
{
var bounds = browserWindow.ScreenRectangle;
var startPoint = new Location(bounds.Width / 2, 200);
var endPoint = new Location(bounds.Width / 2, bounds.Height / 2);
// Perform pull gesture using actual Touch API
Touch.TouchStart(browserWindow, 0, startPoint);
Touch.TouchMove(browserWindow, 0, endPoint, Duration.FromMilliseconds(800));
Touch.TouchEnd(browserWindow, 0, endPoint);
Thread.Sleep(2000);
Report.Info("Pull to Refresh", "Performed pull-to-refresh gesture");
}
}
catch (Exception ex)
{
Report.Debug("Pull to Refresh", $"Could not perform pull-to-refresh: {ex.Message}");
}
}
private void HandleMobileTouchGestures()
{
try
{
// Test swipe gestures
TestSwipeGestures();
// Test pinch zoom
TestPinchZoom();
// Test long press
TestLongPress();
Report.Success("Touch Gestures", "Successfully tested mobile touch gestures");
}
catch (Exception ex)
{
Report.Failure("Touch Gestures", $"Failed: {ex.Message}");
throw;
}
}
private void TestSwipeGestures()
{
try
{
var browserWindow = Host.Local.FindSingle<Form>($"/form[@processname='{_browserProcess}']");
if (browserWindow.Exists)
{
var bounds = browserWindow.ScreenRectangle;
var centerY = bounds.Height / 2;
// Swipe left
Touch.TouchStart(browserWindow, 0, new Location(bounds.Width - 50, centerY));
Touch.TouchMove(browserWindow, 0, new Location(50, centerY), Duration.FromMilliseconds(400));
Touch.TouchEnd(browserWindow, 0, new Location(50, centerY));
Thread.Sleep(1000);
// Swipe right
Touch.TouchStart(browserWindow, 0, new Location(50, centerY));
Touch.TouchMove(browserWindow, 0, new Location(bounds.Width - 50, centerY), Duration.FromMilliseconds(400));
Touch.TouchEnd(browserWindow, 0, new Location(bounds.Width - 50, centerY));
Thread.Sleep(1000);
Report.Info("Swipe Gestures", "Performed left and right swipe gestures");
}
}
catch (Exception ex)
{
Report.Debug("Swipe Gestures", $"Could not perform swipe gestures: {ex.Message}");
}
}
private void TestPinchZoom()
{
try
{
var browserWindow = Host.Local.FindSingle<Form>($"/form[@processname='{_browserProcess}']");
if (browserWindow.Exists)
{
var bounds = browserWindow.ScreenRectangle;
var centerX = bounds.Width / 2;
var centerY = bounds.Height / 2;
// Pinch to zoom in using actual Touch API
var point1Start = new Location(centerX - 50, centerY - 50);
var point1End = new Location(centerX - 100, centerY - 100);
var point2Start = new Location(centerX + 50, centerY + 50);
var point2End = new Location(centerX + 100, centerY + 100);
// Start multi-touch
Touch.TouchStart(browserWindow, 0, point1Start);
Touch.TouchStart(browserWindow, 1, point2Start);
Thread.Sleep(100);
// Move touches apart (zoom in)
Touch.TouchMove(browserWindow, 0, point1End, Duration.FromMilliseconds(500));
Touch.TouchMove(browserWindow, 1, point2End, Duration.FromMilliseconds(500));
Thread.Sleep(200);
// End touches
Touch.TouchEnd(browserWindow, 0, point1End);
Touch.TouchEnd(browserWindow, 1, point2End);
Thread.Sleep(2000);
// Check zoom level
var zoomLevel = _webDocument.ExecuteScript("return window.devicePixelRatio || 1");
Report.Info("Pinch Zoom", $"Zoom level after pinch: {zoomLevel}");
}
}
catch (Exception ex)
{
Report.Debug("Pinch Zoom", $"Could not test pinch zoom: {ex.Message}");
}
}
private void TestLongPress()
{
try
{
// Find an element to long press
var testElement = _webDocument.FindSingle<DivTag>("//div | //p | //span", 5000);
if (testElement.Exists)
{
// Perform long touch using actual Touch API
Touch.LongTouch(testElement, Location.Center, Duration.FromMilliseconds(1500));
Thread.Sleep(2000);
// Try to dismiss context menu by pressing Escape or clicking outside
var browserWindow = Host.Local.FindSingle<Form>($"/form[@processname='{_browserProcess}']");
if (browserWindow.Exists)
{
// Try pressing Escape key first
browserWindow.PressKeys("{Escape}");
Thread.Sleep(500);
// If that doesn't work, click outside to dismiss any context menu
Touch.Tap(browserWindow, new Location(50, 50));
}
Report.Info("Long Press", "Performed long press gesture");
}
}
catch (Exception ex)
{
Report.Debug("Long Press", $"Could not test long press: {ex.Message}");
}
}
private void TestResponsiveDesign()
{
try
{
// Test different screen orientations and layouts
TestPortraitLayout();
TestLandscapeLayout();
TestDifferentViewportSizes();
Report.Success("Responsive Design", "Successfully tested responsive design features");
}
catch (Exception ex)
{
Report.Debug("Responsive Design", $"Could not test responsive design: {ex.Message}");
}
}
private void TestPortraitLayout()
{
try
{
// Capture layout information in portrait mode
var layoutInfo = _webDocument.ExecuteScript(@"
return {
viewport: { width: window.innerWidth, height: window.innerHeight },
mainContent: document.querySelector('main, #main, .main')?.getBoundingClientRect(),
navigation: document.querySelector('nav, .nav, .navigation')?.getBoundingClientRect()
};
");
Report.Info("Portrait Layout", $"Layout info captured: {layoutInfo}");
}
catch (Exception ex)
{
Report.Debug("Portrait Layout", $"Could not capture portrait layout info: {ex.Message}");
}
}
private void TestLandscapeLayout()
{
try
{
// Note: Actual rotation would be handled through external device management
// Here we simulate testing landscape layout patterns
var layoutInfo = _webDocument.ExecuteScript(@"
return {
viewport: { width: window.innerWidth, height: window.innerHeight },
isLandscape: window.innerWidth > window.innerHeight,
navigation: document.querySelector('nav, .nav, .navigation')?.getBoundingClientRect()
};
");
Report.Info("Landscape Layout", $"Layout info captured: {layoutInfo}");
}
catch (Exception ex)
{
Report.Debug("Landscape Layout", $"Could not test landscape layout: {ex.Message}");
}
}
private void TestDifferentViewportSizes()
{
try
{
// Test how the page responds to different viewport sizes
var viewportInfo = _webDocument.ExecuteScript(@"
return {
currentWidth: window.innerWidth,
currentHeight: window.innerHeight,
isMobile: window.innerWidth <= 768,
isTablet: window.innerWidth > 768 && window.innerWidth <= 1024,
isDesktop: window.innerWidth > 1024
};
");
Report.Info("Viewport Sizes", $"Viewport info: {viewportInfo}");
}
catch (Exception ex)
{
Report.Debug("Viewport Sizes", $"Could not test viewport sizes: {ex.Message}");
}
}
private void HandleInfiniteScroll()
{
try
{
// Scroll to bottom of page to test infinite scroll
var browserWindow = Host.Local.FindSingle<Form>($"/form[@processname='{_browserProcess}']");
if (browserWindow.Exists)
{
var bounds = browserWindow.ScreenRectangle;
for (int i = 0; i < 3; i++)
{
var startY = bounds.Y + (int)(bounds.Height * 0.8);
var endY = bounds.Y + (int)(bounds.Height * 0.2);
var centerX = bounds.X + (bounds.Width / 2);
// Perform scroll using actual Touch API
Touch.TouchStart(browserWindow, 0, new Location(centerX - bounds.X, startY - bounds.Y));
Touch.TouchMove(browserWindow, 0, new Location(centerX - bounds.X, endY - bounds.Y), Duration.FromMilliseconds(500));
Touch.TouchEnd(browserWindow, 0, new Location(centerX - bounds.X, endY - bounds.Y));
Thread.Sleep(2000);
// Check if new content loaded
var contentHeight = _webDocument.ExecuteScript("return document.body.scrollHeight");
Report.Info("Infinite Scroll", $"Page height after scroll {i + 1}: {contentHeight}");
}
}
}
catch (Exception ex)
{
Report.Debug("Infinite Scroll", $"Could not test infinite scroll: {ex.Message}");
}
}
private void HandleSwipeNavigation()
{
try
{
var browserWindow = Host.Local.FindSingle<Form>($"/form[@processname='{_browserProcess}']");
if (browserWindow.Exists)
{
var bounds = browserWindow.ScreenRectangle;
var centerY = bounds.Height / 2;
// Swipe left for next page
Touch.TouchStart(browserWindow, 0, new Location(bounds.Width - 50, centerY));
Touch.TouchMove(browserWindow, 0, new Location(50, centerY), Duration.FromMilliseconds(400));
Touch.TouchEnd(browserWindow, 0, new Location(50, centerY));
Thread.Sleep(2000);
// Swipe right for previous page
Touch.TouchStart(browserWindow, 0, new Location(50, centerY));
Touch.TouchMove(browserWindow, 0, new Location(bounds.Width - 50, centerY), Duration.FromMilliseconds(400));
Touch.TouchEnd(browserWindow, 0, new Location(bounds.Width - 50, centerY));
Report.Info("Swipe Navigation", "Performed swipe navigation gestures");
}
}
catch (Exception ex)
{
Report.Debug("Swipe Navigation", $"Could not perform swipe navigation: {ex.Message}");
}
}
// Helper methods for mobile web automation
private Element FindElementWithFallback(string[] xpaths)
{
foreach (var xpath in xpaths)
{
try
{
if (Host.Local.TryFindSingle(xpath, 2000, out Element element))
{
if (element.Exists && element.Visible)
{
return element;
}
}
}
catch (Exception)
{
continue;
}
}
return null;
}
private void TakeScreenshot(string name)
{
try
{
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var adapter = Host.Local.FindSingle<Adapter>("/form");
Report.Screenshot(adapter, $"{name}_{timestamp}");
}
catch (Exception ex)
{
Report.Warn("Screenshot", $"Could not take screenshot: {ex.Message}");
}
}
}
Cross-Platform
Cross-Platform App Automation
using Ranorex;
using System;
using System.Collections.Generic;
public class CrossPlatformAutomation
{
private string _appIdentifier;
private bool _isAndroid;
public void AutomateCrossPlatformApp(string appIdentifier, bool isAndroid = true)
{
Host.Initialize();
try
{
_appIdentifier = appIdentifier;
_isAndroid = isAndroid;
// Setup cross-platform automation
SetupCrossPlatformEnvironment();
// Launch application
LaunchCrossPlatformApp();
// Handle platform-agnostic automation
HandlePlatformAgnosticElements();
// Handle platform-specific differences
HandlePlatformSpecificFeatures();
Report.Success("Cross-Platform", "Successfully automated cross-platform application");
}
catch (Exception ex)
{
Report.Failure("Cross-Platform", $"Failed: {ex.Message}");
// Consider using a custom screenshot utility or Ranorex's built-in screenshot capabilities.
// For example: Report.Screenshot(Host.Local.FindSingle<Adapter>("/form"));
throw;
}
finally
{
Host.Shutdown();
}
}
private void SetupCrossPlatformEnvironment()
{
try
{
// Note: Device info would typically be obtained through actual device management APIs
// or configuration rather than fictional Mobile.Device APIs
var platformName = _isAndroid ? "Android" : "iOS";
Report.Info("Platform Setup", $"Configuring for {platformName} device");
// Configure platform-specific settings
if (_isAndroid)
{
ConfigureAndroidSettings();
}
else
{
ConfigureiOSSettings();
}
}
catch (Exception ex)
{
throw new Exception($"Failed to setup cross-platform environment: {ex.Message}");
}
}
private void ConfigureAndroidSettings()
{
// Android-specific configuration
// Note: Device orientation would be handled through external device management tools
// or through app-specific automation rather than fictional Mobile.Device APIs
Report.Info("Android Config", "Configured Android-specific settings");
}
private void ConfigureiOSSettings()
{
// iOS-specific configuration
// Note: Device orientation would be handled through external device management tools
// or through app-specific automation rather than fictional Mobile.Device APIs
Report.Info("iOS Config", "Configured iOS-specific settings");
}
private void LaunchCrossPlatformApp()
{
try
{
// Note: Application launch would typically be handled through external device management
// tools, app deployment systems, or by connecting to already running applications
// rather than using fictional Mobile.RunApplication API
// Try to connect to running application by process name or app identifier
var appWindow = Host.Local.FindSingle<Form>($"/form[@processname='{_appIdentifier}']", 15000);
if (!appWindow.Exists)
{
// Try to find by partial title match
appWindow = Host.Local.FindSingle<Form>($"//form[contains(@title,'{_appIdentifier}')]", 10000);
}
if (appWindow.Exists)
{
appWindow.Focus();
Thread.Sleep(2000);
// Wait for cross-platform framework to initialize
WaitForFrameworkInitialization(TimeSpan.FromSeconds(20));
Report.Success("App Launch", "Successfully connected to cross-platform application");
}
else
{
Report.Info("App Launch", $"Could not auto-connect to app '{_appIdentifier}'. Please ensure the application is running and try manual connection.");
throw new Exception($"Application '{_appIdentifier}' not found. Please launch manually and retry.");
}
}
catch (Exception ex)
{
throw new Exception($"Failed to launch cross-platform app: {ex.Message}");
}
}
private void WaitForFrameworkInitialization(TimeSpan timeout)
{
// Wait for React Native, Xamarin, or Flutter to initialize
var xpaths = new[]
{
"//*[@class='com.facebook.react.ReactRootView']",
"//*[contains(@class, 'xamarin')]",
"//*[contains(@class, 'flutter')]"
};
if (Host.Local.TryFindSingle(string.Join(" | ", xpaths), (int)timeout.TotalMilliseconds, out Element _))
{
Report.Info("Framework", "Cross-platform framework initialized");
}
else
{
Report.Warn("Framework", "Could not detect cross-platform framework initialization");
}
}
private void HandlePlatformAgnosticElements()
{
try
{
// Use accessibility identifiers that work across platforms
HandleAccessibilityBasedElements();
// Handle common UI patterns
HandleCommonUIPatterns();
// Handle shared business logic
HandleSharedBusinessLogic();
}
catch (Exception ex)
{
Report.Failure("Platform Agnostic", $"Failed: {ex.Message}");
throw;
}
}
private void HandleSharedBusinessLogic()
{
try
{
// Handle common business flows that work across platforms
var searchField = FindCrossPlatformElement("search_input");
if (searchField != null)
{
searchField.Click();
searchField.PressKeys("test query");
var searchButton = FindCrossPlatformElement("search_button");
if (searchButton != null)
{
searchButton.Click();
Thread.Sleep(3000);
// Wait for search results
var resultsContainer = FindCrossPlatformElement("search_results");
if (resultsContainer != null)
{
Report.Success("Search", "Successfully performed search operation");
}
}
}
// Handle common data entry patterns
var dataEntryForm = FindCrossPlatformElement("data_entry_form");
if (dataEntryForm != null)
{
var nameField = FindCrossPlatformElement("name_field");
var emailField = FindCrossPlatformElement("email_field");
var submitButton = FindCrossPlatformElement("submit_button");
if (nameField != null && emailField != null && submitButton != null)
{
nameField.Click();
nameField.PressKeys("Test User");
emailField.Click();
emailField.PressKeys("test@example.com");
submitButton.Click();
Report.Success("Data Entry", "Successfully completed data entry");
}
}
Report.Info("Shared Logic", "Handled shared business logic patterns");
}
catch (Exception ex)
{
Report.Debug("Shared Logic", $"No shared business logic available: {ex.Message}");
}
}
private void HandleAccessibilityBasedElements()
{
try
{
// Use testID/accessibilityIdentifier for cross-platform compatibility
var usernameField = FindCrossPlatformElement("username_input");
if (usernameField != null)
{
usernameField.Click();
usernameField.PressKeys("testuser");
}
var passwordField = FindCrossPlatformElement("password_input");
if (passwordField != null)
{
passwordField.Click();
passwordField.PressKeys("password123");
}
var loginButton = FindCrossPlatformElement("login_button");
if (loginButton != null)
{
loginButton.Click();
}
Report.Success("Accessibility Elements", "Successfully used accessibility-based elements");
}
catch (Exception ex)
{
Report.Failure("Accessibility Elements", $"Failed: {ex.Message}");
throw;
}
}
private Element FindCrossPlatformElement(string identifier)
{
try
{
// Try different platform-specific approaches
var xpaths = new List<string>();
if (_isAndroid)
{
xpaths.Add($"//*[@content-desc='{identifier}']");
xpaths.Add($"//*[@resource-id='*/{identifier}']");
xpaths.Add($"//*[@text='{identifier}']");
}
else
{
xpaths.Add($"//*[@name='{identifier}']");
xpaths.Add($"//*[@label='{identifier}']");
xpaths.Add($"//*[@value='{identifier}']");
}
if (Host.Local.TryFindSingle(string.Join(" | ", xpaths), 2000, out Element element))
{
return element;
}
return null;
}
catch (Exception ex)
{
Report.Debug("Element Search", $"Could not find element {identifier}: {ex.Message}");
return null;
}
}
private void HandleCommonUIPatterns()
{
try
{
// Handle navigation drawer/menu
HandleNavigationDrawer();
// Handle tab navigation
HandleTabNavigation();
// Handle list views
HandleListViews();
Report.Success("UI Patterns", "Successfully handled common UI patterns");
}
catch (Exception ex)
{
Report.Debug("UI Patterns", $"Some UI patterns not available: {ex.Message}");
}
}
private void HandleNavigationDrawer()
{
try
{
var menuButton = FindCrossPlatformElement("menu_button");
if (menuButton != null)
{
menuButton.Click();
var settingsOption = FindCrossPlatformElement("settings_option");
if (settingsOption != null)
{
settingsOption.Click();
Report.Success("Navigation", "Successfully used navigation drawer");
}
}
}
catch (Exception ex)
{
Report.Debug("Navigation Drawer", $"Could not handle navigation drawer: {ex.Message}");
}
}
private void HandleTabNavigation()
{
try
{
var homeTab = FindCrossPlatformElement("home_tab");
var profileTab = FindCrossPlatformElement("profile_tab");
if (homeTab != null && profileTab != null)
{
homeTab.Click();
profileTab.Click();
Report.Success("Tab Navigation", "Successfully navigated between tabs");
}
}
catch (Exception ex)
{
Report.Debug("Tab Navigation", $"Could not handle tab navigation: {ex.Message}");
}
}
private void HandleListViews()
{
try
{
var listContainer = FindCrossPlatformElement("items_list");
if (listContainer != null)
{
// Find items within the list
var listItems = listContainer.Find<Element>(".//*");
Report.Info("List Items", $"Found {listItems.Count} items in list");
if (listItems.Count > 0)
{
listItems[0].Click();
// Wait for the next screen to appear
// The path should be adapted to the application's UI structure.
Host.Local.FindSingle<Adapter>("/form", 5000);
Report.Success("List Interaction", "Successfully interacted with list item");
}
}
}
catch (Exception ex)
{
Report.Debug("List Views", $"Could not handle list views: {ex.Message}");
}
}
private void HandlePlatformSpecificFeatures()
{
try
{
if (_isAndroid)
{
HandleAndroidSpecificFeatures();
}
else
{
HandleiOSSpecificFeatures();
}
}
catch (Exception ex)
{
Report.Debug("Platform Specific", $"Could not handle platform-specific features: {ex.Message}");
}
}
private void HandleAndroidSpecificFeatures()
{
try
{
// Handle Android back button using actual UI navigation
// Note: Hardware button simulation would typically be handled through
// external device automation tools rather than fictional Mobile APIs
var appWindow = Host.Local.FindSingle<Form>("/form");
if (appWindow.Exists)
{
// Try to find and use in-app back button
var backButton = FindCrossPlatformElement("back_button");
if (backButton != null)
{
backButton.Click();
Report.Info("Android Back", "Used in-app back button");
}
else
{
// Simulate back gesture with swipe from left edge
var bounds = appWindow.ScreenRectangle;
Touch.TouchStart(appWindow, 0, new Location(10, bounds.Height / 2));
Touch.TouchMove(appWindow, 0, new Location(bounds.Width / 3, bounds.Height / 2), Duration.FromMilliseconds(400));
Touch.TouchEnd(appWindow, 0, new Location(bounds.Width / 3, bounds.Height / 2));
Report.Info("Android Back", "Simulated back gesture");
}
}
// Handle Android share intent
var shareButton = FindCrossPlatformElement("share_button");
if (shareButton != null)
{
shareButton.Click();
Thread.Sleep(2000);
// Wait for the share dialog to appear and look for share options
var shareDialog = Host.Local.FindSingle<Container>("//container[contains(@text,'Share') or contains(@name,'Share')]", 5000);
if (shareDialog.Exists)
{
Report.Info("Android Share", "Share dialog appeared");
// Dismiss share dialog by tapping outside or finding close button
var closeButton = Host.Local.FindSingle<Button>("//button[@text='Cancel' or @name='Cancel']", 3000);
if (closeButton.Exists)
{
closeButton.Click();
}
else if (appWindow.Exists)
{
// Tap outside to dismiss
Touch.Tap(appWindow, new Location(50, 50));
}
}
}
Report.Success("Android Features", "Successfully handled Android-specific features");
}
catch (Exception ex)
{
Report.Debug("Android Features", $"Could not handle Android features: {ex.Message}");
}
}
private void HandleiOSSpecificFeatures()
{
try
{
// Handle iOS navigation
var backButton = Host.Local.FindSingle<Button>("//*[@name='Back']", 3000);
if (backButton.Exists)
{
backButton.Click();
}
// Handle iOS action sheet
var actionButton = FindCrossPlatformElement("action_button");
if (actionButton != null)
{
actionButton.Click();
// Wait for the action sheet to appear
Host.Local.FindSingle<Container>("//*[@type='UIActionSheet']", 5000);
// Dismiss action sheet
var cancelButton = Host.Local.FindSingle<Button>("//*[@name='Cancel']", 3000);
if (cancelButton.Exists)
{
cancelButton.Click();
}
}
Report.Success("iOS Features", "Successfully handled iOS-specific features");
}
catch (Exception ex)
{
Report.Debug("iOS Features", $"Could not handle iOS features: {ex.Message}");
}
}
}
Best Practices for Mobile Automation
1. Device Management
- Use device farms for testing across multiple devices
- Test on both physical devices and emulators
- Consider different screen sizes and resolutions
2. Platform Compatibility
- Use accessibility identifiers for cross-platform compatibility
- Handle platform-specific UI patterns appropriately
- Test orientation changes and device rotation
3. Performance Considerations
- Optimize wait times for mobile network conditions
- Handle app backgrounding and foregrounding
- Monitor memory usage and app crashes
4. Touch and Gesture Handling
- Use appropriate touch targets (minimum 44px)
- Test complex gestures like pinch, swipe, and long press
- Handle keyboard appearance and dismissal
5. Error Recovery
- Implement retry mechanisms for network-dependent operations
- Handle permission dialogs gracefully
- Test offline scenarios and poor connectivity
Next Steps
For advanced mobile automation scenarios:
- Advanced Integration - API integration with mobile apps
- Web Patterns - Mobile web automation techniques
- Code Cookbook - Reusable mobile automation utilities
This mobile automation framework provides comprehensive coverage for iOS, Android, hybrid, and cross-platform mobile application testing scenarios.