Skip to content

Commit 52d6202

Browse files
seriousQACopilotKazuCocoa
authored
feat: Custom commands execution with ICustomDriverCommandExecutor (#965)
* Reuse an Selenium ICustomDriverCommandExecutor in Appium dotnet client with custom commands Implement #567. Now Appium dotnet has an interface ICustomDriverCommandExecutor for registering/executing custom commands. AppiumDriver implements this interface, reusing Selenium’s extensibility. Users can add custom commands for network, screenshots, preferences, etc.. * CustomDriverCommandExecutor (part 2) Implement #567 . Unit Tests were added. Support of AppiumCommandExecutor. * Update test/integration/CustomCommands/CustomCommandsTests.cs SaveAsFile(filePath) Co-authored-by: Copilot <[email protected]> * Update src/Appium.Net/Appium/AppiumDriver.cs Co-authored-by: Copilot <[email protected]> * fix of ExecuteCustomDriverCommand() * Update src/Appium.Net/Appium/AppiumDriver.cs formatting fix Co-authored-by: Kazuaki Matsuo <[email protected]> * Update src/Appium.Net/Appium/AppiumDriver.cs formatting fix Co-authored-by: Kazuaki Matsuo <[email protected]> * new changes in CustomCommandsTests Accordinf to Dor-bl review. * Added a new name of custom command According to KazuCocoa comment. * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Kazuaki Matsuo <[email protected]>
1 parent b3ebc07 commit 52d6202

File tree

3 files changed

+118
-10
lines changed

3 files changed

+118
-10
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ public AppState GetAppState(string appId) =>
425425
["appId"] = appId
426426
}
427427
}
428-
).ToString()
428+
)?.ToString() ?? throw new InvalidOperationException("ExecuteScript returned null for mobile:queryAppState")
429429
);
430430
}
431431
}

