From d91ba8e19ecae1d03f5a84a454c113582bf2c749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Su=C3=A1rez?= Date: Thu, 31 Oct 2024 15:29:54 +0200 Subject: [PATCH] [Testing] Implement Pinch gestures in UITest extensions methods (#25316) * Implement Pinch gestures in UITest extensions methods * Updated test * Updated test * More changes * Apply suggestions from code review --------- Co-authored-by: Gerald Versluis --- .../Tests/Issues/Bugzilla/Bugzilla57515.cs | 36 ++- .../Actions/AppiumPinchToZoomActions.cs | 261 ++++++++++++++++++ src/TestUtils/src/UITest.Appium/AppiumApp.cs | 1 + .../src/UITest.Appium/HelperExtensions.cs | 70 +++++ 4 files changed, 356 insertions(+), 12 deletions(-) create mode 100644 src/TestUtils/src/UITest.Appium/Actions/AppiumPinchToZoomActions.cs diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Bugzilla/Bugzilla57515.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Bugzilla/Bugzilla57515.cs index 890f2bc97f19..5932c4c03e82 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Bugzilla/Bugzilla57515.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Bugzilla/Bugzilla57515.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +#if ANDROID || IOS // TODO: Cannot find the element ZoomContainer on Desktop. Investigate the reason. +using NUnit.Framework; +using NUnit.Framework.Legacy; using UITest.Appium; using UITest.Core; @@ -6,20 +8,30 @@ namespace Microsoft.Maui.TestCases.Tests.Issues; public class Bugzilla57515 : _IssuesUITest { + const string ZoomContainer = "zoomContainer"; + public Bugzilla57515(TestDevice testDevice) : base(testDevice) { } public override string Issue => "PinchGestureRecognizer not getting called on Android "; - // [Test] - // [Category(UITestCategories.Gestures)] - // [FailsOnIOSWhenRunningOnXamarinUITest] - // public void Bugzilla57515Test() - // { - // App.WaitForElement(ZoomContainer); - // App.WaitForElement("1"); - // App.PinchToZoomIn(ZoomContainer); - // App.WaitForNoElement("1"); // The scale should have changed during the zoom - // } -} \ No newline at end of file + [Test] + [Category(UITestCategories.Gestures)] + [FailsOnIOSWhenRunningOnXamarinUITest] + public void Bugzilla57515Test() + { + if (App is not AppiumApp app2 || app2 is null || app2.Driver is null) + { + throw new InvalidOperationException("Cannot run test. Missing driver to run quick tap actions."); + } + + App.WaitForElement(ZoomContainer); + var zoomScale1 = app2.Driver.FindElement(OpenQA.Selenium.By.XPath("//*[@text='" + "1" + "']")); + ClassicAssert.AreEqual("1", zoomScale1.Text); + App.PinchToZoomIn(ZoomContainer); + var elements = app2.Driver.FindElements(OpenQA.Selenium.By.XPath("//*[@text='" + "1" + "']")); + ClassicAssert.AreEqual(0, elements.Count); // The scale should have changed during the zoom + } +} +#endif diff --git a/src/TestUtils/src/UITest.Appium/Actions/AppiumPinchToZoomActions.cs b/src/TestUtils/src/UITest.Appium/Actions/AppiumPinchToZoomActions.cs new file mode 100644 index 000000000000..e8785714c17b --- /dev/null +++ b/src/TestUtils/src/UITest.Appium/Actions/AppiumPinchToZoomActions.cs @@ -0,0 +1,261 @@ +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Interactions; +using OpenQA.Selenium.Interactions; +using UITest.Core; + +namespace UITest.Appium +{ + class AppiumPinchToZoomActions : ICommandExecutionGroup + { + const string PinchToZoomInCommand = "pinchToZoomIn"; + const string PinchToZoomInCoordinatesCommand = "pinchToZoomInCoordinates"; + const string PinchToZoomOutCommand = "pinchToZoomOut"; + const string PinchToZoomOutCoordinatesCommand = "pinchToZoomOutCoordinates"; + + protected readonly AppiumApp _app; + + readonly List _commands = new() + { + PinchToZoomInCommand, + PinchToZoomInCoordinatesCommand, + PinchToZoomOutCommand, + PinchToZoomOutCoordinatesCommand, + }; + + public AppiumPinchToZoomActions(AppiumApp app) + { + _app = app; + } + + public bool IsCommandSupported(string commandName) + { + return _commands.Contains(commandName, StringComparer.OrdinalIgnoreCase); + } + + public CommandResponse Execute(string commandName, IDictionary parameters) + { + return commandName switch + { + PinchToZoomInCommand => PinchToZoomIn(parameters), + PinchToZoomInCoordinatesCommand => PinchToZoomInCoordinates(parameters), + PinchToZoomOutCommand => PinchToZoomOut(parameters), + PinchToZoomOutCoordinatesCommand => PinchToZoomOutCoordinates(parameters), + _ => CommandResponse.FailedEmptyResponse, + }; + } + + CommandResponse PinchToZoomIn(IDictionary parameters) + { + try + { + parameters.TryGetValue("element", out var value); + var element = GetAppiumElement(value); + + TimeSpan duration = (TimeSpan)parameters["duration"]; + + PinchToZoomIn(_app.Driver, element, duration); + + return CommandResponse.SuccessEmptyResponse; + } + catch + { + return CommandResponse.FailedEmptyResponse; + } + } + + CommandResponse PinchToZoomInCoordinates(IDictionary parameters) + { + try + { + int x = (int)parameters["x"]; + int y = (int)parameters["y"]; + TimeSpan duration = (TimeSpan)parameters["duration"]; + + PinchToZoomInCoordinates(_app.Driver, x, y, duration); + + return CommandResponse.SuccessEmptyResponse; + } + catch + { + return CommandResponse.FailedEmptyResponse; + } + } + + CommandResponse PinchToZoomOut(IDictionary parameters) + { + try + { + parameters.TryGetValue("element", out var value); + var element = GetAppiumElement(value); + + TimeSpan duration = (TimeSpan)parameters["duration"]; + + PinchToZoomOut(_app.Driver, element, duration); + + return CommandResponse.SuccessEmptyResponse; + } + catch + { + return CommandResponse.FailedEmptyResponse; + } + } + + CommandResponse PinchToZoomOutCoordinates(IDictionary parameters) + { + try + { + int x = (int)parameters["x"]; + int y = (int)parameters["y"]; + TimeSpan duration = (TimeSpan)parameters["duration"]; + + PinchToZoomOutCoordinates(_app.Driver, x, y, duration); + + return CommandResponse.SuccessEmptyResponse; + } + catch + { + return CommandResponse.FailedEmptyResponse; + } + } + + static void PinchToZoomIn(AppiumDriver driver, AppiumElement? element, TimeSpan duration) + { + var position = element is not null ? element.Location : System.Drawing.Point.Empty; + var size = element is not null ? element.Size : driver.Manage().Window.Size; + + int touch1StartX = position.X + size.Width / 2; + int touch1StartY = position.Y + size.Height / 2; + int touch1EndX = position.X; + int touch1EndY = position.Y; + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touch1 = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + ActionSequence touch1Sequence = new ActionSequence(touch1, 0); + touch1Sequence.AddAction(touch1.CreatePointerMove(CoordinateOrigin.Viewport, touch1StartX, touch1StartY, TimeSpan.Zero)); + touch1Sequence.AddAction(touch1.CreatePointerDown(PointerButton.TouchContact)); + touch1Sequence.AddAction(touch1.CreatePointerMove(CoordinateOrigin.Viewport, touch1EndX, touch1EndY, duration)); + touch1Sequence.AddAction(touch1.CreatePointerUp(PointerButton.TouchContact)); + + int touch2StartX = position.X + size.Width / 2; + int touch2StartY = position.Y + size.Height / 2; + int touch2EndX = position.X + size.Width; + int touch2EndY = position.Y + size.Height; + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touch2 = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + ActionSequence touch2Sequence = new ActionSequence(touch2, 0); + touch2Sequence.AddAction(touch2.CreatePointerMove(CoordinateOrigin.Viewport, touch2StartX, touch2StartY, TimeSpan.Zero)); + touch2Sequence.AddAction(touch2.CreatePointerDown(PointerButton.TouchContact)); + touch2Sequence.AddAction(touch2.CreatePointerMove(CoordinateOrigin.Viewport, touch2EndX, touch2EndY, duration)); + touch2Sequence.AddAction(touch2.CreatePointerUp(PointerButton.TouchContact)); + + driver.PerformActions([touch1Sequence, touch2Sequence]); + } + + static void PinchToZoomInCoordinates(AppiumDriver driver, int x, int y, TimeSpan duration) + { + const int distance = 50; + + int touch1StartX = x; + int touch1StartY = y; + int touch1EndX = x - distance; + int touch1EndY = y - distance; + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touch1 = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + ActionSequence touch1Sequence = new ActionSequence(touch1, 0); + touch1Sequence.AddAction(touch1.CreatePointerMove(CoordinateOrigin.Viewport, touch1StartX, touch1StartY, TimeSpan.Zero)); + touch1Sequence.AddAction(touch1.CreatePointerDown(PointerButton.TouchContact)); + touch1Sequence.AddAction(touch1.CreatePointerMove(CoordinateOrigin.Viewport, touch1EndX, touch1EndY, duration)); + touch1Sequence.AddAction(touch1.CreatePointerUp(PointerButton.TouchContact)); + + int touch2StartX = x; + int touch2StartY = y; + int touch2EndX = x + distance; + int touch2EndY = y + distance; + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touch2 = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + ActionSequence touch2Sequence = new ActionSequence(touch2, 0); + touch2Sequence.AddAction(touch2.CreatePointerMove(CoordinateOrigin.Viewport, touch2StartX, touch2StartY, TimeSpan.Zero)); + touch2Sequence.AddAction(touch2.CreatePointerDown(PointerButton.TouchContact)); + touch2Sequence.AddAction(touch2.CreatePointerMove(CoordinateOrigin.Viewport, touch2EndX, touch2EndY, duration)); + touch2Sequence.AddAction(touch2.CreatePointerUp(PointerButton.TouchContact)); + + driver.PerformActions([touch1Sequence, touch2Sequence]); + } + + static void PinchToZoomOut(AppiumDriver driver, AppiumElement? element, TimeSpan duration) + { + var position = element is not null ? element.Location : System.Drawing.Point.Empty; + var size = element is not null ? element.Size : driver.Manage().Window.Size; + + int touch1StartX = position.X; + int touch1StartY = position.Y; + int touch1EndX = position.X + size.Width / 2; + int touch1EndY = position.Y + size.Height / 2; + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touch1 = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + ActionSequence touch1Sequence = new ActionSequence(touch1, 0); + touch1Sequence.AddAction(touch1.CreatePointerMove(CoordinateOrigin.Viewport, touch1StartX, touch1StartY, TimeSpan.Zero)); + touch1Sequence.AddAction(touch1.CreatePointerDown(PointerButton.TouchContact)); + touch1Sequence.AddAction(touch1.CreatePointerMove(CoordinateOrigin.Viewport, touch1EndX, touch1EndY, duration)); + touch1Sequence.AddAction(touch1.CreatePointerUp(PointerButton.TouchContact)); + + int touch2StartX = position.X + size.Width; + int touch2StartY = position.Y + size.Height; + int touch2EndX = position.X + size.Width / 2; + int touch2EndY = position.Y + size.Height / 2; + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touch2 = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + ActionSequence touch2Sequence = new ActionSequence(touch2, 0); + touch2Sequence.AddAction(touch2.CreatePointerMove(CoordinateOrigin.Viewport, touch2StartX, touch2StartY, TimeSpan.Zero)); + touch2Sequence.AddAction(touch2.CreatePointerDown(PointerButton.TouchContact)); + touch2Sequence.AddAction(touch2.CreatePointerMove(CoordinateOrigin.Viewport, touch2EndX, touch2EndY, duration)); + touch2Sequence.AddAction(touch2.CreatePointerUp(PointerButton.TouchContact)); + + driver.PerformActions([touch1Sequence, touch2Sequence]); + } + + static void PinchToZoomOutCoordinates(AppiumDriver driver, int x, int y, TimeSpan duration) + { + const int distance = 50; + + int touch1StartX = x - distance; + int touch1StartY = y - distance; + int touch1EndX = x; + int touch1EndY = y; + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touch1 = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + ActionSequence touch1Sequence = new ActionSequence(touch1, 0); + touch1Sequence.AddAction(touch1.CreatePointerMove(CoordinateOrigin.Viewport, touch1StartX, touch1StartY, TimeSpan.Zero)); + touch1Sequence.AddAction(touch1.CreatePointerDown(PointerButton.TouchContact)); + touch1Sequence.AddAction(touch1.CreatePointerMove(CoordinateOrigin.Viewport, touch1EndX, touch1EndY, duration)); + touch1Sequence.AddAction(touch1.CreatePointerUp(PointerButton.TouchContact)); + + int touch2StartX = x + distance; + int touch2StartY = y + distance; + int touch2EndX = x; + int touch2EndY = y; + + OpenQA.Selenium.Appium.Interactions.PointerInputDevice touch2 = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch); + ActionSequence touch2Sequence = new ActionSequence(touch2, 0); + touch2Sequence.AddAction(touch2.CreatePointerMove(CoordinateOrigin.Viewport, touch2StartX, touch2StartY, TimeSpan.Zero)); + touch2Sequence.AddAction(touch2.CreatePointerDown(PointerButton.TouchContact)); + touch2Sequence.AddAction(touch2.CreatePointerMove(CoordinateOrigin.Viewport, touch2EndX, touch2EndY, duration)); + touch2Sequence.AddAction(touch2.CreatePointerUp(PointerButton.TouchContact)); + + driver.PerformActions([touch1Sequence, touch2Sequence]); + } + + static AppiumElement? GetAppiumElement(object? element) + { + if (element is AppiumElement appiumElement) + { + return appiumElement; + } + else if (element is AppiumDriverElement driverElement) + { + return driverElement.AppiumElement; + } + + return null; + } + } +} diff --git a/src/TestUtils/src/UITest.Appium/AppiumApp.cs b/src/TestUtils/src/UITest.Appium/AppiumApp.cs index 4da6f1153e4c..99160df665bc 100644 --- a/src/TestUtils/src/UITest.Appium/AppiumApp.cs +++ b/src/TestUtils/src/UITest.Appium/AppiumApp.cs @@ -24,6 +24,7 @@ public AppiumApp(AppiumDriver driver, IConfig config) _commandExecutor.AddCommandGroup(new AppiumGeneralActions()); _commandExecutor.AddCommandGroup(new AppiumDeviceActions(this)); _commandExecutor.AddCommandGroup(new AppiumVirtualKeyboardActions(this)); + _commandExecutor.AddCommandGroup(new AppiumPinchToZoomActions(this)); _commandExecutor.AddCommandGroup(new AppiumSliderActions(this)); _commandExecutor.AddCommandGroup(new AppiumSwipeActions(this)); _commandExecutor.AddCommandGroup(new AppiumScrollActions(this)); diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs index 6c2d3070e470..7790e2134d38 100644 --- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs +++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs @@ -490,6 +490,76 @@ internal static void DragAndDrop(this IApp app, IUIElement? dragSourceElement, I } } + /// + /// Performs a pinch gestures on the matched element to zoom the view in. + /// If multiple elements are matched, the first one will be used. + /// + /// Represents the main gateway to interact with an app. + /// Element to zoom in. + /// The TimeSpan duration of the pinch gesture. + public static void PinchToZoomIn(this IApp app, string element, TimeSpan? duration = null) + { + var elementToPinchToZoomIn = app.FindElement(element); + + app.CommandExecutor.Execute("pinchToZoomIn", new Dictionary + { + { "element", elementToPinchToZoomIn }, + { "duration", duration ?? TimeSpan.FromSeconds(1) } + }); + } + + /// + /// Performs a pinch gestures to zoom the view in on the given coordinates. + /// + /// Represents the main gateway to interact with an app. + /// The x coordinate of the center of the pinch. + /// The y coordinate of the center of the pinch. + /// The TimeSpan duration of the pinch gesture. + public static void PinchToZoomInCoordinates(this IApp app, float x, float y, TimeSpan? duration = null) + { + app.CommandExecutor.Execute("pinchToZoomInCoordinates", new Dictionary + { + { "x", x }, + { "y", y }, + { "duration", duration ?? TimeSpan.FromSeconds(1) } + }); + } + + /// + /// Performs a pinch gestures on the matched element to zoom the view out. + /// If multiple elements are matched, the first one will be used. + /// + /// Represents the main gateway to interact with an app. + /// Element to zoom in. + /// The TimeSpan duration of the pinch gesture. + public static void PinchToZoomOut(this IApp app, string element, TimeSpan? duration = null) + { + var elementToPinchToZoomOut = app.FindElement(element); + + app.CommandExecutor.Execute("pinchToZoomOut", new Dictionary + { + { "element", elementToPinchToZoomOut }, + { "duration", duration ?? TimeSpan.FromSeconds(1) } + }); + } + + /// + /// Performs a pinch gestures to zoom the view out on the given coordinates. + /// + /// Represents the main gateway to interact with an app. + /// The x coordinate of the center of the pinch. + /// The y coordinate of the center of the pinch. + /// The TimeSpan duration of the pinch gesture. + public static void PinchToZoomOutCoordinates(this IApp app, float x, float y, TimeSpan? duration = null) + { + app.CommandExecutor.Execute("pinchToZoomOutCoordinates", new Dictionary + { + { "x", x }, + { "y", y }, + { "duration", duration ?? TimeSpan.FromSeconds(1) } + }); + } + /// /// Scroll until an element that matches the toElementId is shown on the screen. ///