diff --git a/src/Controls/src/Core/Page/Page.cs b/src/Controls/src/Core/Page/Page.cs index bc49235eccba..007b91ce6c84 100644 --- a/src/Controls/src/Core/Page/Page.cs +++ b/src/Controls/src/Core/Page/Page.cs @@ -282,7 +282,7 @@ public Task DisplayActionSheet(string title, string cancel, string destr /// Displays a platform action sheet, allowing the application user to choose from several buttons. /// /// Title of the displayed action sheet. Can be to hide the title. - /// Text to be displayed in the 'Cancel' button. Can be null to hide the action. + /// Text to be displayed in the 'Cancel' button. Can be null to hide the cancel action. /// Text to be displayed in the 'Destruct' button. Can be to hide the destructive option. /// The flow direction to be used by the action sheet. /// Text labels for additional buttons. diff --git a/src/Controls/src/Core/Platform/AlertManager/AlertManager.iOS.cs b/src/Controls/src/Core/Platform/AlertManager/AlertManager.iOS.cs index e4323b096bbf..14a8c19e4988 100644 --- a/src/Controls/src/Core/Platform/AlertManager/AlertManager.iOS.cs +++ b/src/Controls/src/Core/Platform/AlertManager/AlertManager.iOS.cs @@ -187,12 +187,15 @@ static void PresentPopUp(Page sender, Window virtualView, UIWindow platformView, presentingWindow = senderPageWindow; } - if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad && arguments != null) + if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad && + arguments is not null && + alert.PopoverPresentationController is not null && + platformView.RootViewController?.View is not null) { var topViewController = GetTopUIViewController(presentingWindow); UIDevice.CurrentDevice.BeginGeneratingDeviceOrientationNotifications(); var observer = NSNotificationCenter.DefaultCenter.AddObserver(UIDevice.OrientationDidChangeNotification, - n => { alert.PopoverPresentationController.SourceRect = topViewController.View.Bounds; }); + n => alert.PopoverPresentationController.SourceRect = topViewController.View.Bounds); arguments.Result.Task.ContinueWith(t => { @@ -216,7 +219,7 @@ static void PresentPopUp(Page sender, Window virtualView, UIWindow platformView, static UIViewController GetTopUIViewController(UIWindow platformWindow) { var topUIViewController = platformWindow.RootViewController; - while (topUIViewController.PresentedViewController is not null) + while (topUIViewController?.PresentedViewController is not null) { topUIViewController = topUIViewController.PresentedViewController; } diff --git a/src/Controls/tests/CustomAttributes/Test.cs b/src/Controls/tests/CustomAttributes/Test.cs index ae46f9c3a975..1eed63091e90 100644 --- a/src/Controls/tests/CustomAttributes/Test.cs +++ b/src/Controls/tests/CustomAttributes/Test.cs @@ -744,6 +744,16 @@ public enum InputTransparency CascadeTransLayoutOverlayWithButton, } + public enum Alerts + { + AlertCancel, + AlertAcceptCancelClickAccept, + AlertAcceptCancelClickCancel, + ActionSheetClickItem, + ActionSheetClickCancel, + ActionSheetClickDestroy, + } + public static class InputTransparencyMatrix { // this is both for color diff and cols diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Concepts/AlertsGalleryTests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Concepts/AlertsGalleryTests.cs new file mode 100644 index 000000000000..5527b2fa9bb6 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Concepts/AlertsGalleryTests.cs @@ -0,0 +1,149 @@ +using NUnit.Framework; +using NUnit.Framework.Legacy; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests +{ + public class AlertsGalleryTests : CoreGalleryBasePageTest + { + public AlertsGalleryTests(TestDevice device) + : base(device) + { + } + + protected override void NavigateToGallery() + { + App.NavigateToGallery("Alerts Gallery"); + } + +// TODO: UI testing alert code is not yet implemented on Windows. +#if !WINDOWS + [Test] + public void AlertCancel() + { + var test = Test.Alerts.AlertCancel; + + var remote = new EventViewContainerRemote(UITestContext, test); + remote.GoTo(test.ToString()); + + var textBeforeClick = remote.GetEventLabel().GetText(); + ClassicAssert.AreEqual($"Event: {test} (none)", textBeforeClick); + + remote.TapView(); + + var alert = App.WaitForElement(() => App.GetAlert()); + ClassicAssert.NotNull(alert); + + var alertText = alert.GetAlertText(); + CollectionAssert.Contains(alertText, "Alert Title Here"); + CollectionAssert.Contains(alertText, "Alert Message Here"); + + var buttons = alert.GetAlertButtons(); + CollectionAssert.IsNotEmpty(buttons); + ClassicAssert.True(buttons.Count == 1, $"Expected 1 buttonText, found {buttons.Count}."); + + var cancel = buttons.First(); + ClassicAssert.AreEqual("CANCEL", cancel.GetText()); + + cancel.Click(); + + App.WaitForNoElement(() => App.GetAlert()); + + var textAfterClick = remote.GetEventLabel().GetText(); + ClassicAssert.AreEqual($"Event: {test} (SUCCESS 1)", textAfterClick); + } + + [Test] + [TestCase(Test.Alerts.AlertAcceptCancelClickAccept, "ACCEPT")] + [TestCase(Test.Alerts.AlertAcceptCancelClickCancel, "CANCEL")] + public void AlertAcceptCancel(Test.Alerts test, string buttonText) + { + var remote = new EventViewContainerRemote(UITestContext, test); + remote.GoTo(test.ToString()); + + var textBeforeClick = remote.GetEventLabel().GetText(); + ClassicAssert.AreEqual($"Event: {test} (none)", textBeforeClick); + + remote.TapView(); + + var alert = App.WaitForElement(() => App.GetAlert()); + ClassicAssert.NotNull(alert); + + var alertText = alert.GetAlertText(); + CollectionAssert.Contains(alertText, "Alert Title Here"); + CollectionAssert.Contains(alertText, "Alert Message Here"); + + var buttons = alert.GetAlertButtons() + .Select(b => (Element: b, Text: b.GetText())) + .ToList(); + CollectionAssert.IsNotEmpty(buttons); + ClassicAssert.True(buttons.Count == 2, $"Expected 2 buttons, found {buttons.Count}."); + CollectionAssert.Contains(buttons.Select(b => b.Text), "ACCEPT"); + CollectionAssert.Contains(buttons.Select(b => b.Text), "CANCEL"); + + var button = buttons.Single(b => b.Text == buttonText); + button.Element.Click(); + + App.WaitForNoElement(() => App.GetAlert()); + + var textAfterClick = remote.GetEventLabel().GetText(); + ClassicAssert.AreEqual($"Event: {test} (SUCCESS 1)", textAfterClick); + } + + [Test] + [TestCase(Test.Alerts.ActionSheetClickItem, "ITEM 2")] + [TestCase(Test.Alerts.ActionSheetClickCancel, "CANCEL")] + [TestCase(Test.Alerts.ActionSheetClickDestroy, "DESTROY")] + public void ActionSheetClickItem(Test.Alerts test, string itemText) + { + var remote = new EventViewContainerRemote(UITestContext, test); + remote.GoTo(test.ToString()); + + var textBeforeClick = remote.GetEventLabel().GetText(); + ClassicAssert.AreEqual($"Event: {test} (none)", textBeforeClick); + + remote.TapView(); + + var alert = App.WaitForElement(() => App.GetAlert()); + ClassicAssert.NotNull(alert); + + var alertText = alert.GetAlertText(); + CollectionAssert.Contains(alertText, "Action Sheet Title Here"); + + var buttons = alert.GetAlertButtons() + .Select(b => (Element: b, Text: b.GetText())) + .ToList(); + CollectionAssert.IsNotEmpty(buttons); + ClassicAssert.True(buttons.Count >= 4 && buttons.Count <= 5, $"Expected 4 or 5 buttons, found {buttons.Count}."); + CollectionAssert.Contains(buttons.Select(b => b.Text), "DESTROY"); + CollectionAssert.Contains(buttons.Select(b => b.Text), "ITEM 1"); + CollectionAssert.Contains(buttons.Select(b => b.Text), "ITEM 2"); + CollectionAssert.Contains(buttons.Select(b => b.Text), "ITEM 3"); + + // handle the case where the dismiss button is an actual button + if (buttons.Count == 5) + CollectionAssert.Contains(buttons.Select(b => b.Text), "CANCEL"); + + if (buttons.Count == 4 && itemText == "CANCEL") + { + // handle the case where the dismiss button is a "click outside the popup" + + alert.DismissAlert(); + } + else + { + // handle the case where the dismiss button is an actual button + + var button = buttons.Single(b => b.Text == itemText); + button.Element.Click(); + } + + App.WaitForNoElement(() => App.GetAlert()); + + var textAfterClick = remote.GetEventLabel().GetText(); + ClassicAssert.AreEqual($"Event: {test} (SUCCESS 1)", textAfterClick); + } +#endif + } +} diff --git a/src/Controls/tests/TestCases/Concepts/AlertsGalleryPage.cs b/src/Controls/tests/TestCases/Concepts/AlertsGalleryPage.cs new file mode 100644 index 000000000000..d4756fc656b0 --- /dev/null +++ b/src/Controls/tests/TestCases/Concepts/AlertsGalleryPage.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; + +namespace Maui.Controls.Sample +{ + internal class AlertsGalleryPage : CoreGalleryBasePage + { + protected override void Build() + { + // ALERTS + + // Test with a single button alert that can be dismissed by tapping the button + Add(Test.Alerts.AlertCancel, async t => + { + await DisplayAlert( + "Alert Title Here", + "Alert Message Here", + "CANCEL"); + t.ReportSuccessEvent(); + }); + + // Test alert with options to Accept or Cancel, Accept is the correct option + Add(Test.Alerts.AlertAcceptCancelClickAccept, async t => + { + var result = await DisplayAlert( + "Alert Title Here", + "Alert Message Here", + "ACCEPT", "CANCEL"); + if (result) + t.ReportSuccessEvent(); + else + t.ReportFailEvent(); + }); + + // Test alert with options to Accept or Cancel, Cancel is the correct option + Add(Test.Alerts.AlertAcceptCancelClickCancel, async t => + { + var result = await DisplayAlert( + "Alert Title Here", + "Alert Message Here", + "ACCEPT", "CANCEL"); + if (result) + t.ReportFailEvent(); + else + t.ReportSuccessEvent(); + }); + + // ACTION SHEETS + + // Test action sheet with items and Cancel, Item 2 is the correct option + Add(Test.Alerts.ActionSheetClickItem, async t => + { + var result = await DisplayActionSheet( + "Action Sheet Title Here", + "CANCEL", "DESTROY", + "ITEM 1", "ITEM 2", "ITEM 3"); + if (result == "ITEM 2") + t.ReportSuccessEvent(); + else + t.ReportFailEvent(); + }); + + // Test action sheet with items and Cancel, Cancel is the correct option + Add(Test.Alerts.ActionSheetClickCancel, async t => + { + var result = await DisplayActionSheet( + "Action Sheet Title Here", + "CANCEL", "DESTROY", + "ITEM 1", "ITEM 2", "ITEM 3"); + if (result == "CANCEL") + t.ReportSuccessEvent(); + else + t.ReportFailEvent(); + }); + + // Test action sheet with items and Cancel, Destroy is the correct option + Add(Test.Alerts.ActionSheetClickDestroy, async t => + { + var result = await DisplayActionSheet( + "Action Sheet Title Here", + "CANCEL", "DESTROY", + "ITEM 1", "ITEM 2", "ITEM 3"); + if (result == "DESTROY") + t.ReportSuccessEvent(); + else + t.ReportFailEvent(); + }); + } + + ExpectedEventViewContainer