From 2fb57620fd1d5ef736f245b9a6c0cfab0b164513 Mon Sep 17 00:00:00 2001 From: aristotelos Date: Mon, 22 Apr 2024 13:40:42 +0200 Subject: [PATCH] Add appium:appWorkingDir capability and return only matched Implement the custom `appium:appWorkingDir` capability. Along with this, instead of returning all capabilities, return only matched capabilities. --- README.md | 1 + src/FlaUI.WebDriver.UITests/SessionTests.cs | 36 ++++++++- .../Controllers/SessionController.cs | 75 +++++++++++++++++-- .../Models/CreateSessionResponse.cs | 2 +- 4 files changed, 103 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3bb289d..6782a08 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The following capabilities are supported: | platformName | Must be set to `windows` (case-insensitive). | `windows` | | appium:app | The path to the application, or in case of an UWP app, `!App`. It is also possible to set app to `Root`. In such case the session will be invoked without any explicit target application. Either this capability, `appTopLevelWindow` or `appTopLevelWindowTitleMatch` must be provided on session startup. | `C:\Windows\System32\notepad.exe`, `Microsoft.WindowsCalculator_8wekyb3d8bbwe!App` | | appium:appArguments | Application arguments string, for example `/?`. | | +| appium:appWorkingDir | Full path to the folder, which is going to be set as the working dir for the application under test. This is only applicable for classic apps. When this is used the `appium:app` may contain a relative file path. | `C:\MyApp\` | | appium:appTopLevelWindow | The hexadecimal handle of an existing application top level window to attach to, for example `0x12345` (should be of string type). Either this capability, `appTopLevelWindowTitleMatch` or `app` must be provided on session startup. | `0xC0B46` | | appium:appTopLevelWindowTitleMatch | The title of an existing application top level window to attach to, for example `My App Window Title` (should be of string type). Either this capability, `appTopLevelWindow` or `app` must be provided on session startup. | `My App Window Title` or `My App Window Title - .*` | | appium:newCommandTimeout | The number of seconds the to wait for clients to send commands before deciding that the client has gone away and the session should shut down. Default one minute (60). | `120` | diff --git a/src/FlaUI.WebDriver.UITests/SessionTests.cs b/src/FlaUI.WebDriver.UITests/SessionTests.cs index 85f71e3..76e66c5 100644 --- a/src/FlaUI.WebDriver.UITests/SessionTests.cs +++ b/src/FlaUI.WebDriver.UITests/SessionTests.cs @@ -10,13 +10,23 @@ namespace FlaUI.WebDriver.UITests public class SessionTests { [Test] - public void NewSession_CapabilitiesDoNotMatch_ReturnsError() + public void NewSession_PlatformNameMissing_ReturnsError() { var emptyOptions = FlaUIDriverOptions.Empty(); var newSession = () => new RemoteWebDriver(WebDriverFixture.WebDriverUrl, emptyOptions); - Assert.That(newSession, Throws.TypeOf().With.Message.EqualTo("Required capabilities did not match. Capability `platformName` with value `windows` is required (SessionNotCreated)")); + Assert.That(newSession, Throws.TypeOf().With.Message.EqualTo("Required capabilities did not match. Capability `platformName` with value `windows` is required, and one of appium:app, appium:appTopLevelWindow or appium:appTopLevelWindowTitleMatch must be passed as a capability (SessionNotCreated)")); + } + + [Test] + public void NewSession_AllAppCapabilitiesMissing_ReturnsError() + { + var emptyOptions = FlaUIDriverOptions.Empty(); + emptyOptions.AddAdditionalOption("appium:platformName", "windows"); + var newSession = () => new RemoteWebDriver(WebDriverFixture.WebDriverUrl, emptyOptions); + + Assert.That(newSession, Throws.TypeOf().With.Message.EqualTo("Required capabilities did not match. Capability `platformName` with value `windows` is required, and one of appium:app, appium:appTopLevelWindow or appium:appTopLevelWindowTitleMatch must be passed as a capability (SessionNotCreated)")); } [Test] @@ -54,6 +64,28 @@ public void NewSession_AppNotAString_Throws(object value) Throws.TypeOf().With.Message.EqualTo("Capability appium:app must be a string")); } + [Test] + public void NewSession_AppWorkingDir_IsSupported() + { + var driverOptions = FlaUIDriverOptions.TestApp(); + driverOptions.AddAdditionalOption("appium:appWorkingDir", "C:\\"); + using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions); + + var title = driver.Title; + + Assert.That(title, Is.EqualTo("FlaUI WPF Test App")); + } + + [Test] + public void NewSession_NotSupportedCapability_Throws() + { + var driverOptions = FlaUIDriverOptions.TestApp(); + driverOptions.AddAdditionalOption("unknown:unknown", "value"); + + Assert.That(() => new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions), + Throws.TypeOf().With.Message.EqualTo("Required capabilities did not match. Capability `platformName` with value `windows` is required, and one of appium:app, appium:appTopLevelWindow or appium:appTopLevelWindowTitleMatch must be passed as a capability (SessionNotCreated)")); + } + [Test] public void NewSession_AppTopLevelWindow_IsSupported() { diff --git a/src/FlaUI.WebDriver/Controllers/SessionController.cs b/src/FlaUI.WebDriver/Controllers/SessionController.cs index ee7583e..6cef097 100644 --- a/src/FlaUI.WebDriver/Controllers/SessionController.cs +++ b/src/FlaUI.WebDriver/Controllers/SessionController.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -29,9 +30,10 @@ public SessionController(ILogger logger, ISessionRepository s public async Task CreateNewSession([FromBody] CreateSessionRequest request) { var possibleCapabilities = GetPossibleCapabilities(request); - var matchingCapabilities = possibleCapabilities.Where( - capabilities => capabilities.TryGetValue("platformName", out var platformName) && platformName.GetString()?.ToLowerInvariant() == "windows" - ); + IDictionary? matchedCapabilities = null; + IEnumerable> matchingCapabilities = possibleCapabilities + .Where(capabilities => IsMatchingCapabilitySet(capabilities, out matchedCapabilities)) + .Select(capabillities => matchedCapabilities!); Core.Application? app; var capabilities = matchingCapabilities.FirstOrDefault(); @@ -40,7 +42,7 @@ public async Task CreateNewSession([FromBody] CreateSessionRequest return WebDriverResult.Error(new ErrorResponse { ErrorCode = "session not created", - Message = "Required capabilities did not match. Capability `platformName` with value `windows` is required" + Message = "Required capabilities did not match. Capability `platformName` with value `windows` is required, and one of appium:app, appium:appTopLevelWindow or appium:appTopLevelWindowTitleMatch must be passed as a capability" }); } if (TryGetStringCapability(capabilities, "appium:app", out var appPath)) @@ -61,6 +63,10 @@ public async Task CreateNewSession([FromBody] CreateSessionRequest else { var processStartInfo = new ProcessStartInfo(appPath, appArguments ?? ""); + if(TryGetStringCapability(capabilities, "appium:appWorkingDir", out var appWorkingDir)) + { + processStartInfo.WorkingDirectory = appWorkingDir; + } app = Core.Application.Launch(processStartInfo); } } @@ -98,7 +104,60 @@ public async Task CreateNewSession([FromBody] CreateSessionRequest })); } - private static bool TryGetStringCapability(Dictionary capabilities, string key, [MaybeNullWhen(false)] out string value) + private bool IsMatchingCapabilitySet(IDictionary capabilities, out IDictionary matchedCapabilities) + { + matchedCapabilities = new Dictionary(); + if (TryGetStringCapability(capabilities, "platformName", out var platformName) + && platformName.ToLowerInvariant() == "windows") + { + matchedCapabilities.Add("platformName", capabilities["platformName"]); + } + else + { + return false; + } + + if (TryGetStringCapability(capabilities, "appium:app", out var appPath)) + { + matchedCapabilities.Add("appium:app", capabilities["appium:app"]); + + if (appPath != "Root") + { + if(TryGetStringCapability(capabilities, "appium:appArguments", out _)) + { + matchedCapabilities.Add("appium:appArguments", capabilities["appium:appArguments"]); + } + if (!appPath.EndsWith("!App")) + { + if (TryGetStringCapability(capabilities, "appium:appWorkingDir", out _)) + { + matchedCapabilities.Add("appium:appWorkingDir", capabilities["appium:appWorkingDir"]); + } + } + } + } + else if (TryGetStringCapability(capabilities, "appium:appTopLevelWindow", out _)) + { + matchedCapabilities.Add("appium:appTopLevelWindow", capabilities["appium:appTopLevelWindow"]); + } + else if (TryGetStringCapability(capabilities, "appium:appTopLevelWindowTitleMatch", out _)) + { + matchedCapabilities.Add("appium:appTopLevelWindowTitleMatch", capabilities["appium:appTopLevelWindowTitleMatch"]); + } + else + { + return false; + } + + if (TryGetNumberCapability(capabilities, "appium:newCommandTimeout", out _)) + { + matchedCapabilities.Add("appium:newCommandTimeout", capabilities["appium:newCommandTimeout"]); ; + } + + return matchedCapabilities.Count == capabilities.Count; + } + + private static bool TryGetStringCapability(IDictionary capabilities, string key, [MaybeNullWhen(false)] out string value) { if(capabilities.TryGetValue(key, out var valueJson)) { @@ -115,7 +174,7 @@ private static bool TryGetStringCapability(Dictionary capab return false; } - private static bool TryGetNumberCapability(Dictionary capabilities, string key, out double value) + private static bool TryGetNumberCapability(IDictionary capabilities, string key, out double value) { if (capabilities.TryGetValue(key, out var valueJson)) { @@ -178,14 +237,14 @@ private static Process GetProcessByMainWindowHandle(string appTopLevelWindowStri return process; } - private static IEnumerable> GetPossibleCapabilities(CreateSessionRequest request) + private static IEnumerable> GetPossibleCapabilities(CreateSessionRequest request) { var requiredCapabilities = request.Capabilities.AlwaysMatch ?? new Dictionary(); var allFirstMatchCapabilities = request.Capabilities.FirstMatch ?? new List>(new[] { new Dictionary() }); return allFirstMatchCapabilities.Select(firstMatchCapabilities => MergeCapabilities(firstMatchCapabilities, requiredCapabilities)); } - private static Dictionary MergeCapabilities(Dictionary firstMatchCapabilities, Dictionary requiredCapabilities) + private static IDictionary MergeCapabilities(IDictionary firstMatchCapabilities, IDictionary requiredCapabilities) { var duplicateKeys = firstMatchCapabilities.Keys.Intersect(requiredCapabilities.Keys); if (duplicateKeys.Any()) diff --git a/src/FlaUI.WebDriver/Models/CreateSessionResponse.cs b/src/FlaUI.WebDriver/Models/CreateSessionResponse.cs index 3dacf7e..fa9ed75 100644 --- a/src/FlaUI.WebDriver/Models/CreateSessionResponse.cs +++ b/src/FlaUI.WebDriver/Models/CreateSessionResponse.cs @@ -6,6 +6,6 @@ namespace FlaUI.WebDriver.Models public class CreateSessionResponse { public string SessionId { get; set; } = null!; - public Dictionary Capabilities { get; set; } = new Dictionary(); + public IDictionary Capabilities { get; set; } = new Dictionary(); } }