Skip to content

Commit 5657846

Browse files
authored
feat: add InstallApp method for Android and iOS (#974)
* feat: add InstallApp method for Android and iOS, including tests for installation scenarios * fix: improve error handling in Dispose method of InstallAppTest * fix: remove unused AppiumOptions instantiation in InstallAppTest * fix: correct typo in region name from 'Device Kesys' to 'Device Keys' * fix: update documentation link for mobile: installApp in AndroidCommandExecutionHelper
1 parent 6f16c47 commit 5657846

File tree

5 files changed

+188
-1
lines changed

5 files changed

+188
-1
lines changed

src/Appium.Net/Appium/Android/AndroidCommandExecutionHelper.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,50 @@ public static float GetDisplayDensity(IExecuteMethod executeMethod)
172172

173173
#endregion
174174

175+
/// <summary>
176+
/// Install an app on the Android device using mobile: installApp script.
177+
/// For documentation, see <see href="https://github.com/appium/appium-uiautomator2-driver?tab=readme-ov-file#mobile-installapp">mobile: installApp</see>.
178+
/// </summary>
179+
/// <param name="executeMethod">The execute method</param>
180+
/// <param name="appPath">Full path to the .apk on the local filesystem or a remote URL.</param>
181+
/// <param name="timeout">Optional timeout in milliseconds to wait for the app installation to complete. 60000ms by default.</param>
182+
/// <param name="allowTestPackages">Optional flag to allow test packages installation. false by default.</param>
183+
/// <param name="useSdcard">Optional flag to install the app on sdcard instead of device memory. false by default.</param>
184+
/// <param name="grantPermissions">Optional flag to grant all permissions requested in the app manifest automatically after installation. false by default.</param>
185+
/// <param name="replace">Optional flag to upgrade/reinstall if app is already present. true by default.</param>
186+
/// <param name="checkVersion">Optional flag to skip installation if device has equal or greater app version. false by default.</param>
187+
public static void InstallApp(
188+
IExecuteMethod executeMethod,
189+
string appPath,
190+
int? timeout = null,
191+
bool? allowTestPackages = null,
192+
bool? useSdcard = null,
193+
bool? grantPermissions = null,
194+
bool? replace = null,
195+
bool? checkVersion = null)
196+
{
197+
var args = new Dictionary<string, object> { { "appPath", appPath } };
198+
199+
if (timeout.HasValue)
200+
args["timeout"] = timeout.Value;
201+
if (allowTestPackages.HasValue)
202+
args["allowTestPackages"] = allowTestPackages.Value;
203+
if (useSdcard.HasValue)
204+
args["useSdcard"] = useSdcard.Value;
205+
if (grantPermissions.HasValue)
206+
args["grantPermissions"] = grantPermissions.Value;
207+
if (replace.HasValue)
208+
args["replace"] = replace.Value;
209+
if (checkVersion.HasValue)
210+
args["checkVersion"] = checkVersion.Value;
211+
212+
executeMethod.Execute(DriverCommand.ExecuteScript, new Dictionary<string, object>
213+
{
214+
["script"] = "mobile: installApp",
215+
["args"] = new object[] { args }
216+
});
217+
}
218+
175219
public static Dictionary<string, object> GetSettings(IExecuteMethod executeMethod) =>
176220
(Dictionary<string, object>) executeMethod.Execute(AppiumDriverCommand.GetSettings).Value;
177221

src/Appium.Net/Appium/Android/AndroidDriver.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,32 @@ public AndroidDriver(AppiumLocalService service, DriverOptions driverOptions, Ti
182182

183183
public string CurrentPackage => AndroidCommandExecutionHelper.GetCurrentPackage(this);
184184

185-
#region Device Kesys
185+
#region App Management
186+
187+
/// <summary>
188+
/// Install an app on the Android device using mobile: installApp script.
189+
/// For documentation, see <see href="https://github.com/appium/appium-uiautomator2-driver?tab=readme-ov-file#mobile-installapp">mobile: installApp</see>.
190+
/// </summary>
191+
/// <param name="appPath">Full path to the .apk on the local filesystem or a remote URL.</param>
192+
/// <param name="timeout">Optional timeout in milliseconds to wait for the app installation to complete. 60000ms by default.</param>
193+
/// <param name="allowTestPackages">Optional flag to allow test packages installation. false by default.</param>
194+
/// <param name="useSdcard">Optional flag to install the app on sdcard instead of device memory. false by default.</param>
195+
/// <param name="grantPermissions">Optional flag to grant all permissions requested in the app manifest automatically after installation. false by default.</param>
196+
/// <param name="replace">Optional flag to upgrade/reinstall if app is already present. true by default.</param>
197+
/// <param name="checkVersion">Optional flag to skip installation if device has equal or greater app version. false by default.</param>
198+
public void InstallApp(
199+
string appPath,
200+
int? timeout = null,
201+
bool? allowTestPackages = null,
202+
bool? useSdcard = null,
203+
bool? grantPermissions = null,
204+
bool? replace = null,
205+
bool? checkVersion = null) =>
206+
AndroidCommandExecutionHelper.InstallApp(this, appPath, timeout, allowTestPackages, useSdcard, grantPermissions, replace, checkVersion);
207+
208+
#endregion
209+
210+
#region Device Keys
186211

187212
public void PressKeyCode(int keyCode, int metastate = -1) =>
188213
AppiumCommandExecutionHelper.PressKeyCode(this, keyCode, metastate);

src/Appium.Net/Appium/iOS/IOSCommandExecutionHelper.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,30 @@ public static Image GetClipboardImage(IExecuteMethod executeMethod)
9494
return null;
9595
}
9696

97+
/// <summary>
98+
/// Install an app on the iOS device using mobile: installApp script.
99+
/// For documentation, see <see href="https://appium.github.io/appium-xcuitest-driver/latest/reference/execute-methods/#mobile-installapp">mobile: installApp</see>.
100+
/// </summary>
101+
/// <param name="executeMethod">The execute method</param>
102+
/// <param name="appPath">Full path to the .ipa on the local filesystem or a remote URL.</param>
103+
/// <param name="timeoutMs">Optional timeout in milliseconds to wait for the app installation to complete.</param>
104+
public static void InstallApp(
105+
IExecuteMethod executeMethod,
106+
string appPath,
107+
int? timeoutMs = null)
108+
{
109+
var args = new Dictionary<string, object> { { "appPath", appPath } };
110+
111+
if (timeoutMs.HasValue)
112+
args["timeoutMs"] = timeoutMs.Value;
113+
114+
executeMethod.Execute(DriverCommand.ExecuteScript, new Dictionary<string, object>
115+
{
116+
["script"] = "mobile: installApp",
117+
["args"] = new object[] { args }
118+
});
119+
}
120+
97121
public static Dictionary<string, object> GetSettings(IExecuteMethod executeMethod) =>
98122
(Dictionary<string, object>)executeMethod.Execute(AppiumDriverCommand.GetSettings).Value;
99123

src/Appium.Net/Appium/iOS/IOSDriver.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,19 @@ public Dictionary<string, object> Settings
200200
/// <param name="match">Whether to simulate biometric match.</param>
201201
public void PerformTouchID(bool match) => IOSCommandExecutionHelper.PerformTouchID(this, match);
202202

203+
#region App Management
204+
205+
/// <summary>
206+
/// Install an app on the iOS device using mobile: installApp script.
207+
/// For documentation, see <see href="https://appium.github.io/appium-xcuitest-driver/latest/reference/execute-methods/#mobile-installapp">mobile: installApp</see>.
208+
/// </summary>
209+
/// <param name="appPath">Full path to the .ipa on the local filesystem or a remote URL.</param>
210+
/// <param name="timeoutMs">Optional timeout in milliseconds to wait for the app installation to complete.</param>
211+
public void InstallApp(string appPath, int? timeoutMs = null) =>
212+
IOSCommandExecutionHelper.InstallApp(this, appPath, timeoutMs);
213+
214+
#endregion
215+
203216
/// <summary>
204217
/// Check if the device is locked
205218
/// </summary>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System;
2+
using Appium.Net.Integration.Tests.helpers;
3+
using OpenQA.Selenium;
4+
using OpenQA.Selenium.Appium;
5+
using OpenQA.Selenium.Appium.Android;
6+
using NUnit.Framework;
7+
8+
namespace Appium.Net.Integration.Tests.Android.App
9+
{
10+
public class InstallAppTest : IDisposable
11+
{
12+
private readonly AndroidDriver _driver;
13+
private readonly string _apkPath;
14+
private readonly string _packageName;
15+
16+
public InstallAppTest()
17+
{
18+
_apkPath = Apps.Get(Apps.androidApiDemos);
19+
_packageName = Apps.GetId(Apps.androidApiDemos);
20+
var serverUri = Env.ServerIsRemote() ? AppiumServers.RemoteServerUri : AppiumServers.LocalServiceUri;
21+
AppiumOptions opts = Caps.GetAndroidUIAutomatorCaps();
22+
// Do not preinstall the app via capabilities; we test explicit installation.
23+
_driver = new AndroidDriver(serverUri, opts, TimeSpan.FromMinutes(2));
24+
}
25+
26+
[Test]
27+
public void MobileInstallApp_InstallsPackage()
28+
{
29+
_driver.InstallApp(_apkPath);
30+
Assert.That(_driver.IsAppInstalled(_packageName), Is.True);
31+
}
32+
33+
[Test]
34+
public void MobileInstallApp_IsIdempotent()
35+
{
36+
_driver.InstallApp(_apkPath);
37+
_driver.InstallApp(_apkPath); // second time should not fail
38+
Assert.That(_driver.IsAppInstalled(_packageName), Is.True);
39+
}
40+
41+
42+
[Test]
43+
public void MobileInstallApp_InvalidPath_Throws()
44+
{
45+
var badPath = "/nonexistent/path/app.apk";
46+
var ex = Assert.Throws<UnknownErrorException>(() => _driver.InstallApp(badPath));
47+
Assert.That(ex.Message, Does.Contain("does not exist").IgnoreCase);
48+
}
49+
50+
[Test]
51+
public void MobileInstallApp_WithTimeout_InstallsPackage()
52+
{
53+
_driver.InstallApp(_apkPath, timeout: 60000);
54+
Assert.That(_driver.IsAppInstalled(_packageName), Is.True);
55+
}
56+
57+
[Test]
58+
public void MobileInstallApp_WithCheckVersion_InstallsPackage()
59+
{
60+
_driver.InstallApp(_apkPath, checkVersion: true);
61+
Assert.That(_driver.IsAppInstalled(_packageName), Is.True);
62+
}
63+
64+
public void Dispose()
65+
{
66+
GC.SuppressFinalize(this);
67+
try
68+
{
69+
if (!string.IsNullOrWhiteSpace(_packageName) && _driver.IsAppInstalled(_packageName))
70+
{
71+
_driver.RemoveApp(_packageName);
72+
}
73+
}
74+
catch (Exception ex)
75+
{
76+
Console.Error.WriteLine($"Exception during Dispose: {ex}");
77+
}
78+
_driver?.Quit();
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)