Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using OpenQA.Selenium.Appium;
using Plugin.Maui.UITestHelpers.Core;

namespace Plugin.Maui.UITestHelpers.Appium;

public class AppiumAndroidAlertActions : ICommandExecutionGroup
{
const string GetAlertsCommand = "getAlerts";
const string GetAlertButtonsCommand = "getAlertButtons";
const string GetAlertTextCommand = "getAlertText";

readonly List<string> _commands = new()
{
GetAlertsCommand,
GetAlertButtonsCommand,
GetAlertTextCommand,
};
readonly AppiumApp _appiumApp;

public AppiumAndroidAlertActions(AppiumApp appiumApp)
{
_appiumApp = appiumApp;
}

public bool IsCommandSupported(string commandName)
{
return _commands.Contains(commandName, StringComparer.OrdinalIgnoreCase);
}

public CommandResponse Execute(string commandName, IDictionary<string, object> parameters)
{
return commandName switch
{
GetAlertsCommand => GetAlerts(parameters),
GetAlertButtonsCommand => GetAlertButtons(parameters),
GetAlertTextCommand => GetAlertText(parameters),
_ => CommandResponse.FailedEmptyResponse,
};
}

CommandResponse GetAlerts(IDictionary<string, object> parameters)
{
var alerts = _appiumApp.Query.ById("parentPanel");

if (alerts is null || alerts.Count == 0)
return CommandResponse.FailedEmptyResponse;

return new CommandResponse(alerts, CommandResponseResult.Success);
}

CommandResponse GetAlertButtons(IDictionary<string, object> parameters)
{
var alert = GetAppiumElement(parameters["element"]);
if (alert is null)
return CommandResponse.FailedEmptyResponse;

var items = AppiumQuery.ByClass("android.widget.ListView")
.FindElements(alert, _appiumApp)
.FirstOrDefault()
?.ByClass("android.widget.TextView");

var buttons = AppiumQuery.ByClass("android.widget.Button")
.FindElements(alert, _appiumApp);

var all = new List<IUIElement>();
if (items is not null)
all.AddRange(items);
all.AddRange(buttons);

return new CommandResponse(all, CommandResponseResult.Success);
}

CommandResponse GetAlertText(IDictionary<string, object> parameters)
{
var alert = GetAppiumElement(parameters["element"]);
if (alert is null)
return CommandResponse.FailedEmptyResponse;

var text = AppiumQuery.ByClass("android.widget.TextView").FindElements(alert, _appiumApp);
var strings = text.Select(t => t.GetText()).ToList();

return new CommandResponse(strings, CommandResponseResult.Success);
}

static AppiumElement? GetAppiumElement(object element) =>
element switch
{
AppiumElement appiumElement => appiumElement,
AppiumDriverElement driverElement => driverElement.AppiumElement,
_ => null
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using OpenQA.Selenium.Appium;
using Plugin.Maui.UITestHelpers.Core;

namespace Plugin.Maui.UITestHelpers.Appium;

public abstract class AppiumAppleAlertActions : ICommandExecutionGroup
{
const string GetAlertsCommand = "getAlerts";
const string GetAlertButtonsCommand = "getAlertButtons";
const string GetAlertTextCommand = "getAlertText";

readonly List<string> _commands = new()
{
GetAlertsCommand,
GetAlertButtonsCommand,
GetAlertTextCommand,
};

protected readonly AppiumApp _appiumApp;

public AppiumAppleAlertActions(AppiumApp appiumApp)
{
_appiumApp = appiumApp;
}

public virtual bool IsCommandSupported(string commandName) =>
_commands.Contains(commandName, StringComparer.OrdinalIgnoreCase);

public virtual CommandResponse Execute(string commandName, IDictionary<string, object> parameters) =>
commandName switch
{
GetAlertsCommand => GetAlerts(parameters),
GetAlertButtonsCommand => GetAlertButtons(parameters),
GetAlertTextCommand => GetAlertText(parameters),
_ => CommandResponse.FailedEmptyResponse,
};

protected abstract IReadOnlyCollection<IUIElement> OnGetAlerts(AppiumApp appiumApp, IDictionary<string, object> parameters);

CommandResponse GetAlerts(IDictionary<string, object> parameters)
{
var alerts = OnGetAlerts(_appiumApp, parameters);

if (alerts is null || alerts.Count == 0)
return CommandResponse.FailedEmptyResponse;

return new CommandResponse(alerts, CommandResponseResult.Success);
}

CommandResponse GetAlertButtons(IDictionary<string, object> parameters)
{
var alert = GetAppiumElement(parameters["element"]);
if (alert is null)
return CommandResponse.FailedEmptyResponse;

var buttons = AppiumQuery.ByClass("XCUIElementTypeButton").FindElements(alert, _appiumApp);

return new CommandResponse(buttons, CommandResponseResult.Success);
}

CommandResponse GetAlertText(IDictionary<string, object> parameters)
{
var alert = GetAppiumElement(parameters["element"]);
if (alert is null)
return CommandResponse.FailedEmptyResponse;

var text = AppiumQuery.ByClass("XCUIElementTypeStaticText").FindElements(alert, _appiumApp);
var strings = text.Select(t => t.GetText()).ToList();

return new CommandResponse(strings, CommandResponseResult.Success);
}

protected static AppiumElement? GetAppiumElement(object element) =>
element switch
{
AppiumElement appiumElement => appiumElement,
AppiumDriverElement driverElement => driverElement.AppiumElement,
_ => null
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Plugin.Maui.UITestHelpers.Core;

namespace Plugin.Maui.UITestHelpers.Appium;

public class AppiumCatalystAlertActions : AppiumAppleAlertActions
{
// Selects the inner "popover contents" of a popover window.
const string PossibleActionSheetXPath =
"/XCUIElementTypeApplication/XCUIElementTypeWindow/XCUIElementTypePopover";

const string DismissAlertCommand = "dismissAlert";

readonly List<string> _commands = new()
{
DismissAlertCommand,
};

public AppiumCatalystAlertActions(AppiumApp appiumApp)
: base(appiumApp)
{
}

public override bool IsCommandSupported(string commandName) =>
_commands.Contains(commandName, StringComparer.OrdinalIgnoreCase) || base.IsCommandSupported(commandName);

public override CommandResponse Execute(string commandName, IDictionary<string, object> parameters) =>
commandName switch
{
DismissAlertCommand => DismissAlert(parameters),
_ => base.Execute(commandName, parameters),
};

CommandResponse DismissAlert(IDictionary<string, object> parameters)
{
var alert = GetAppiumElement(parameters["element"]);
if (alert is null)
return CommandResponse.FailedEmptyResponse;

// XCUIElementTypePopover == 18
if (!"18".Equals(alert.GetAttribute("elementType"), StringComparison.OrdinalIgnoreCase))
return CommandResponse.FailedEmptyResponse;

var dismissRegions = AppiumQuery.ById("PopoverDismissRegion").FindElements(_appiumApp).ToList();
for (var i = dismissRegions.Count - 1; i >= 0; i--)
{
var region = GetAppiumElement(dismissRegions[i])!;
if ("true".Equals(region.GetAttribute("enabled"), StringComparison.OrdinalIgnoreCase))
{
region.Click();
return CommandResponse.SuccessEmptyResponse;
}
}

return CommandResponse.FailedEmptyResponse;
}

protected override IReadOnlyCollection<IUIElement> OnGetAlerts(AppiumApp appiumApp, IDictionary<string, object> parameters)
{
// Catalyst uses action sheets for alerts and macOS 14
var alerts = appiumApp.FindElements(AppiumQuery.ByClass("XCUIElementTypeSheet"));

// But it also uses popovers for action sheets on macOS 13
if (alerts is null || alerts.Count == 0)
alerts = appiumApp.FindElements(AppiumQuery.ByXPath(PossibleActionSheetXPath));

return alerts;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Plugin.Maui.UITestHelpers.Core;

namespace Plugin.Maui.UITestHelpers.Appium;

public class AppiumIOSAlertActions : AppiumAppleAlertActions
{
// Selects VISIBLE "Other" elements that are the direct child of
// a VISIBLE window AND are OVERLAYED on top of the first window.
const string PossibleAlertXPath =
"//XCUIElementTypeWindow[@visible='true']/XCUIElementTypeOther[@visible='true' and @index > 0]";

public AppiumIOSAlertActions(AppiumApp appiumApp)
: base(appiumApp)
{
}

protected override IReadOnlyCollection<IUIElement> OnGetAlerts(AppiumApp appiumApp, IDictionary<string, object> parameters)
{
// First try the type used on iOS.
var alerts = appiumApp.FindElements(AppiumQuery.ByClass("XCUIElementTypeAlert"));

// It appears iOS sometimes uses the XCUIElementTypeOther class for action sheets
// so we need a way to do a more fuzzy check.
if (alerts is null || alerts.Count == 0)
alerts = appiumApp.FindElements(AppiumQuery.ByXPath(PossibleAlertXPath));

return alerts;
}
}
1 change: 1 addition & 0 deletions src/Plugin.Maui.UITestHelpers.Appium/AppiumAndroidApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ private AppiumAndroidApp(Uri remoteAddress, IConfig config)
: base(new AndroidDriver(remoteAddress, GetOptions(config)), config)
{
_commandExecutor.AddCommandGroup(new AppiumAndroidVirtualKeyboardActions(this));
_commandExecutor.AddCommandGroup(new AppiumAndroidAlertActions(this));
}

public static AppiumAndroidApp CreateAndroidApp(Uri remoteAddress, IConfig config)
Expand Down
1 change: 1 addition & 0 deletions src/Plugin.Maui.UITestHelpers.Appium/AppiumCatalystApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public AppiumCatalystApp(Uri remoteAddress, IConfig config)
{
_commandExecutor.AddCommandGroup(new AppiumCatalystMouseActions(this));
_commandExecutor.AddCommandGroup(new AppiumCatalystTouchActions(this));
_commandExecutor.AddCommandGroup(new AppiumCatalystAlertActions(this));
}

public override ApplicationState AppState
Expand Down
1 change: 1 addition & 0 deletions src/Plugin.Maui.UITestHelpers.Appium/AppiumIOSApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public AppiumIOSApp(Uri remoteAddress, IConfig config)
_commandExecutor.AddCommandGroup(new AppiumIOSMouseActions(this));
_commandExecutor.AddCommandGroup(new AppiumIOSTouchActions(this));
_commandExecutor.AddCommandGroup(new AppiumIOSVirtualKeyboardActions(this));
_commandExecutor.AddCommandGroup(new AppiumIOSAlertActions(this));
}

public override ApplicationState AppState
Expand Down
13 changes: 13 additions & 0 deletions src/Plugin.Maui.UITestHelpers.Appium/AppiumQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public class AppiumQuery : IQuery
const string IdQuery = IdToken + "={0}";
const string NameQuery = NameToken + "={0}";
const string AccessibilityQuery = AccessibilityToken + "={0}";
const string XPathToken = "xpath";
const string ClassQuery = ClassToken + "={0}";
const string XPathQuery = XPathToken + "={0}";
readonly string _queryStr;

public AppiumQuery(string queryStr)
Expand Down Expand Up @@ -48,6 +50,11 @@ IQuery IQuery.ByName(string nameQuery)
return new AppiumQuery(this, string.Format(NameQuery, nameQuery));
}

IQuery IQuery.ByXPath(string xpath)
{
return new AppiumQuery(this, string.Format(XPathQuery, Uri.EscapeDataString(xpath)));
}

public static AppiumQuery ById(string id)
{
return new AppiumQuery(string.Format(IdQuery, id));
Expand All @@ -58,6 +65,11 @@ public static AppiumQuery ByName(string nameQuery)
return new AppiumQuery(string.Format(NameQuery, nameQuery));
}

public static AppiumQuery ByXPath(string xpath)
{
return new AppiumQuery(string.Format(XPathQuery, Uri.EscapeDataString(xpath)));
}

public static AppiumQuery ByAccessibilityId(string id)
{
return new AppiumQuery(string.Format(AccessibilityQuery, id));
Expand Down Expand Up @@ -184,6 +196,7 @@ private static By GetQueryBy(string token, string value)
NameToken => MobileBy.Name(value),
AccessibilityToken => MobileBy.AccessibilityId(value),
IdToken => MobileBy.Id(value),
XPathToken => MobileBy.XPath(Uri.UnescapeDataString(value)),
_ => throw new ArgumentException("Unknown query type"),
};
}
Expand Down
Loading