From 5c5a6cd036c81695bacddb9ef9e7fab5bfdbd67a Mon Sep 17 00:00:00 2001 From: Anandhan Rajagopal <97146406+anandhan-rajagopal@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:37:15 +0530 Subject: [PATCH 1/4] Enabled Shell Flyout Behavior UITests from XamarinUITest to Appium (#21) * Migrate issue FlyoutBehaviourShell * Updated migrated test cases * Updated the comments --------- Co-authored-by: NafeelaNazhir --- .../Issues/XFIssue/FlyoutBehaviorShell.cs | 129 ++++----- .../src/UITest.Appium/HelperExtensions.cs | 245 ++++++++++++++++-- 2 files changed, 297 insertions(+), 77 deletions(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/XFIssue/FlyoutBehaviorShell.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/XFIssue/FlyoutBehaviorShell.cs index cb22ef04cf87..1bcfd33c7d38 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/XFIssue/FlyoutBehaviorShell.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/XFIssue/FlyoutBehaviorShell.cs @@ -5,77 +5,90 @@ namespace Microsoft.Maui.TestCases.Tests.Issues; public class FlyoutBehaviorShell : _IssuesUITest { + const string title = "Basic Test"; + const string FlyoutItem = "Flyout Item"; + const string EnableFlyoutBehavior = "EnableFlyoutBehavior"; + const string DisableFlyoutBehavior = "DisableFlyoutBehavior"; + const string LockFlyoutBehavior = "LockFlyoutBehavior"; + const string OpenFlyout = "OpenFlyout"; + const string EnableBackButtonBehavior = "EnableBackButtonBehavior"; + const string DisableBackButtonBehavior = "DisableBackButtonBehavior"; public FlyoutBehaviorShell(TestDevice testDevice) : base(testDevice) { } public override string Issue => "Shell Flyout Behavior"; - // [FailsOnAndroidWhenRunningOnXamarinUITest] - // [Test] - // [Category(UITestCategories.Shell)] - // public void FlyoutTests() - // { - // // Flyout is visible - // App.WaitForElement(EnableFlyoutBehavior); - // // Starting shell out as disabled correctly disables flyout - // App.WaitForNoElement(FlyoutIconAutomationId, "Flyout Icon Visible on Startup"); - // ShowFlyout(usingSwipe: true, testForFlyoutIcon: false); - // App.WaitForNoElement(FlyoutItem, "Flyout Visible on Startup"); - - // // Enable Flyout Test - // App.Tap(EnableFlyoutBehavior); - // ShowFlyout(usingSwipe: true); - // App.WaitForElement(FlyoutItem, "Flyout Not Visible after Enabled"); - // App.Tap(FlyoutItem); + [Test] + [Category(UITestCategories.Shell)] + public void FlyoutTests() + { + // Flyout is visible + App.WaitForElement(EnableFlyoutBehavior); +#if !MACCATALYST && !WINDOWS //Swipe Options for Shell are not applicable for Desktop Platforms. + // Starting shell out as disabled correctly disables flyout + App.WaitForNoElement(FlyoutIconAutomationId, "Flyout Icon Visible on Startup"); + App.ShowFlyout(usingSwipe: true, waitForFlyoutIcon: false); + App.WaitForNoElement(FlyoutItem, "Flyout Visible on Startup"); - // // Flyout Icon is not visible but you can still swipe open - // App.Tap(DisableFlyoutBehavior); - // App.WaitForNoElement(FlyoutIconAutomationId, "Flyout Icon Visible after being Disabled"); - // ShowFlyout(usingSwipe: true, testForFlyoutIcon: false); - // App.WaitForNoElement(FlyoutItem, "Flyout Visible after being Disabled"); + // Enable Flyout Test + App.Tap(EnableFlyoutBehavior); + App.ShowFlyout(usingSwipe: true); + App.WaitForElement(FlyoutItem, "Flyout Not Visible after Enabled"); + App.Tap(FlyoutItem); + // Flyout Icon is not visible but you can still swipe open + App.Tap(DisableFlyoutBehavior); + App.WaitForNoElement(FlyoutIconAutomationId, "Flyout Icon Visible after being Disabled"); + App.ShowFlyout(usingSwipe: true, waitForFlyoutIcon: false); + App.WaitForNoElement(FlyoutItem, "Flyout Visible after being Disabled"); - // // enable flyout and make sure disabling back button behavior doesn't hide icon - // App.Tap(EnableFlyoutBehavior); - // App.WaitForElement(FlyoutIconAutomationId); - // App.Tap(DisableBackButtonBehavior); - // ShowFlyout(usingSwipe: true); - // App.WaitForElement(FlyoutItem, "Flyout swipe not working after Disabling Back Button Behavior"); - // App.Tap(FlyoutItem); - // // make sure you can still open flyout via code - // App.Tap(EnableFlyoutBehavior); - // App.Tap(EnableBackButtonBehavior); - // App.Tap(OpenFlyout); - // App.WaitForElement(FlyoutItem, "Flyout not opening via code"); - // App.Tap(FlyoutItem); + // enable flyout and make sure disabling back button behavior doesn't hide icon + App.Tap(EnableFlyoutBehavior); + App.WaitForFlyoutIcon(); + App.Tap(DisableBackButtonBehavior); + App.ShowFlyout(usingSwipe: true); + App.WaitForElement(FlyoutItem, "Flyout swipe not working after Disabling Back Button Behavior"); + App.Tap(FlyoutItem); +#endif + // // make sure you can still open flyout via code + App.Tap(EnableFlyoutBehavior); + App.Tap(EnableBackButtonBehavior); + App.Tap(OpenFlyout); + App.WaitForElement(FlyoutItem, "Flyout not opening via code"); + App.Tap(FlyoutItem); - // // make sure you can still open flyout via code if flyout behavior is disabled - // App.Tap(DisableFlyoutBehavior); - // App.Tap(EnableBackButtonBehavior); - // App.Tap(OpenFlyout); - // App.WaitForElement(FlyoutItem, "Flyout not opening via code when flyout behavior disabled"); - // App.Tap(FlyoutItem); +#if !IOS && !MACCATALYST // When DisableFlyoutBehavior is set, flyout items become inaccessible via inspection tools, leading to timeout exceptions on both iOS and Catalyst. + + // make sure you can still open flyout via code if flyout behavior is disabled + App.Tap(DisableFlyoutBehavior); + App.Tap(EnableBackButtonBehavior); + App.Tap(OpenFlyout); + App.WaitForElement(FlyoutItem, "Flyout not opening via code when flyout behavior disabled"); + App.Tap(FlyoutItem); +#endif - // // make sure you can still open flyout via code if back button behavior is disabled - // App.Tap(EnableFlyoutBehavior); - // App.Tap(DisableBackButtonBehavior); - // App.Tap(OpenFlyout); - // App.WaitForElement(FlyoutItem, "Flyout not opening via code when back button behavior is disabled"); - // App.Tap(FlyoutItem); + // make sure you can still open flyout via code if back button behavior is disabled + App.Tap(EnableFlyoutBehavior); + App.Tap(DisableBackButtonBehavior); + App.Tap(OpenFlyout); + App.WaitForElement(FlyoutItem, "Flyout not opening via code when back button behavior is disabled"); + App.Tap(FlyoutItem); - // } + } - // [Test] - // public void WhenFlyoutIsLockedButtonsAreStillVisible() - // { - // // FlyoutLocked ensure that the flyout and buttons are still visible - // App.Tap(EnableBackButtonBehavior); - // App.Tap(LockFlyoutBehavior); - // App.WaitForElement(title, "Flyout Locked hiding content"); - // App.Tap(EnableFlyoutBehavior); - // App.WaitForNoElement(FlyoutItem); - // } + [Test] + [Category(UITestCategories.Shell)] + public void WhenFlyoutIsLockedButtonsAreStillVisible() + { + // FlyoutLocked ensure that the flyout and buttons are still visible + App.WaitForElement(EnableBackButtonBehavior); + App.Tap(EnableBackButtonBehavior); + App.Tap(LockFlyoutBehavior); + App.WaitForElement(title,"Flyout Locked hiding content"); + App.Tap(EnableFlyoutBehavior); + App.WaitForNoElement(FlyoutItem); + } } diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs index 610a53fd254d..342587ebb272 100644 --- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs +++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs @@ -1779,40 +1779,247 @@ public static IDictionary GetSystemBars(this IApp app) } /// - /// Navigates back in the application by simulating a tap on the platform-specific back navigation button. + /// Navigates back in the application by simulating a tap on the platform-specific back navigation button or using a custom identifier. /// /// The IApp instance representing the main gateway to interact with the application. - /// Optional. The custom identifier for the back button. If not provided, default platform-specific identifiers will be used. + /// Optional custom identifier string for the back button. If not provided, the default back arrow query will be used. public static void TapBackArrow(this IApp app, string customBackButtonIdentifier = "") { - switch (app) + var query = string.IsNullOrEmpty(customBackButtonIdentifier) + ? GetDefaultBackArrowQuery(app) + : GetCustomBackArrowQuery(app, customBackButtonIdentifier); + + TapBackArrow(app, query); + } + + /// + /// Navigates back in the application using a custom IQuery. + /// + /// The IApp instance representing the main gateway to interact with the application. + /// The custom IQuery for the back button. + public static void TapBackArrow(this IApp app, IQuery query) + { + app.Tap(query); + } + + /// + /// Gets the default query for the back arrow button based on the app type. + /// + /// The IApp instance representing the application. + /// An IQuery for the default back arrow button. + /// Thrown when an unsupported app type is provided. + static IQuery GetDefaultBackArrowQuery(IApp app) + { + return app switch + { + AppiumAndroidApp _ => AppiumQuery.ByXPath("//android.widget.ImageButton[@content-desc='Navigate up']"), + AppiumIOSApp _ => AppiumQuery.ByAccessibilityId("Back"), + AppiumCatalystApp _ => AppiumQuery.ByAccessibilityId("Back"), + AppiumWindowsApp _ => AppiumQuery.ByAccessibilityId("NavigationViewBackButton"), + _ => throw new ArgumentException("Unsupported app type", nameof(app)) + }; + } + + /// + /// Gets a custom query for the back arrow button based on the app type and a custom identifier. + /// Note that for Windows apps, the back button is not customizable, so the default identifier is used. + /// + /// The IApp instance representing the application. + /// The custom identifier for the back button. + /// An IQuery for the custom back arrow button. + /// Thrown when an unsupported app type is provided. + static IQuery GetCustomBackArrowQuery(IApp app, string customBackButtonIdentifier) + { + return app switch { - case AppiumAndroidApp _: - app.Tap(AppiumQuery.ByXPath(string.IsNullOrEmpty(customBackButtonIdentifier) - ? "//android.widget.ImageButton[@content-desc='Navigate up']" - : $"//android.widget.ImageButton[@content-desc='{customBackButtonIdentifier}']")); - break; + AppiumAndroidApp _ => AppiumQuery.ByXPath($"//android.widget.ImageButton[@content-desc='{customBackButtonIdentifier}']"), + AppiumIOSApp _ => AppiumQuery.ByXPath($"//XCUIElementTypeButton[@name='{customBackButtonIdentifier}']"), + AppiumCatalystApp _ => AppiumQuery.ByName(customBackButtonIdentifier), + AppiumWindowsApp _ => AppiumQuery.ByAccessibilityId("NavigationViewBackButton"), + _ => throw new ArgumentException("Unsupported app type", nameof(app)) + }; + } - case AppiumIOSApp _: - case AppiumCatalystApp _: - if (string.IsNullOrEmpty(customBackButtonIdentifier)) + /// + /// Waits for an element to be ready until page navigation has settled, with additional waiting for MacCatalyst. + /// This method helps prevent null reference exceptions during page transitions, especially in MacCatalyst. + /// + /// The IApp instance. + /// The id of the element to wait for. + /// Optional timeout for the wait operation. Default is null, which uses the default timeout. + public static void WaitForElementTillPageNavigationSettled(this IApp app, string elementId, TimeSpan? timeout = null) + { + if(app is AppiumCatalystApp) + app.WaitForElement(AppiumQuery.ById(elementId), timeout: timeout); + + app.WaitForElement(elementId, timeout: timeout); + } + + /// + /// Waits for an element to be ready until page navigation has settled, with additional waiting for MacCatalyst. + /// This method helps prevent null reference exceptions during page transitions, especially in MacCatalyst. + /// + /// The IApp instance. + /// The query to use for finding the element. + /// Optional timeout for the wait operation. Default is null, which uses the default timeout. + public static void WaitForElementTillPageNavigationSettled(this IApp app, IQuery query, TimeSpan? timeout = null) + { + if(app is AppiumCatalystApp) + app.WaitForElement(query, timeout: timeout); + + app.WaitForElement(query, timeout: timeout); + } + /// + /// Waits for the flyout icon to appear in the app. + /// + /// The IApp instance representing the application. + /// The automation ID of the flyout icon (default is an empty string). + /// Indicates whether the app is using Shell navigation (default is true). + public static void WaitForFlyoutIcon(this IApp app, string automationId = "", bool isShell = true) + { + if(app is AppiumAndroidApp) + { + app.WaitForElement(AppiumQuery.ByXPath("//android.widget.ImageButton[@content-desc=\"Open navigation drawer\"]")); + } + else if (app is AppiumIOSApp || app is AppiumCatalystApp || app is AppiumWindowsApp) + { + if(isShell){ + app.WaitForElement("OK"); + } + if (!isShell){ + if(app is AppiumWindowsApp) { - app.Tap(AppiumQuery.ByAccessibilityId("Back")); + app.WaitForElement(AppiumQuery.ByAccessibilityId("TogglePaneButton")); } else { - app.Tap(app is AppiumIOSApp - ? AppiumQuery.ByXPath($"//XCUIElementTypeButton[@name='{customBackButtonIdentifier}']") - : AppiumQuery.ByName(customBackButtonIdentifier)); + app.WaitForElement(automationId); } - break; + } + } + } - case AppiumWindowsApp _: - app.Tap(AppiumQuery.ByAccessibilityId("NavigationViewBackButton")); - break; + /// + /// Shows the flyout menu in the app. + /// + /// The IApp instance representing the application. + /// The automation ID of the flyout icon (default is an empty string). + /// Indicates whether to use swipe gesture to open the flyout (default is false). + /// Indicates whether to wait for the flyout icon before showing the flyout (default is true). + /// Indicates whether the app is using Shell navigation (default is true). + public static void ShowFlyout(this IApp app, string automationId = "", bool usingSwipe = false, bool waitForFlyoutIcon = true, bool isShell = true) + { + if (waitForFlyoutIcon) + { + app.WaitForFlyoutIcon(automationId, isShell); + } + + if (usingSwipe) + { + app.DragCoordinates(5, 500, 800, 500); + } + else + { + app.TapFlyoutIcon(automationId, isShell, false); } } + /// + /// Taps the Flyout icon for Shell or FlyoutPage. + /// + /// Represents the main gateway to interact with an app. + /// Optional title for FlyoutPage (default is empty string). + /// Indicates whether the Flyout is for Shell (true) or FlyoutPage (false). + private static void TapFlyoutIcon(this IApp app, string title = "", bool isShell = true, bool waitForFlyoutIcon = true) + { + if (waitForFlyoutIcon) + { + app.WaitForFlyoutIcon(title, isShell); + } + if (app is AppiumAndroidApp) + { + app.Tap(AppiumQuery.ByXPath("//android.widget.ImageButton[@content-desc=\"Open navigation drawer\"]")); + } + else if (app is AppiumIOSApp || app is AppiumCatalystApp || app is AppiumWindowsApp) + { + if (isShell) + { + app.Tap(AppiumQuery.ByAccessibilityId("OK")); + } + else + { + if(app is AppiumWindowsApp) + { + app.Tap(AppiumQuery.ByAccessibilityId("TogglePaneButton")); + } + else + { + app.Tap(title); + } + } + } + } + + /// + /// Taps the Flyout icon for Shell pages. + /// + /// Represents the main gateway to interact with an app. + public static void TapShellFlyoutIcon(this IApp app) + { + app.TapFlyoutIcon(); + } + + /// + /// Taps the Flyout icon for FlyoutPage. + /// + /// Represents the main gateway to interact with an app. + /// Optional title for FlyoutPage (default is empty string). + public static void TapFlyoutPageIcon(this IApp app, string title = "") + { + app.TapFlyoutIcon(title, false); + } + + /// + /// Taps an item in the specified flyout menu. + /// + /// The IApp instance representing the application. + /// The text or accessibility identifier of the flyout item to tap. + /// True if it's a Shell flyout, false for FlyoutPage flyout. + private static void TapInFlyout(this IApp app, string flyoutItem, bool isShellFlyout) + { + if (isShellFlyout) + { + app.TapShellFlyoutIcon(); + } + else + { + app.TapFlyoutPageIcon(); + } + + app.WaitForElement(flyoutItem); + app.Tap(flyoutItem); + } + + /// + /// Taps an item in the Shell flyout menu. + /// + /// The IApp instance representing the application. + /// The text or accessibility identifier of the flyout item to tap. + public static void TapInShellFlyout(this IApp app, string flyoutItem) + { + app.TapInFlyout(flyoutItem, true); + } + + /// + /// Taps an item in the FlyoutPage flyout menu. + /// + /// The IApp instance representing the application. + /// The text or accessibility identifier of the flyout item to tap. + public static void TapInFlyoutPageFlyout(this IApp app, string flyoutItem) + { + app.TapInFlyout(flyoutItem, false); + } + static IUIElement Wait(Func query, Func satisfactory, string? timeoutMessage = null, From 106a457164d2451b99b9e745d16bd1922d35ae99 Mon Sep 17 00:00:00 2001 From: Anandhan Rajagopal <97146406+anandhan-rajagopal@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:31:30 +0530 Subject: [PATCH 2/4] Addressed the review changes --- src/TestUtils/src/UITest.Appium/HelperExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs index a7d398461de5..aceff8b977a6 100644 --- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs +++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs @@ -2018,7 +2018,7 @@ public static void TapInShellFlyout(this IApp app, string flyoutItem) public static void TapInFlyoutPageFlyout(this IApp app, string flyoutItem) { app.TapInFlyout(flyoutItem, false); - } + } /// Taps the "More" button in the app, with platform-specific logic for Android and Windows. /// This method does not currently support iOS and macOS platforms, where the "More" button is not shown. From e7687496d518ee99f379befacdc798835f959f1c Mon Sep 17 00:00:00 2001 From: Anandhan Rajagopal <97146406+anandhan-rajagopal@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:15:18 +0530 Subject: [PATCH 3/4] Resolved build error --- .../src/UITest.Appium/HelperExtensions.cs | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs index d1e38a7d284b..7ead6313ecc0 100644 --- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs +++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs @@ -1898,36 +1898,6 @@ public static void WaitForFlyoutIcon(this IApp app, string automationId = "", bo } } } - - /// - /// Waits for an element to be ready until page navigation has settled, with additional waiting for MacCatalyst. - /// This method helps prevent null reference exceptions during page transitions, especially in MacCatalyst. - /// - /// The IApp instance. - /// The id of the element to wait for. - /// Optional timeout for the wait operation. Default is null, which uses the default timeout. - public static void WaitForElementTillPageNavigationSettled(this IApp app, string elementId, TimeSpan? timeout = null) - { - if(app is AppiumCatalystApp) - app.WaitForElement(AppiumQuery.ById(elementId), timeout: timeout); - - app.WaitForElement(elementId, timeout: timeout); - } - - /// - /// Waits for an element to be ready until page navigation has settled, with additional waiting for MacCatalyst. - /// This method helps prevent null reference exceptions during page transitions, especially in MacCatalyst. - /// - /// The IApp instance. - /// The query to use for finding the element. - /// Optional timeout for the wait operation. Default is null, which uses the default timeout. - public static void WaitForElementTillPageNavigationSettled(this IApp app, IQuery query, TimeSpan? timeout = null) - { - if(app is AppiumCatalystApp) - app.WaitForElement(query, timeout: timeout); - - app.WaitForElement(query, timeout: timeout); - } /// /// Shows the flyout menu in the app. From ff54454479a21f270222622eef580098fe4ed5c1 Mon Sep 17 00:00:00 2001 From: Anandhan Rajagopal <97146406+anandhan-rajagopal@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:01:10 +0530 Subject: [PATCH 4/4] Update HelperExtensions.cs --- .../src/UITest.Appium/HelperExtensions.cs | 55 +------------------ 1 file changed, 1 insertion(+), 54 deletions(-) diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs index 698f19161bef..4061345fc02d 100644 --- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs +++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs @@ -1869,60 +1869,6 @@ public static void WaitForElementTillPageNavigationSettled(this IApp app, IQuery app.WaitForElement(query, timeout: timeout); } - /// - /// Waits for the flyout icon to appear in the app. - /// - /// The IApp instance representing the application. - /// The automation ID of the flyout icon (default is an empty string). - /// Indicates whether the app is using Shell navigation (default is true). - public static void WaitForFlyoutIcon(this IApp app, string automationId = "", bool isShell = true) - { - if(app is AppiumAndroidApp) - { - app.WaitForElement(AppiumQuery.ByXPath("//android.widget.ImageButton[@content-desc=\"Open navigation drawer\"]")); - } - else if (app is AppiumIOSApp || app is AppiumCatalystApp || app is AppiumWindowsApp) - { - if(isShell){ - app.WaitForElement("OK"); - } - if (!isShell){ - if(app is AppiumWindowsApp) - { - app.WaitForElement(AppiumQuery.ByAccessibilityId("TogglePaneButton")); - } - else - { - app.WaitForElement(automationId); - } - } - } - } - - /// - /// Shows the flyout menu in the app. - /// - /// The IApp instance representing the application. - /// The automation ID of the flyout icon (default is an empty string). - /// Indicates whether to use swipe gesture to open the flyout (default is false). - /// Indicates whether to wait for the flyout icon before showing the flyout (default is true). - /// Indicates whether the app is using Shell navigation (default is true). - public static void ShowFlyout(this IApp app, string automationId = "", bool usingSwipe = false, bool waitForFlyoutIcon = true, bool isShell = true) - { - if (waitForFlyoutIcon) - { - app.WaitForFlyoutIcon(automationId, isShell); - } - - if (usingSwipe) - { - app.DragCoordinates(5, 500, 800, 500); - } - else - { - app.TapFlyoutIcon(automationId, isShell, false); - } - } /// /// Waits for the flyout icon to appear in the app. @@ -2075,6 +2021,7 @@ public static void TapInFlyoutPageFlyout(this IApp app, string flyoutItem) app.TapInFlyout(flyoutItem, false); } + /// /// Taps the "More" button in the app, with platform-specific logic for Android and Windows. /// This method does not currently support iOS and macOS platforms, where the "More" button is not shown. ///