src/Appium.Net/Appium/AppiumDriver.cs

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using OpenQA.Selenium.Appium.Interfaces;
1717
using OpenQA.Selenium.Appium.Service;
1818
using OpenQA.Selenium.Interactions;
19+
using OpenQA.Selenium.Remote;
1920
using System;
2021
using System.Collections;
2122
using System.Collections.Generic;
@@ -31,7 +32,7 @@ public abstract class AppiumDriver : WebDriver,
3132
IHasSessionDetails,
3233
IHasLocation,
3334
IHidesKeyboard, IInteractsWithFiles, IFindsByFluentSelector<AppiumElement>,
34-
IInteractsWithApps, IRotatable, IContextAware
35+
IInteractsWithApps, IRotatable, IContextAware, ICustomDriverCommandExecutor
3536
{
3637
private const string NativeApp = "NATIVE_APP";
3738

@@ -110,8 +111,7 @@ public AppiumDriver(AppiumLocalService service, ICapabilities appiumOptions, Tim
110111
{
111112
}
112113

113-
114-
#endregion Constructors
114+
#endregion Constructors
115115

116116
#region Public Methods
117117

@@ -140,21 +140,59 @@ Response IExecuteMethod.Execute(string commandName, Dictionary<string, object> p
140140

141141
#region MJsonMethod Members
142142

143+
/// <summary>Install an app on the device.</summary>
144+
/// <param name="appPath">Full path to the app (.apk or .ipa) on the local filesystem.</param>
143145
public void InstallApp(string appPath) =>
144146
Execute(AppiumDriverCommand.InstallApp, AppiumCommandExecutionHelper.PrepareArgument("appPath", appPath));
145147

148+
/// <summary> Remove an app from the device. </summary>
149+
/// <param name="appId"> The bundle identifier of the app (e.g., com.example.myapp). </param>
146150
public void RemoveApp(string appId) =>
147151
Execute(AppiumDriverCommand.RemoveApp, AppiumCommandExecutionHelper.PrepareArgument("appId", appId));
148152

153+
/// <summary> Launch an app on the device. </summary>
154+
/// <param name="appId"> The bundle identifier of the app (e.g., com.example.myapp). </param>
149155
public void ActivateApp(string appId) =>
150156
Execute(AppiumDriverCommand.ActivateApp, AppiumCommandExecutionHelper.PrepareArgument("appId", appId));
151157

158+
/// <summary> Launch an app on the device with a timeout. </summary>
159+
/// <param name="appId"> The bundle identifier of the app (e.g., com.example.myapp). </param>
160+
/// <param name="timeout">The timeout to wait for the app to launch.</param>
152161
public void ActivateApp(string appId, TimeSpan timeout) =>
153162
Execute(AppiumDriverCommand.ActivateApp,
154-
AppiumCommandExecutionHelper.PrepareArguments(new string[] {"appId", "options"},
163+
AppiumCommandExecutionHelper.PrepareArguments(new string[] { "appId", "options" },
155164
new object[]
156165
{appId, new Dictionary<string, object>() {{"timeout", (long) timeout.TotalMilliseconds}}}));
157166

167+
/// <summary> Register a custom command in the command executor. </summary>
168+
/// <param name="commandName"> The name of the command to register. </param>
169+
/// <param name="method"> The HTTP method of the command to register. </param>
170+
/// <param name="resourcePath">The resource path of the command to register.</param>
171+
public void RegisterCustomDriverCommand(string commandName, string method, string resourcePath)
172+
{
173+
if (this.CommandExecutor is HttpCommandExecutor httpExecutor)
174+
{
175+
httpExecutor.TryAddCommand(commandName, new HttpCommandInfo(method, resourcePath));
176+
}
177+
else if (this.CommandExecutor is AppiumCommandExecutor appiumExecutor)
178+
{
179+
appiumExecutor.TryAddCommand(commandName, new HttpCommandInfo(method, resourcePath));
180+
}
181+
else
182+
{
183+
throw new NotSupportedException("Custom commands can only be registered with HttpCommandExecutor or AppiumCommandExecutor.");
184+
}
185+
}
186+
187+
/// <summary> Execute a custom command in the command executor. </summary>
188+
/// <param name="commandName"> The name of the command to execute. </param>
189+
/// <param name="parameters"> The parameters of the command to execute. </param>
190+
public new object ExecuteCustomDriverCommand(string commandName, Dictionary<string, object> parameters = null)
191+
{
192+
var response = Execute(commandName, parameters);
193+
return response?.Value;
194+
}
195+
158196
public bool TerminateApp(string appId) =>
159197
Convert.ToBoolean(Execute(AppiumDriverCommand.TerminateApp,
160198
AppiumCommandExecutionHelper.PrepareArgument("appId", appId)).Value.ToString());
@@ -190,12 +228,15 @@ public void PushFile(string pathOnDevice, FileInfo file) =>
190228
public void FingerPrint(int fingerprintId) =>
191229
AppiumCommandExecutionHelper.FingerPrint(this, fingerprintId);
192230

193-
public void BackgroundApp() {
231+
public void BackgroundApp()
232+
{
194233
BackgroundApp(TimeSpan.FromSeconds(-1));
195234
}
196235

197-
public void BackgroundApp(TimeSpan timeSpan) {
198-
Execute(DriverCommand.ExecuteScript, new Dictionary<string, object> {
236+
public void BackgroundApp(TimeSpan timeSpan)
237+
{
238+
Execute(DriverCommand.ExecuteScript, new Dictionary<string, object>
239+
{
199240
["script"] = "mobile:backgroundApp",
200241
["args"] = new object[] {
201242
new Dictionary<string, object> {
@@ -228,7 +269,7 @@ public Dictionary<string, object> GetAppStringDictionary(string language = null,
228269
return Execute(DriverCommand.ExecuteScript, new Dictionary<string, object>
229270
{
230271
["script"] = "mobile:getAppStrings",
231-
["args"] = parameters.Count == 0 ? Array.Empty<object>() : new object[] {parameters}
272+
["args"] = parameters.Count == 0 ? Array.Empty<object>() : new object[] { parameters }
232273
}).Value as Dictionary<string, object>;
233274
}
234275

@@ -414,7 +455,8 @@ public string DeviceTime
414455
{
415456
get
416457
{
417-
return Execute(DriverCommand.ExecuteScript, new Dictionary<string, object> {
458+
return Execute(DriverCommand.ExecuteScript, new Dictionary<string, object>
459+
{
418460
["script"] = "mobile:getDeviceTime",
419461
["args"] = new object[] {}
420462
}).Value.ToString();
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using Appium.Net.Integration.Tests.helpers;
2+
using NUnit.Framework;
3+
using OpenQA.Selenium.Appium.Android;
4+
using System.Collections.Generic;
5+
using OpenQA.Selenium;
6+
using OpenQA.Selenium.Remote;
7+
using System;
8+
using OpenQA.Selenium.Appium;
9+
10+
namespace Appium.Net.Integration.Tests
11+
{
12+
[TestFixture]
13+
public class CustomCommandTests
14+
{
15+
private AndroidDriver _driver;
16+
17+
[SetUp]
18+
public void SetUp()
19+
{
20+
var capabilities = Caps.GetAndroidUIAutomatorCaps();
21+
capabilities.UseWebSocketUrl = true;
22+
var serverUri = Env.ServerIsRemote() ? AppiumServers.RemoteServerUri : AppiumServers.LocalServiceUri;
23+
_driver = new AndroidDriver(serverUri, capabilities, Env.InitTimeoutSec);
24+
}
25+
26+
[TearDown]
27+
public void TearDown()
28+
{
29+
_driver?.Quit();
30+
}
31+
32+
[Test]
33+
public void CustomCommand_Status_ReturnsServerStatusSuccessfully()
34+
{
35+
// Register a custom command for /status endpoint (GET)
36+
_driver.RegisterCustomDriverCommand("getStatusExample", "GET", "/status");
37+
38+
// Execute the custom command
39+
var result = _driver.ExecuteCustomDriverCommand("getStatusExample");
40+
41+
// Assert that the result is a dictionary and contains the expected keys/values
42+
Assert.That(result, Is.Not.Null);
43+
Assert.That(result, Is.InstanceOf<Dictionary<string, object>>());
44+
var dict = (Dictionary<string, object>)result;
45+
Assert.That(dict.ContainsKey("ready"), Is.True);
46+
Assert.That(dict.ContainsKey("message"), Is.True);
47+
Assert.That(dict["ready"], Is.True);
48+
Assert.That(dict["message"], Is.EqualTo("The server is ready to accept new connections"));
49+
}
50+
51+
[Test]
52+
public void RegisterAndExecuteCustomCommand_ShouldReturnCurrentActivity()
53+
{
54+
// Register a custom command for /appium/device/current_activity endpoint (GET)
55+
_driver.RegisterCustomDriverCommand("getCurrentActivityExample", "GET", "/session/{sessionId}/appium/device/current_activity");
56+
57+
// Execute the custom command
58+
var result = _driver.ExecuteCustomDriverCommand("getCurrentActivityExample");
59+
60+
// Assert that the result is a string (activity name)
61+
Assert.That(result, Is.Not.Null);
62+
Assert.That(result, Is.TypeOf<string>());
63+
Assert.That(result.ToString(), Does.StartWith(".").Or.Contain("Activity"));
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)