From f4a37b576985cde51a6399b04e45ca861ddc9fe0 Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Wed, 8 May 2024 17:30:45 -0300 Subject: [PATCH 1/3] update files following the #21305 from the .NET MAUI PR --- .../Actions/AppiumCatalystMouseActions.cs | 63 ++++ ...tions.cs => AppiumCatalystTouchActions.cs} | 12 +- .../Actions/AppiumIOSMouseActions.cs | 65 ++++ ...terActions.cs => AppiumIOSTouchActions.cs} | 12 +- .../Actions/AppiumMouseActions.cs | 220 ++++++++++++ .../Actions/AppiumTouchActions.cs | 331 ++++++++++++++++++ 6 files changed, 691 insertions(+), 12 deletions(-) create mode 100644 src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumCatalystMouseActions.cs rename src/Plugin.Maui.UITestHelpers.Appium/Actions/{AppiumCatalystPointerActions.cs => AppiumCatalystTouchActions.cs} (87%) create mode 100644 src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumIOSMouseActions.cs rename src/Plugin.Maui.UITestHelpers.Appium/Actions/{AppiumIOSPointerActions.cs => AppiumIOSTouchActions.cs} (90%) create mode 100644 src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumMouseActions.cs create mode 100644 src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumTouchActions.cs diff --git a/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumCatalystMouseActions.cs b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumCatalystMouseActions.cs new file mode 100644 index 00000000..0b6e0059 --- /dev/null +++ b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumCatalystMouseActions.cs @@ -0,0 +1,63 @@ +using OpenQA.Selenium.Appium; +using Plugin.Maui.UITestHelpers.Core; + +namespace Plugin.Maui.UITestHelpers.Appium +{ + public class AppiumCatalystMouseActions : ICommandExecutionGroup + { + const string DoubleClickCommand = "doubleClick"; + + readonly List _commands = new() + { + DoubleClickCommand, + }; + readonly AppiumApp _appiumApp; + + public AppiumCatalystMouseActions(AppiumApp appiumApp) + { + _appiumApp = appiumApp; + } + + public bool IsCommandSupported(string commandName) + { + return _commands.Contains(commandName, StringComparer.OrdinalIgnoreCase); + } + + public CommandResponse Execute(string commandName, IDictionary parameters) + { + return commandName switch + { + DoubleClickCommand => DoubleClick(parameters), + _ => CommandResponse.FailedEmptyResponse, + }; + } + + CommandResponse DoubleClick(IDictionary parameters) + { + var element = GetAppiumElement(parameters["element"]); + + if (element != null) + { + _appiumApp.Driver.ExecuteScript("macos: doubleClick", new Dictionary + { + { "elementId", element.Id }, + }); + } + return CommandResponse.SuccessEmptyResponse; + } + + static AppiumElement? GetAppiumElement(object element) + { + if (element is AppiumElement appiumElement) + { + return appiumElement; + } + else if (element is AppiumDriverElement driverElement) + { + return driverElement.AppiumElement; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumCatalystPointerActions.cs b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumCatalystTouchActions.cs similarity index 87% rename from src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumCatalystPointerActions.cs rename to src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumCatalystTouchActions.cs index 1f59042d..910ba809 100644 --- a/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumCatalystPointerActions.cs +++ b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumCatalystTouchActions.cs @@ -3,19 +3,19 @@ namespace Plugin.Maui.UITestHelpers.Appium { - public class AppiumCatalystPointerActions : ICommandExecutionGroup + public class AppiumCatalystTouchActions : ICommandExecutionGroup { - const string DoubleClickCommand = "doubleClick"; + const string DoubleTapCommand = "doubleTap"; const string DragAndDropCommand = "dragAndDrop"; readonly List _commands = new() { - DoubleClickCommand, + DoubleTapCommand, DragAndDropCommand, }; readonly AppiumApp _appiumApp; - public AppiumCatalystPointerActions(AppiumApp appiumApp) + public AppiumCatalystTouchActions(AppiumApp appiumApp) { _appiumApp = appiumApp; } @@ -29,13 +29,13 @@ public CommandResponse Execute(string commandName, IDictionary p { return commandName switch { - DoubleClickCommand => DoubleClick(parameters), + DoubleTapCommand => DoubleTap(parameters), DragAndDropCommand => DragAndDrop(parameters), _ => CommandResponse.FailedEmptyResponse, }; } - CommandResponse DoubleClick(IDictionary parameters) + CommandResponse DoubleTap(IDictionary parameters) { var element = GetAppiumElement(parameters["element"]); diff --git a/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumIOSMouseActions.cs b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumIOSMouseActions.cs new file mode 100644 index 00000000..645de5e0 --- /dev/null +++ b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumIOSMouseActions.cs @@ -0,0 +1,65 @@ +using OpenQA.Selenium.Appium; +using Plugin.Maui.UITestHelpers.Core; + + +namespace Plugin.Maui.UITestHelpers.Appium +{ + public class AppiumIOSMouseActions : ICommandExecutionGroup + { + const string DoubleClickCommand = "doubleClick"; + + readonly List _commands = new() + { + DoubleClickCommand, + }; + readonly AppiumApp _appiumApp; + + public AppiumIOSMouseActions(AppiumApp appiumApp) + { + _appiumApp = appiumApp; + } + + public bool IsCommandSupported(string commandName) + { + return _commands.Contains(commandName, StringComparer.OrdinalIgnoreCase); + } + + public CommandResponse Execute(string commandName, IDictionary parameters) + { + return commandName switch + { + DoubleClickCommand => DoubleClick(parameters), + _ => CommandResponse.FailedEmptyResponse, + }; + } + + CommandResponse DoubleClick(IDictionary parameters) + { + var element = GetAppiumElement(parameters["element"]); + + if (element != null) + { + _appiumApp.Driver.ExecuteScript("mobile: doubleTap", new Dictionary + { + { "elementId", element.Id }, + }); + } + + return CommandResponse.SuccessEmptyResponse; + } + + static AppiumElement? GetAppiumElement(object element) + { + if (element is AppiumElement appiumElement) + { + return appiumElement; + } + else if (element is AppiumDriverElement driverElement) + { + return driverElement.AppiumElement; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumIOSPointerActions.cs b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumIOSTouchActions.cs similarity index 90% rename from src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumIOSPointerActions.cs rename to src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumIOSTouchActions.cs index 1f2560b9..ee947f60 100644 --- a/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumIOSPointerActions.cs +++ b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumIOSTouchActions.cs @@ -3,19 +3,19 @@ namespace Plugin.Maui.UITestHelpers.Appium { - public class AppiumIOSPointerActions : ICommandExecutionGroup + public class AppiumIOSTouchActions : ICommandExecutionGroup { - const string DoubleClickCommand = "doubleClick"; + const string DoubleTapCommand = "doubleTap"; const string DragAndDropCommand = "dragAndDrop"; readonly List _commands = new() { - DoubleClickCommand, + DoubleTapCommand, DragAndDropCommand }; readonly AppiumApp _appiumApp; - public AppiumIOSPointerActions(AppiumApp appiumApp) + public AppiumIOSTouchActions(AppiumApp appiumApp) { _appiumApp = appiumApp; } @@ -29,13 +29,13 @@ public CommandResponse Execute(string commandName, IDictionary p { return commandName switch { - DoubleClickCommand => DoubleClick(parameters), + DoubleTapCommand => DoubleTap(parameters), DragAndDropCommand => DragAndDrop(parameters), _ => CommandResponse.FailedEmptyResponse, }; } - CommandResponse DoubleClick(IDictionary parameters) + CommandResponse DoubleTap(IDictionary parameters) { var element = GetAppiumElement(parameters["element"]); diff --git a/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumMouseActions.cs b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumMouseActions.cs new file mode 100644 index 00000000..928261bb --- /dev/null +++ b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumMouseActions.cs @@ -0,0 +1,220 @@ +using System.Drawing; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Interactions; +using OpenQA.Selenium.Interactions; +using Plugin.Maui.UITestHelpers.Core; + + +namespace Plugin.Maui.UITestHelpers.Appium +{ + public class AppiumMouseActions : ICommandExecutionGroup + { + const string ClickCommand = "click"; + const string ClickCoordinatesCommand = "clickCoordinates"; + const string DoubleClickCommand = "doubleClick"; + const string DoubleClickCoordinatesCommand = "doubleClickCoordinates"; + const string LongPressCommand = "longPress"; + + readonly AppiumApp _appiumApp; + + readonly List _commands = new() + { + ClickCommand, + ClickCoordinatesCommand, + DoubleClickCommand, + DoubleClickCoordinatesCommand, + LongPressCommand, + }; + + public AppiumMouseActions(AppiumApp appiumApp) + { + _appiumApp = appiumApp; + } + + public bool IsCommandSupported(string commandName) + { + return _commands.Contains(commandName, StringComparer.OrdinalIgnoreCase); + } + + public CommandResponse Execute(string commandName, IDictionary parameters) + { + return commandName switch + { + ClickCommand => Click(parameters), + ClickCoordinatesCommand => ClickCoordinates(parameters), + DoubleClickCommand => DoubleClick(parameters), + DoubleClickCoordinatesCommand => DoubleClickCoordinates(parameters), + LongPressCommand => LongPress(parameters), + _ => CommandResponse.FailedEmptyResponse, + }; + } + + CommandResponse Click(IDictionary parameters) + { + if (parameters.TryGetValue("element", out var val)) + { + AppiumElement? element = GetAppiumElement(parameters["element"]); + if (element == null) + { + return CommandResponse.FailedEmptyResponse; + } + return ClickElement(element); + } + else if (parameters.TryGetValue("x", out var x) && + parameters.TryGetValue("y", out var y)) + { + return ClickCoordinates(Convert.ToSingle(x), Convert.ToSingle(y)); + } + + return CommandResponse.FailedEmptyResponse; + } + + CommandResponse DoubleClickCoordinates(IDictionary parameters) + { + if (parameters.TryGetValue("x", out var x) && + parameters.TryGetValue("y", out var y)) + { + return DoubleClickCoordinates(Convert.ToSingle(x), Convert.ToSingle(y)); + } + + return CommandResponse.FailedEmptyResponse; + } + + CommandResponse ClickCoordinates(IDictionary parameters) + { + if (parameters.TryGetValue("x", out var x) && + parameters.TryGetValue("y", out var y)) + { + return ClickCoordinates(Convert.ToSingle(x), Convert.ToSingle(y)); + } + + return CommandResponse.FailedEmptyResponse; + } + + CommandResponse ClickElement(AppiumElement element) + { + try + { + element.Click(); + return CommandResponse.SuccessEmptyResponse; + } + catch (InvalidOperationException) + { + return ProcessException(); + } + catch (WebDriverException) + { + return ProcessException(); + } + + CommandResponse ProcessException() + { + // Some elements aren't "clickable" from an automation perspective (e.g., Frame renders as a Border + // with content in it; if the content is just a TextBlock, we'll end up here) + + // All is not lost; we can figure out the location of the element in in the application window and Tap in that spot + PointF p = ElementToClickablePoint(element); + ClickCoordinates(p.X, p.Y); + return CommandResponse.SuccessEmptyResponse; + } + } + + CommandResponse ClickCoordinates(float x, float y) + { + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Mouse); + var sequence = new ActionSequence(touchDevice, 0); + sequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, (int)x, (int)y, TimeSpan.FromMilliseconds(5))); + sequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + _appiumApp.Driver.PerformActions(new List { sequence }); + + return CommandResponse.SuccessEmptyResponse; + } + + CommandResponse DoubleClick(IDictionary parameters) + { + var element = GetAppiumElement(parameters["element"]); + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Mouse); + var sequence = new ActionSequence(touchDevice, 0); + sequence.AddAction(touchDevice.CreatePointerMove(element, 0, 0, TimeSpan.FromMilliseconds(5))); + + sequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePause(TimeSpan.FromMilliseconds(250))); + sequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + _appiumApp.Driver.PerformActions(new List { sequence }); + + return CommandResponse.SuccessEmptyResponse; + } + + CommandResponse DoubleClickCoordinates(float x, float y) + { + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Mouse); + var sequence = new ActionSequence(touchDevice, 0); + sequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, (int)x, (int)y, TimeSpan.FromMilliseconds(5))); + + sequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePause(TimeSpan.FromMilliseconds(250))); + sequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + _appiumApp.Driver.PerformActions(new List { sequence }); + + return CommandResponse.SuccessEmptyResponse; + } + + CommandResponse LongPress(IDictionary parameters) + { + var element = GetAppiumElement(parameters["element"]); + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Mouse); + var longPress = new ActionSequence(touchDevice, 0); + + longPress.AddAction(touchDevice.CreatePointerMove(element, 0, 0, TimeSpan.FromMilliseconds(0))); + longPress.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + longPress.AddAction(touchDevice.CreatePointerMove(element, 0, 0, TimeSpan.FromMilliseconds(2000))); + longPress.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + _appiumApp.Driver.PerformActions(new List { longPress }); + + return CommandResponse.SuccessEmptyResponse; + } + + CommandResponse TapCoordinates(IDictionary parameters) + { + if (parameters.TryGetValue("x", out var x) && + parameters.TryGetValue("y", out var y)) + { + return ClickCoordinates(Convert.ToSingle(x), Convert.ToSingle(y)); + } + + return CommandResponse.FailedEmptyResponse; + } + + static AppiumElement? GetAppiumElement(object element) + { + if (element is AppiumElement appiumElement) + { + return appiumElement; + } + else if (element is AppiumDriverElement driverElement) + { + return driverElement.AppiumElement; + } + + return null; + } + + static PointF ElementToClickablePoint(AppiumElement element) + { + string cpString = element.GetAttribute("ClickablePoint"); + string[] parts = cpString.Split(','); + float x = float.Parse(parts[0]); + float y = float.Parse(parts[1]); + + return new PointF(x, y); + } + } +} \ No newline at end of file diff --git a/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumTouchActions.cs b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumTouchActions.cs new file mode 100644 index 00000000..9fe6d21c --- /dev/null +++ b/src/Plugin.Maui.UITestHelpers.Appium/Actions/AppiumTouchActions.cs @@ -0,0 +1,331 @@ +using System.Diagnostics; +using System.Drawing; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Interactions; +using OpenQA.Selenium.Interactions; +using Plugin.Maui.UITestHelpers.Core; + + +namespace Plugin.Maui.UITestHelpers.Appium +{ + public class AppiumTouchActions : ICommandExecutionGroup + { + const string TapCommand = "tap"; + const string TapCoordinatesCommand = "tapCoordinates"; + const string DoubleTapCommand = "doubleTap"; + const string DoubleTapCoordinatesCommand = "doubleTapCoordinates"; + const string TouchAndHoldCommand = "touchAndHold"; + const string DragAndDropCommand = "dragAndDrop"; + const string ScrollToCommand = "scrollTo"; + const string DragCoordinatesCommand = "dragCoordinates"; + + readonly AppiumApp _appiumApp; + + readonly List _commands = new() + { + TapCommand, + TapCoordinatesCommand, + DoubleTapCommand, + DoubleTapCoordinatesCommand, + TouchAndHoldCommand, + DragAndDropCommand, + ScrollToCommand, + DragCoordinatesCommand + }; + + public AppiumTouchActions(AppiumApp appiumApp) + { + _appiumApp = appiumApp; + } + + public bool IsCommandSupported(string commandName) + { + return _commands.Contains(commandName, StringComparer.OrdinalIgnoreCase); + } + + public CommandResponse Execute(string commandName, IDictionary parameters) + { + return commandName switch + { + TapCommand => Tap(parameters), + TapCoordinatesCommand => TapCoordinates(parameters), + DoubleTapCommand => DoubleTap(parameters), + DoubleTapCoordinatesCommand => DoubleTapCoordinates(parameters), + TouchAndHoldCommand => TouchAndHold(parameters), + DragAndDropCommand => DragAndDrop(parameters), + ScrollToCommand => ScrollTo(parameters), + DragCoordinatesCommand => DragCoordinates(parameters), + _ => CommandResponse.FailedEmptyResponse, + }; + } + + CommandResponse Tap(IDictionary parameters) + { + if (parameters.TryGetValue("element", out var val)) + { + AppiumElement? element = GetAppiumElement(parameters["element"]); + if (element == null) + { + return CommandResponse.FailedEmptyResponse; + } + return TapElement(element); + } + else if (parameters.TryGetValue("x", out var x) && + parameters.TryGetValue("y", out var y)) + { + return TapCoordinates(Convert.ToSingle(x), Convert.ToSingle(y)); + } + + return CommandResponse.FailedEmptyResponse; + } + + CommandResponse TapElement(AppiumElement element) + { + try + { + element.Click(); + return CommandResponse.SuccessEmptyResponse; + } + catch (InvalidOperationException) + { + return ProcessException(); + } + catch (WebDriverException) + { + return ProcessException(); + } + + CommandResponse ProcessException() + { + // Some elements aren't "clickable" from an automation perspective (e.g., Frame renders as a Border + // with content in it; if the content is just a TextBlock, we'll end up here) + + // All is not lost; we can figure out the location of the element in in the application window and Tap in that spot + PointF p = ElementToClickablePoint(element); + TapCoordinates(p.X, p.Y); + return CommandResponse.SuccessEmptyResponse; + } + } + + CommandResponse TapCoordinates(float x, float y) + { + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + var sequence = new ActionSequence(touchDevice, 0); + sequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, (int)x, (int)y, TimeSpan.FromMilliseconds(5))); + sequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + _appiumApp.Driver.PerformActions(new List { sequence }); + + return CommandResponse.SuccessEmptyResponse; + } + + CommandResponse DoubleTap(IDictionary parameters) + { + var element = GetAppiumElement(parameters["element"]); + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + var sequence = new ActionSequence(touchDevice, 0); + sequence.AddAction(touchDevice.CreatePointerMove(element, 0, 0, TimeSpan.FromMilliseconds(5))); + + sequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePause(TimeSpan.FromMilliseconds(250))); + sequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + _appiumApp.Driver.PerformActions(new List { sequence }); + + return CommandResponse.SuccessEmptyResponse; + } + + CommandResponse DoubleTapCoordinates(float x, float y) + { + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + var sequence = new ActionSequence(touchDevice, 0); + sequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, (int)x, (int)y, TimeSpan.FromMilliseconds(5))); + + sequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePause(TimeSpan.FromMilliseconds(250))); + sequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + _appiumApp.Driver.PerformActions(new List { sequence }); + + return CommandResponse.SuccessEmptyResponse; + } + + CommandResponse TouchAndHold(IDictionary parameters) + { + var element = GetAppiumElement(parameters["element"]); + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + var longPress = new ActionSequence(touchDevice, 0); + + longPress.AddAction(touchDevice.CreatePointerMove(element, 0, 0, TimeSpan.FromMilliseconds(0))); + longPress.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + longPress.AddAction(touchDevice.CreatePointerMove(element, 0, 0, TimeSpan.FromMilliseconds(2000))); + longPress.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + _appiumApp.Driver.PerformActions(new List { longPress }); + + return CommandResponse.SuccessEmptyResponse; + } + + CommandResponse DragAndDrop(IDictionary actionParams) + { + AppiumElement? sourceAppiumElement = GetAppiumElement(actionParams["sourceElement"]); + AppiumElement? destinationAppiumElement = GetAppiumElement(actionParams["destinationElement"]); + + if (sourceAppiumElement != null && destinationAppiumElement != null) + { + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + var sequence = new ActionSequence(touchDevice, 0); + sequence.AddAction(touchDevice.CreatePointerMove(sourceAppiumElement, 0, 0, TimeSpan.FromMilliseconds(5))); + sequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + sequence.AddAction(touchDevice.CreatePause(TimeSpan.FromSeconds(1))); // Have to pause so the device doesn't think we are scrolling + sequence.AddAction(touchDevice.CreatePointerMove(destinationAppiumElement, 0, 0, TimeSpan.FromSeconds(1))); + sequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + _appiumApp.Driver.PerformActions(new List { sequence }); + + return CommandResponse.SuccessEmptyResponse; + } + return CommandResponse.FailedEmptyResponse; + } + + CommandResponse ScrollTo(IDictionary parameters) + { + // This method will keep scrolling in the specified direction until it finds an element + // which matches the query, or until it times out. + + bool down = !parameters.TryGetValue("down", out object? val) || (bool)val; + string toElementId = (string)parameters["elementId"]; + + // First we need to determine the area within which we'll make our scroll gestures + var window = _appiumApp?.Driver.Manage().Window + ?? throw new InvalidOperationException("Element to scroll within not specified, and no Window available. Cannot scroll."); + Size scrollAreaSize = window.Size; + + var x = scrollAreaSize.Width / 2; + var windowHeight = scrollAreaSize.Height; + var topEdgeOfScrollAction = windowHeight * 0.1; + var bottomEdgeOfScrollAction = windowHeight * 0.5; + var startY = down ? bottomEdgeOfScrollAction : topEdgeOfScrollAction; + var endY = down ? topEdgeOfScrollAction : bottomEdgeOfScrollAction; + + var timeout = TimeSpan.FromSeconds(15); + DateTime start = DateTime.Now; + + while (true) + { + try + { + IUIElement found = _appiumApp.FindElement(toElementId); + + if (found != null) + { + // Success! + return CommandResponse.SuccessEmptyResponse; + } + } + catch (TimeoutException) + { + // Haven't found it yet, keep scrolling + } + + long elapsed = DateTime.Now.Subtract(start).Ticks; + if (elapsed >= timeout.Ticks) + { + Debug.WriteLine($">>>>> {elapsed} ticks elapsed, timeout value is {timeout.Ticks}"); + throw new TimeoutException($"Timed out scrolling to {toElementId}"); + } + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + var scrollSequence = new ActionSequence(touchDevice, 0); + scrollSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, x, (int)startY, TimeSpan.Zero)); + scrollSequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + scrollSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, x, (int)endY, TimeSpan.FromMilliseconds(500))); + scrollSequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + _appiumApp.Driver.PerformActions([scrollSequence]); + } + } + + CommandResponse TapCoordinates(IDictionary parameters) + { + if (parameters.TryGetValue("x", out var x) && + parameters.TryGetValue("y", out var y)) + { + return TapCoordinates(Convert.ToSingle(x), Convert.ToSingle(y)); + } + + return CommandResponse.FailedEmptyResponse; + } + + CommandResponse DoubleTapCoordinates(IDictionary parameters) + { + if (parameters.TryGetValue("x", out var x) && + parameters.TryGetValue("y", out var y)) + { + return DoubleTapCoordinates(Convert.ToSingle(x), Convert.ToSingle(y)); + } + + return CommandResponse.FailedEmptyResponse; + } + + CommandResponse DragCoordinates(IDictionary parameters) + { + parameters.TryGetValue("fromX", out var fromX); + parameters.TryGetValue("fromY", out var fromY); + + parameters.TryGetValue("toX", out var toX); + parameters.TryGetValue("toY", out var toY); + + if (fromX is not null && fromY is not null && toX is not null && toY is not null) + { + DragCoordinates( + _appiumApp.Driver, + Convert.ToDouble(fromX), + Convert.ToDouble(fromY), + Convert.ToDouble(toX), + Convert.ToDouble(toY)); + + return CommandResponse.SuccessEmptyResponse; + } + + return CommandResponse.FailedEmptyResponse; + } + + static AppiumElement? GetAppiumElement(object element) + { + if (element is AppiumElement appiumElement) + { + return appiumElement; + } + else if (element is AppiumDriverElement driverElement) + { + return driverElement.AppiumElement; + } + + return null; + } + + static PointF ElementToClickablePoint(AppiumElement element) + { + string cpString = element.GetAttribute("ClickablePoint"); + string[] parts = cpString.Split(','); + float x = float.Parse(parts[0]); + float y = float.Parse(parts[1]); + + return new PointF(x, y); + } + + static void DragCoordinates(AppiumDriver driver, double fromX, double fromY, double toX, double toY) + { + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + var dragSequence = new ActionSequence(touchDevice, 0); + dragSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, (int)fromX, (int)fromY, TimeSpan.Zero)); + dragSequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact)); + dragSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, (int)toX, (int)toY, TimeSpan.FromMilliseconds(250))); + dragSequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact)); + driver.PerformActions([dragSequence]); + } + } +} \ No newline at end of file From 5649cb1ac957a8c0973bf5ee63099d82c56baa66 Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Wed, 8 May 2024 17:35:11 -0300 Subject: [PATCH 2/3] update AppiumApp to call the new CommandGroups --- src/Plugin.Maui.UITestHelpers.Appium/AppiumApp.cs | 3 ++- src/Plugin.Maui.UITestHelpers.Appium/AppiumCatalystApp.cs | 3 ++- src/Plugin.Maui.UITestHelpers.Appium/AppiumIOSApp.cs | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Plugin.Maui.UITestHelpers.Appium/AppiumApp.cs b/src/Plugin.Maui.UITestHelpers.Appium/AppiumApp.cs index 119f4d6b..ec6d0e10 100644 --- a/src/Plugin.Maui.UITestHelpers.Appium/AppiumApp.cs +++ b/src/Plugin.Maui.UITestHelpers.Appium/AppiumApp.cs @@ -17,7 +17,8 @@ public AppiumApp(AppiumDriver driver, IConfig config) _config = config ?? throw new ArgumentNullException(nameof(config)); _commandExecutor = new AppiumCommandExecutor(); - _commandExecutor.AddCommandGroup(new AppiumPointerActions(this)); + _commandExecutor.AddCommandGroup(new AppiumMouseActions(this)); + _commandExecutor.AddCommandGroup(new AppiumTouchActions(this)); _commandExecutor.AddCommandGroup(new AppiumTextActions()); _commandExecutor.AddCommandGroup(new AppiumGeneralActions()); _commandExecutor.AddCommandGroup(new AppiumVirtualKeyboardActions(this)); diff --git a/src/Plugin.Maui.UITestHelpers.Appium/AppiumCatalystApp.cs b/src/Plugin.Maui.UITestHelpers.Appium/AppiumCatalystApp.cs index 53798827..38c81ba7 100644 --- a/src/Plugin.Maui.UITestHelpers.Appium/AppiumCatalystApp.cs +++ b/src/Plugin.Maui.UITestHelpers.Appium/AppiumCatalystApp.cs @@ -10,7 +10,8 @@ public class AppiumCatalystApp : AppiumApp, ICatalystApp public AppiumCatalystApp(Uri remoteAddress, IConfig config) : base(new MacDriver(remoteAddress, GetOptions(config)), config) { - _commandExecutor.AddCommandGroup(new AppiumCatalystPointerActions(this)); + _commandExecutor.AddCommandGroup(new AppiumCatalystMouseActions(this)); + _commandExecutor.AddCommandGroup(new AppiumCatalystTouchActions(this)); } public override ApplicationState AppState diff --git a/src/Plugin.Maui.UITestHelpers.Appium/AppiumIOSApp.cs b/src/Plugin.Maui.UITestHelpers.Appium/AppiumIOSApp.cs index 84d56fb4..7a4826dd 100644 --- a/src/Plugin.Maui.UITestHelpers.Appium/AppiumIOSApp.cs +++ b/src/Plugin.Maui.UITestHelpers.Appium/AppiumIOSApp.cs @@ -11,7 +11,8 @@ public class AppiumIOSApp : AppiumApp, IIOSApp public AppiumIOSApp(Uri remoteAddress, IConfig config) : base(new IOSDriver(remoteAddress, GetOptions(config)), config) { - _commandExecutor.AddCommandGroup(new AppiumIOSPointerActions(this)); + _commandExecutor.AddCommandGroup(new AppiumIOSMouseActions(this)); + _commandExecutor.AddCommandGroup(new AppiumIOSTouchActions(this)); _commandExecutor.AddCommandGroup(new AppiumIOSVirtualKeyboardActions(this)); } From 44b31361972f022a2c4f36695ae61fabd54a750e Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Wed, 8 May 2024 17:35:24 -0300 Subject: [PATCH 3/3] update HelperExtensions to match .NET MUI --- .../HelperExtensions.cs | 168 +++++++++++++++++- 1 file changed, 162 insertions(+), 6 deletions(-) diff --git a/src/Plugin.Maui.UITestHelpers.Appium/HelperExtensions.cs b/src/Plugin.Maui.UITestHelpers.Appium/HelperExtensions.cs index 4a47dd7d..b8e041cb 100644 --- a/src/Plugin.Maui.UITestHelpers.Appium/HelperExtensions.cs +++ b/src/Plugin.Maui.UITestHelpers.Appium/HelperExtensions.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Drawing; +using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Interfaces; using Plugin.Maui.UITestHelpers.Core; @@ -17,7 +18,17 @@ public static class HelperExtensions /// https://github.com/dotnet/maui/issues/19754 /// /// Represents the main gateway to interact with an app. - /// Target Element + /// Target Element. + public static void Tap(this IApp app, string element) + { + app.FindElement(element).Click(); + } + + /// + /// Performs a mouse click on the matched element. + /// + /// Represents the main gateway to interact with an app. + /// Target Element. public static void Click(this IApp app, string element) { app.FindElement(element).Click(); @@ -57,6 +68,12 @@ public static Rectangle GetRect(this IUIElement element) throw new InvalidOperationException($"Could not get Rect of element"); } + /// + /// Enters text into the currently focused element. + /// + /// Represents the main gateway to interact with an app. + /// Target Element. + /// The text to enter. public static void EnterText(this IApp app, string element, string text) { var appElement = app.FindElement(element); @@ -64,11 +81,20 @@ public static void EnterText(this IApp app, string element, string text) app.DismissKeyboard(); } + /// + /// Hides soft keyboard if present. + /// + /// Represents the main gateway to interact with an app. public static void DismissKeyboard(this IApp app) { app.CommandExecutor.Execute("dismissKeyboard", ImmutableDictionary.Empty); } + /// + /// Whether or not the soft keyboard is shown. + /// + /// Represents the main gateway to interact with an app. + /// true if the soft keyboard is shown; otherwise, false. public static bool IsKeyboardShown(this IApp app) { var response = app.CommandExecutor.Execute("isKeyboardShown", ImmutableDictionary.Empty); @@ -99,21 +125,38 @@ public static void SendKeys(this IApp app, int keyCode, int metastate = 0) throw new InvalidOperationException($"SendKeys is not supported on {aaa.Driver}"); } + /// + /// Clears text from the currently focused element. + /// + /// Represents the main gateway to interact with an app. + /// Target Element. public static void ClearText(this IApp app, string element) { app.FindElement(element).Clear(); } + /// + /// Performs a mouse click on the matched element. + /// + /// Target Element. + public static void Click(this IUIElement element) + { + element.Command.Execute("click", new Dictionary() + { + { "element", element } + }); + } + /// /// For desktop, this will perform a mouse click on the target element. /// For mobile, this will tap the element. /// This API works for all platforms whereas TapCoordinates currently doesn't work on Catalyst /// https://github.com/dotnet/maui/issues/19754 /// - /// Target Element - public static void Click(this IUIElement element) + /// Target Element. + public static void Tap(this IUIElement element) { - element.Command.Execute("click", new Dictionary() + element.Command.Execute("tap", new Dictionary() { { "element", element } }); @@ -136,15 +179,98 @@ public static void Clear(this IUIElement element) }); } + /// + /// Performs a mouse double click on the matched element. + /// + /// Represents the main gateway to interact with an app. + /// Target Element. public static void DoubleClick(this IApp app, string element) { - var elementToClick = app.FindElement(element); + var elementToDoubleClick = app.FindElement(element); app.CommandExecutor.Execute("doubleClick", new Dictionary { - { "element", elementToClick }, + { "element", elementToDoubleClick }, + }); + } + + /// + /// Performs a mouse double click on the given coordinates. + /// + /// Represents the main gateway to interact with an app. + /// The x coordinate to double click. + /// The y coordinate to double click. + public static void DoubleClickCoordinates(this IApp app, float x, float y) + { + app.CommandExecutor.Execute("doubleClickCoordinates", new Dictionary + { + { "x", x }, + { "y", y } }); } + /// + /// Performs two quick tap / touch gestures on the matched element. + /// + /// Represents the main gateway to interact with an app. + /// Target Element. + public static void DoubleTap(this IApp app, string element) + { + var elementToDoubleTap = app.FindElement(element); + app.CommandExecutor.Execute("doubleTap", new Dictionary + { + { "element", elementToDoubleTap }, + }); + } + + /// + /// Performs two quick tap / touch gestures on the given coordinates. + /// + /// Represents the main gateway to interact with an app. + /// The x coordinate to double tap. + /// The y coordinate to double tap. + public static void DoubleTapCoordinates(this IApp app, float x, float y) + { + app.CommandExecutor.Execute("doubleTapCoordinates", new Dictionary + { + { "x", x }, + { "y", y } + }); + } + + /// + /// Performs a long mouse click on the matched element. + /// + /// Represents the main gateway to interact with an app. + /// Target Element. + public static void LongPress(this IApp app, string element) + { + var elementToLongPress = app.FindElement(element); + app.CommandExecutor.Execute("longPress", new Dictionary + { + { "element", elementToLongPress }, + }); + } + + /// + /// Performs a continuous touch gesture on the matched element. + /// + /// Represents the main gateway to interact with an app. + /// Target Element. + public static void TouchAndHold(this IApp app, string element) + { + var elementToTouchAndHold = app.FindElement(element); + app.CommandExecutor.Execute("touchAndHold", new Dictionary + { + { "element", elementToTouchAndHold }, + }); + } + + /// + /// Performs a long touch on an item, followed by dragging the item to a second item and dropping it. + /// + /// Represents the main gateway to interact with an app. + /// Element to be dragged. + /// Element to be dropped. public static void DragAndDrop(this IApp app, string dragSource, string dragTarget) { var dragSourceElement = app.FindElement(dragSource); @@ -157,6 +283,12 @@ public static void DragAndDrop(this IApp app, string dragSource, string dragTarg }); } + /// + /// Scroll until an element that matches the toElementId is shown on the screen. + /// + /// Represents the main gateway to interact with an app. + /// Specify what element to scroll within. + /// Whether scrolls should be down or up. public static void ScrollTo(this IApp app, string toElementId, bool down = true) { app.CommandExecutor.Execute("scrollTo", new Dictionary @@ -414,6 +546,21 @@ public static void SetOrientationPortrait(this IApp app) app.CommandExecutor.Execute("setOrientationPortrait", ImmutableDictionary.Empty); } + /// + /// Performs a mouse click on the given coordinates. + /// + /// Represents the main gateway to interact with an app. + /// The x coordinate to click. + /// The y coordinate to click. + public static void ClickCoordinates(this IApp app, float x, float y) + { + app.CommandExecutor.Execute("clickCoordinates", new Dictionary + { + { "x", x }, + { "y", y } + }); + } + /// /// Performs a tap / touch gesture on the given coordinates. /// This API currently doesn't work on Catalyst https://github.com/dotnet/maui/issues/19754 @@ -603,6 +750,15 @@ public static bool IsFocused(this IApp app, string id) var activeElement = aaa.Driver.SwitchTo().ActiveElement(); var element = (AppiumDriverElement)app.WaitForElement(id); + if (app.GetTestDevice() == TestDevice.Mac && activeElement is AppiumElement activeAppiumElement) + { + // For some reason on catalyst the ActiveElement returns an AppiumElement with a different id + // The TagName (AutomationId) and the location all match, so, other than the Id it walks and talks + // like the same element + return element.AppiumElement.TagName.Equals(activeAppiumElement.TagName, StringComparison.OrdinalIgnoreCase) && + element.AppiumElement.Location.Equals(activeAppiumElement.Location); + } + return element.AppiumElement.Equals(activeElement); }