Skip to content

Commit

Permalink
Merge pull request #19 from FlaUI/app-working-dir-capability
Browse files Browse the repository at this point in the history
Add appium:appWorkingDir capability and return only matched
  • Loading branch information
aristotelos authored Apr 22, 2024
2 parents fef5489 + 2fb5762 commit e1f7914
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 11 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, `<package family name>!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` |
Expand Down
36 changes: 34 additions & 2 deletions src/FlaUI.WebDriver.UITests/SessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InvalidOperationException>().With.Message.EqualTo("Required capabilities did not match. Capability `platformName` with value `windows` is required (SessionNotCreated)"));
Assert.That(newSession, Throws.TypeOf<InvalidOperationException>().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<InvalidOperationException>().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]
Expand Down Expand Up @@ -54,6 +64,28 @@ public void NewSession_AppNotAString_Throws(object value)
Throws.TypeOf<WebDriverArgumentException>().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<InvalidOperationException>().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()
{
Expand Down
75 changes: 67 additions & 8 deletions src/FlaUI.WebDriver/Controllers/SessionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -29,9 +30,10 @@ public SessionController(ILogger<SessionController> logger, ISessionRepository s
public async Task<ActionResult> CreateNewSession([FromBody] CreateSessionRequest request)
{
var possibleCapabilities = GetPossibleCapabilities(request);
var matchingCapabilities = possibleCapabilities.Where(
capabilities => capabilities.TryGetValue("platformName", out var platformName) && platformName.GetString()?.ToLowerInvariant() == "windows"
);
IDictionary<string, JsonElement>? matchedCapabilities = null;
IEnumerable<IDictionary<string, JsonElement>> matchingCapabilities = possibleCapabilities
.Where(capabilities => IsMatchingCapabilitySet(capabilities, out matchedCapabilities))
.Select(capabillities => matchedCapabilities!);

Core.Application? app;
var capabilities = matchingCapabilities.FirstOrDefault();
Expand All @@ -40,7 +42,7 @@ public async Task<ActionResult> 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))
Expand All @@ -61,6 +63,10 @@ public async Task<ActionResult> 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);
}
}
Expand Down Expand Up @@ -98,7 +104,60 @@ public async Task<ActionResult> CreateNewSession([FromBody] CreateSessionRequest
}));
}

private static bool TryGetStringCapability(Dictionary<string, JsonElement> capabilities, string key, [MaybeNullWhen(false)] out string value)
private bool IsMatchingCapabilitySet(IDictionary<string, JsonElement> capabilities, out IDictionary<string, JsonElement> matchedCapabilities)
{
matchedCapabilities = new Dictionary<string, JsonElement>();
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<string, JsonElement> capabilities, string key, [MaybeNullWhen(false)] out string value)
{
if(capabilities.TryGetValue(key, out var valueJson))
{
Expand All @@ -115,7 +174,7 @@ private static bool TryGetStringCapability(Dictionary<string, JsonElement> capab
return false;
}

private static bool TryGetNumberCapability(Dictionary<string, JsonElement> capabilities, string key, out double value)
private static bool TryGetNumberCapability(IDictionary<string, JsonElement> capabilities, string key, out double value)
{
if (capabilities.TryGetValue(key, out var valueJson))
{
Expand Down Expand Up @@ -178,14 +237,14 @@ private static Process GetProcessByMainWindowHandle(string appTopLevelWindowStri
return process;
}

private static IEnumerable<Dictionary<string, JsonElement>> GetPossibleCapabilities(CreateSessionRequest request)
private static IEnumerable<IDictionary<string, JsonElement>> GetPossibleCapabilities(CreateSessionRequest request)
{
var requiredCapabilities = request.Capabilities.AlwaysMatch ?? new Dictionary<string, JsonElement>();
var allFirstMatchCapabilities = request.Capabilities.FirstMatch ?? new List<Dictionary<string, JsonElement>>(new[] { new Dictionary<string, JsonElement>() });
return allFirstMatchCapabilities.Select(firstMatchCapabilities => MergeCapabilities(firstMatchCapabilities, requiredCapabilities));
}

private static Dictionary<string, JsonElement> MergeCapabilities(Dictionary<string, JsonElement> firstMatchCapabilities, Dictionary<string, JsonElement> requiredCapabilities)
private static IDictionary<string, JsonElement> MergeCapabilities(IDictionary<string, JsonElement> firstMatchCapabilities, IDictionary<string, JsonElement> requiredCapabilities)
{
var duplicateKeys = firstMatchCapabilities.Keys.Intersect(requiredCapabilities.Keys);
if (duplicateKeys.Any())
Expand Down
2 changes: 1 addition & 1 deletion src/FlaUI.WebDriver/Models/CreateSessionResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ namespace FlaUI.WebDriver.Models
public class CreateSessionResponse
{
public string SessionId { get; set; } = null!;
public Dictionary<string, JsonElement> Capabilities { get; set; } = new Dictionary<string, JsonElement>();
public IDictionary<string, JsonElement> Capabilities { get; set; } = new Dictionary<string, JsonElement>();
}
}

0 comments on commit e1f7914

Please sign in to comment.