diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 08371f46f87f..0dab7bf7fd78 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "microsoft.dotnet.xharness.cli": { - "version": "9.0.0-prerelease.24311.2", + "version": "9.0.0-prerelease.24312.3", "commands": [ "xharness" ] diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index ba8454f62a00..da107f057221 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -49,6 +49,7 @@ body: - 9.0.0-preview.3.10457 - 9.0.0-preview.2.10293 - 9.0.0-preview.1.9973 + - 8.0.60 SR6 - 8.0.40 SR5 - 8.0.21 SR4.1 - 8.0.20 SR4 @@ -111,6 +112,7 @@ body: - 8.0.20 SR4 - 8.0.21 SR4.1 - 8.0.40 SR5 + - 8.0.60 SR6 - 9.0.0-preview.1.9973 - 9.0.0-preview.2.10293 - 9.0.0-preview.3.10457 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4930f5b8a0c4..45d0d17ffd51 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,7 @@ { "recommendations": [ - "ms-dotnettools.csharp", - "ms-vscode.mono-debug", - "visualstudioexptteam.vscodeintellicode", + "ms-dotnettools.vscodeintellicode-csharp", + "ms-dotnettools.dotnet-maui", + "github.copilot-chat" ] } \ No newline at end of file diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 95c10af5868c..1534a65f5854 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -135,17 +135,17 @@ https://github.com/dotnet/runtime 4a0a04cf3e8b8c2a3613270df7f838246e4597e8 - + https://github.com/dotnet/xharness - 975b330d51119efc4884f7a323784662cbf74391 + 6ce15319de72ab6d4c3b0f4c40f59300cffc5450 - + https://github.com/dotnet/xharness - 975b330d51119efc4884f7a323784662cbf74391 + 6ce15319de72ab6d4c3b0f4c40f59300cffc5450 - + https://github.com/dotnet/xharness - 975b330d51119efc4884f7a323784662cbf74391 + 6ce15319de72ab6d4c3b0f4c40f59300cffc5450 diff --git a/eng/Versions.props b/eng/Versions.props index beb4d12083dd..78fb859fb8c8 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -62,7 +62,7 @@ 9.0.0-preview.5.24273.1 $(MicrosoftNETWorkloadEmscriptenCurrentManifest90100TransportVersion) - 1.5.240311000 + 1.5.240607001 10.0.22621.756 1.2.0 @@ -108,9 +108,9 @@ <_HarfBuzzSharpVersion>7.3.0.2 <_SkiaSharpNativeAssetsVersion>0.0.0-commit.7af1d0840a381c0ce7ef2877454a88dbb2949686.1086 7.0.114 - 9.0.0-prerelease.24311.2 - 9.0.0-prerelease.24311.2 - 9.0.0-prerelease.24311.2 + 9.0.0-prerelease.24312.3 + 9.0.0-prerelease.24312.3 + 9.0.0-prerelease.24312.3 0.9.2 1.0.0.16 1.3.0 diff --git a/src/Controls/src/Core/Cells/Cell.cs b/src/Controls/src/Core/Cells/Cell.cs index 2da47d8120ec..eafc18ba5658 100644 --- a/src/Controls/src/Core/Cells/Cell.cs +++ b/src/Controls/src/Core/Cells/Cell.cs @@ -273,7 +273,7 @@ async void OnForceUpdateSizeRequested() // don't run more than once per 16 milliseconds await Task.Delay(TimeSpan.FromMilliseconds(16)); ForceUpdateSizeRequested?.Invoke(this, null); - Handler.Invoke("ForceUpdateSizeRequested", null); + Handler?.Invoke("ForceUpdateSizeRequested", null); _nextCallToForceUpdateSizeQueued = false; } @@ -307,8 +307,14 @@ void OnParentPropertyChanging(object sender, PropertyChangingEventArgs e) internal Android.Views.View ConvertView { get; set; } #elif IOS internal UIKit.UITableViewCell ReusableCell { get; set; } - internal UIKit.UITableView TableView { get; set; } + WeakReference _tableView; + + internal UIKit.UITableView TableView + { + get => _tableView?.GetTargetOrDefault(); + set => _tableView = value is null ? null : new(value); + } #endif diff --git a/src/Controls/src/Core/Compatibility/Handlers/Android/FrameRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Android/FrameRenderer.cs index 6cab4f0a2e94..bb64e53baf5b 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Android/FrameRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Android/FrameRenderer.cs @@ -176,7 +176,7 @@ protected override void OnLayout(bool changed, int l, int t, int r, int b) if (Element.Handler is IPlatformViewHandler pvh && - Element is IContentView cv) + Element is ICrossPlatformLayout cv) { pvh.LayoutVirtualView(l, t, r, b, cv.CrossPlatformArrange); } diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/Windows/ListViewRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/Windows/ListViewRenderer.cs index 942adb97ce67..1b5c24b9fd15 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/Windows/ListViewRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/Windows/ListViewRenderer.cs @@ -260,8 +260,6 @@ void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) ClearSizeEstimate(); ReloadData(); } - - Element.Dispatcher.DispatchIfRequired(() => List?.UpdateLayout()); } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs index eab9e8276267..23d034109c1b 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs @@ -18,7 +18,7 @@ public class CellRenderer : ElementHandler, IRegisterable public static CommandMapper CommandMapper = new CommandMapper(ElementHandler.ElementCommandMapper); - UITableView? _tableView; + WeakReference? _tableView; private protected event PropertyChangedEventHandler? CellPropertyChanged; @@ -37,7 +37,7 @@ protected override UITableViewCell CreatePlatformElement() public virtual UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv) { - _tableView = tv; + _tableView = new(tv); Performance.Start(out string reference); var tvc = reusableCell as CellTableViewCell ?? new CellTableViewCell(UITableViewCellStyle.Default, item.GetType().FullName); @@ -173,15 +173,17 @@ public override void Invoke(string command, object? args) if (command == "ForceUpdateSizeRequested" && VirtualView is BindableObject bindableObject && - GetRealCell(bindableObject) is UITableViewCell ctv) + GetRealCell(bindableObject) is UITableViewCell ctv && + _tableView is not null && + _tableView.TryGetTarget(out var tableView)) { - var index = _tableView?.IndexPathForCell(ctv); + var index = tableView.IndexPathForCell(ctv); if (index == null && VirtualView is Cell c) { index = Controls.Compatibility.Platform.iOS.CellExtensions.GetIndexPath(c); } if (index != null) - _tableView?.ReloadRows(new[] { index }, UITableViewRowAnimation.None); + tableView.ReloadRows(new[] { index }, UITableViewRowAnimation.None); } } } diff --git a/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs index 7c6a684c1492..1d69ffefe8d3 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs @@ -48,7 +48,8 @@ public class NavigationRenderer : UINavigationController, INavigationViewHandler public static CommandMapper CommandMapper = new CommandMapper(ViewHandler.ViewCommandMapper); ViewHandlerDelegator _viewHandlerWrapper; bool _navigating = false; - VisualElement _element; + WeakReference _element; + WeakReference _current; bool _uiRequestedPop; // User tapped the back button or swiped to navigate back MauiNavigationDelegate NavigationDelegate => Delegate as MauiNavigationDelegate; @@ -60,14 +61,18 @@ public NavigationRenderer() : base(typeof(MauiControlsNavigationBar), null) Delegate = new MauiNavigationDelegate(this); } - Page Current { get; set; } + Page Current + { + get => _current?.GetTargetOrDefault(); + set => _current = value is null ? null : new(value); + } IPageController PageController => Element as IPageController; NavigationPage NavPage => Element as NavigationPage; INavigationPageController NavPageController => NavPage; - public VisualElement Element { get => _viewHandlerWrapper.Element ?? _element; } + public VisualElement Element { get => _viewHandlerWrapper.Element ?? _element?.GetTargetOrDefault(); } public event EventHandler ElementChanged; @@ -85,7 +90,7 @@ public UIView NativeView public void SetElement(VisualElement element) { (this as IElementHandler).SetVirtualView(element); - _element = element; + _element = element is null ? null : new(element); } public UIViewController ViewController @@ -171,7 +176,8 @@ public override void ViewDidDisappear(bool animated) public override void ViewWillLayoutSubviews() { base.ViewWillLayoutSubviews(); - if (Current == null) + + if (Current is not Page current) return; UpdateToolBarVisible(); @@ -180,7 +186,7 @@ public override void ViewWillLayoutSubviews() var toolbar = _secondaryToolbar; //save the state of the Current page we are calculating, this will fire before Current is updated - _hasNavigationBar = NavigationPage.GetHasNavigationBar(Current); + _hasNavigationBar = NavigationPage.GetHasNavigationBar(current); // Use 0 if the NavBar is hidden or will be hidden var toolbarY = NavigationBarHidden || NavigationBar.Translucent || !_hasNavigationBar ? 0 : navBarFrameBottom; @@ -475,8 +481,8 @@ void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) } else if (e.PropertyName == NavigationPage.CurrentPageProperty.PropertyName) { - Current = NavPage?.CurrentPage; - ValidateNavbarExists(Current); + var current = Current = NavPage?.CurrentPage; + ValidateNavbarExists(current); } else if (e.PropertyName == IsNavigationBarTranslucentProperty.PropertyName) { @@ -806,7 +812,7 @@ void UpdateBarTextColor() } // set Tint color (i. e. Back Button arrow and Text) - var iconColor = Current != null ? NavigationPage.GetIconColor(Current) : null; + var iconColor = Current is Page current ? NavigationPage.GetIconColor(current) : null; if (iconColor == null) iconColor = barTextColor; @@ -860,8 +866,8 @@ void UpdateToolBarVisible() if (currentHidden != _secondaryToolbar.Hidden) { - if (Current?.Handler != null) - Current.ToPlatform().InvalidateMeasure(Current); + if (Current is Page current && current.Handler is not null) + current.ToPlatform().InvalidateMeasure(current); if (VisibleViewController is ParentingViewController pvc) pvc.UpdateFrames(); @@ -1716,7 +1722,7 @@ void IElementHandler.SetMauiContext(IMauiContext mauiContext) void IElementHandler.SetVirtualView(Maui.IElement view) { _viewHandlerWrapper.SetVirtualView(view, ElementChanged, false); - _element = view as VisualElement; + _element = view is VisualElement v ? new(v) : null; void ElementChanged(ElementChangedEventArgs e) { diff --git a/src/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cs index fe066c9e7116..54613aab56b1 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cs @@ -27,7 +27,7 @@ public class TabbedRenderer : UITabBarController, IPlatformViewHandler bool? _defaultBarTranslucent; IMauiContext _mauiContext; UITabBarAppearance _tabBarAppearance; - VisualElement _element; + WeakReference _element; IMauiContext MauiContext => _mauiContext; public static IPropertyMapper Mapper = new PropertyMapper(TabbedViewHandler.ViewMapper); @@ -57,7 +57,7 @@ protected TabbedPage Tabbed get { return (TabbedPage)Element; } } - public VisualElement Element => _viewHandlerWrapper.Element ?? _element; + public VisualElement Element => _viewHandlerWrapper.Element ?? _element?.GetTargetOrDefault(); public event EventHandler ElementChanged; @@ -72,11 +72,14 @@ public UIView NativeView public void SetElement(VisualElement element) { _viewHandlerWrapper.SetVirtualView(element, OnElementChanged, false); - _element = element; + _element = element is null ? null : new(element); FinishedCustomizingViewControllers += HandleFinishedCustomizingViewControllers; - Tabbed.PropertyChanged += OnPropertyChanged; - Tabbed.PagesChanged += OnPagesChanged; + if (element is TabbedPage tabbed) + { + tabbed.PropertyChanged += OnPropertyChanged; + tabbed.PagesChanged += OnPagesChanged; + } OnPagesChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); @@ -134,8 +137,11 @@ protected override void Dispose(bool disposing) Page?.SendDisappearing(); - Tabbed.PropertyChanged -= OnPropertyChanged; - Tabbed.PagesChanged -= OnPagesChanged; + if (Tabbed is TabbedPage tabbed) + { + tabbed.PropertyChanged -= OnPropertyChanged; + tabbed.PagesChanged -= OnPagesChanged; + } FinishedCustomizingViewControllers -= HandleFinishedCustomizingViewControllers; } @@ -188,8 +194,8 @@ void OnPagesChanged(object sender, NotifyCollectionChangedEventArgs e) SetControllers(); UIViewController controller = null; - if (Tabbed.CurrentPage != null) - controller = GetViewController(Tabbed.CurrentPage); + if (Tabbed?.CurrentPage is Page currentPage) + controller = GetViewController(currentPage); if (controller != null && controller != base.SelectedViewController) base.SelectedViewController = controller; @@ -202,7 +208,7 @@ void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(TabbedPage.CurrentPage)) { - var current = Tabbed.CurrentPage; + var current = Tabbed?.CurrentPage; if (current == null) return; @@ -244,15 +250,20 @@ public override UIViewController ChildViewControllerForStatusBarHidden() void UpdateCurrentPagePreferredStatusBarUpdateAnimation() { - PageUIStatusBarAnimation animation = ((Page)Element).OnThisPlatform().PreferredStatusBarUpdateAnimation(); - Tabbed.CurrentPage.OnThisPlatform().SetPreferredStatusBarUpdateAnimation(animation); + if (Page is Page page) + { + PageUIStatusBarAnimation animation = page.OnThisPlatform().PreferredStatusBarUpdateAnimation(); + Tabbed?.CurrentPage?.OnThisPlatform().SetPreferredStatusBarUpdateAnimation(animation); + } } void UpdatePrefersStatusBarHiddenOnPages() { + if (Tabbed is not TabbedPage tabbed) + return; for (var i = 0; i < ViewControllers.Length; i++) { - Tabbed.GetPageByIndex(i).OnThisPlatform().SetPrefersStatusBarHidden(Tabbed.OnThisPlatform().PrefersStatusBarHidden()); + tabbed.GetPageByIndex(i).OnThisPlatform().SetPrefersStatusBarHidden(tabbed.OnThisPlatform().PrefersStatusBarHidden()); } } @@ -260,7 +271,7 @@ public override UIViewController ChildViewControllerForHomeIndicatorAutoHidden { get { - var current = Tabbed.CurrentPage; + var current = Tabbed?.CurrentPage; if (current == null) return null; @@ -276,15 +287,19 @@ void UpdatePageSpecifics() void Reset() { + if (Tabbed is not TabbedPage tabbed) + return; var i = 0; - foreach (var page in Tabbed.Children) + foreach (var page in tabbed.Children) SetupPage(page, i++); } void SetControllers() { + if (Tabbed is not TabbedPage tabbed) + return; var list = new List(); - var pages = Tabbed.InternalChildren; + var pages = tabbed.InternalChildren; for (var i = 0; i < pages.Count; i++) { var child = pages[i]; @@ -315,10 +330,10 @@ void TeardownPage(Page page, int index) void UpdateBarBackgroundColor() { - if (Tabbed == null || TabBar == null) + if (Tabbed is not TabbedPage tabbed || TabBar == null) return; - var barBackgroundColor = Tabbed.BarBackgroundColor; + var barBackgroundColor = tabbed.BarBackgroundColor; var isDefaultColor = barBackgroundColor == null; if (isDefaultColor && !_barBackgroundColorWasSet) @@ -342,20 +357,20 @@ void UpdateBarBackgroundColor() void UpdateBarBackground() { - if (Tabbed == null || TabBar == null) + if (Tabbed is not TabbedPage tabbed || TabBar == null) return; - var barBackground = Tabbed.BarBackground; + var barBackground = tabbed.BarBackground; TabBar.UpdateBackground(barBackground); } void UpdateBarTextColor() { - if (Tabbed == null || TabBar == null || TabBar.Items == null) + if (Tabbed is not TabbedPage tabbed || TabBar == null || TabBar.Items == null) return; - var barTextColor = Tabbed.BarTextColor; + var barTextColor = tabbed.BarTextColor; var isDefaultColor = barTextColor == null; if (isDefaultColor && !_barTextColorWasSet) @@ -395,11 +410,11 @@ void UpdateBarTextColor() void UpdateBarTranslucent() { - if (Tabbed == null || TabBar == null || Element == null) + if (Tabbed == null || TabBar == null || Element is not VisualElement element) return; _defaultBarTranslucent = _defaultBarTranslucent ?? TabBar.Translucent; - switch (TabbedPageConfiguration.GetTranslucencyMode(Element)) + switch (TabbedPageConfiguration.GetTranslucencyMode(element)) { case TranslucencyMode.Translucent: TabBar.Translucent = true; @@ -415,12 +430,14 @@ void UpdateBarTranslucent() void UpdateChildrenOrderIndex(UIViewController[] viewControllers) { + if (Tabbed is not TabbedPage tabbed) + return; for (var i = 0; i < viewControllers.Length; i++) { var originalIndex = -1; if (int.TryParse(viewControllers[i].TabBarItem.Tag.ToString(), out originalIndex)) { - var page = (Page)Tabbed.InternalChildren[originalIndex]; + var page = (Page)tabbed.InternalChildren[originalIndex]; TabbedPage.SetIndex(page, i); } } @@ -428,9 +445,12 @@ void UpdateChildrenOrderIndex(UIViewController[] viewControllers) void UpdateCurrentPage() { - var count = Tabbed.InternalChildren.Count; - var index = (int)SelectedIndex; - ((TabbedPage)Element).CurrentPage = index >= 0 && index < count ? Tabbed.GetPageByIndex(index) : null; + if (Tabbed is TabbedPage tabbed) + { + var count = tabbed.InternalChildren.Count; + var index = (int)SelectedIndex; + tabbed.CurrentPage = index >= 0 && index < count ? tabbed.GetPageByIndex(index) : null; + } } async void SetTabBarItem(IPlatformViewHandler renderer) @@ -442,7 +462,7 @@ async void SetTabBarItem(IPlatformViewHandler renderer) var icons = await GetIcon(page); renderer.ViewController.TabBarItem = new UITabBarItem(page.Title, icons?.Item1, icons?.Item2) { - Tag = Tabbed.Children.IndexOf(page), + Tag = Tabbed?.Children.IndexOf(page) ?? -1, AccessibilityIdentifier = page.AutomationId }; icons?.Item1?.Dispose(); @@ -451,12 +471,12 @@ async void SetTabBarItem(IPlatformViewHandler renderer) void UpdateSelectedTabColors() { - if (Tabbed == null || TabBar == null || TabBar.Items == null) + if (Tabbed is not TabbedPage tabbed || TabBar == null || TabBar.Items == null) return; - if (Tabbed.IsSet(TabbedPage.SelectedTabColorProperty) && Tabbed.SelectedTabColor != null) + if (tabbed.IsSet(TabbedPage.SelectedTabColorProperty) && tabbed.SelectedTabColor != null) { - TabBar.TintColor = Tabbed.SelectedTabColor.ToPlatform(); + TabBar.TintColor = tabbed.SelectedTabColor.ToPlatform(); } else { @@ -467,8 +487,8 @@ void UpdateSelectedTabColors() UpdateiOS15TabBarAppearance(); else { - if (Tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) && Tabbed.UnselectedTabColor != null) - TabBar.UnselectedItemTintColor = Tabbed.UnselectedTabColor.ToPlatform(); + if (tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) && tabbed.UnselectedTabColor != null) + TabBar.UnselectedItemTintColor = tabbed.UnselectedTabColor.ToPlatform(); else TabBar.UnselectedItemTintColor = UITabBar.Appearance.TintColor; } @@ -501,14 +521,16 @@ protected virtual Task> GetIcon(Page page) [System.Runtime.Versioning.SupportedOSPlatform("tvos15.0")] void UpdateiOS15TabBarAppearance() { + if (Tabbed is not TabbedPage tabbed) + return; TabBar.UpdateiOS15TabBarAppearance( ref _tabBarAppearance, _defaultBarColor, _defaultBarTextColor, - Tabbed.IsSet(TabbedPage.SelectedTabColorProperty) ? Tabbed.SelectedTabColor : null, - Tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) ? Tabbed.UnselectedTabColor : null, - Tabbed.IsSet(TabbedPage.BarBackgroundColorProperty) ? Tabbed.BarBackgroundColor : null, - Tabbed.IsSet(TabbedPage.BarTextColorProperty) ? Tabbed.BarTextColor : null, + tabbed.IsSet(TabbedPage.SelectedTabColorProperty) ? tabbed.SelectedTabColor : null, + tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) ? tabbed.UnselectedTabColor : null, + tabbed.IsSet(TabbedPage.BarBackgroundColorProperty) ? tabbed.BarBackgroundColor : null, + tabbed.IsSet(TabbedPage.BarTextColorProperty) ? tabbed.BarTextColor : null, null); } diff --git a/src/Controls/src/Core/Compatibility/Handlers/ViewHandlerDelegator.cs b/src/Controls/src/Core/Compatibility/Handlers/ViewHandlerDelegator.cs index 7ed4921d5a51..ca93a919967a 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ViewHandlerDelegator.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ViewHandlerDelegator.cs @@ -20,8 +20,14 @@ class ViewHandlerDelegator internal IPropertyMapper _mapper; internal readonly CommandMapper _commandMapper; IPlatformViewHandler _viewHandler; +#if IOS || MACCATALYST + TElement? _tempElement; + WeakReference? _element; + public TElement? Element => _tempElement ?? _element?.GetTargetOrDefault(); +#else TElement? _element; public TElement? Element => _element; +#endif bool _disposed; public ViewHandlerDelegator( @@ -48,11 +54,11 @@ public void Invoke(string command, object? args) public void DisconnectHandler() { - if (_element == null) + if (Element is not TElement element) return; - if (_element.Handler == _viewHandler) - _element.Handler = null; + if (element.Handler == _viewHandler) + element.Handler = null; _element = null; @@ -80,6 +86,12 @@ public void SetVirtualView( { #if WINDOWS VisualElementRenderer.SetVirtualView(view, _viewHandler, onElementChanged, ref _element, ref _mapper, _defaultMapper, autoPackage); +#elif IOS || MACCATALYST + // _tempElement is used here, because the Element property is accessed before SetVirtualView() returns + VisualElementRenderer.SetVirtualView(view, _viewHandler, onElementChanged, ref _tempElement, ref _mapper, _defaultMapper, autoPackage); + // We use _element as a WeakReference, and clear _tempElement + _element = _tempElement is null ? null : new(_tempElement); + _tempElement = null; #else VisualElementRenderer.SetVirtualView(view, _viewHandler, onElementChanged, ref _element, ref _mapper, _defaultMapper, autoPackage); #endif 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/src/Core/Platform/GestureManager/GesturePlatformManager.Windows.cs b/src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.Windows.cs index 75a511c14e25..ba6d8a98f69c 100644 --- a/src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.Windows.cs +++ b/src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.Windows.cs @@ -608,6 +608,11 @@ void OnTap(object sender, RoutedEventArgs e) if (view == null) return; + if (!view.IsEnabled) + { + return; + } + var tapPosition = e.GetPositionRelativeToPlatformElement(Control); if (tapPosition == null) diff --git a/src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.iOS.cs b/src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.iOS.cs index 758342768947..128878f5e566 100644 --- a/src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.iOS.cs +++ b/src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.iOS.cs @@ -757,6 +757,11 @@ public bool ShouldReceiveTouch(UIGestureRecognizer recognizer, UITouch touch) return false; } + if (!virtualView.IsEnabled) + { + return false; + } + if (touch.View == platformView) { return true; diff --git a/src/Controls/src/Core/SwipeView/SwipeView.Mapper.cs b/src/Controls/src/Core/SwipeView/SwipeView.Mapper.cs new file mode 100644 index 000000000000..1b1f918451d6 --- /dev/null +++ b/src/Controls/src/Core/SwipeView/SwipeView.Mapper.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.Maui.Controls.Compatibility; + +namespace Microsoft.Maui.Controls +{ + public partial class SwipeView + { + [Obsolete("Use SwipeViewHandler.Mapper instead.")] + internal static IPropertyMapper ControlsSwipeMapper = + new ControlsMapper(SwipeViewHandler.Mapper); + + internal static new void RemapForControls() + { + // Adjusted the mapping to preserve SwipeView.Entry legacy behavior + SwipeViewHandler.Mapper.AppendToMapping(nameof(Background), MapBackground); + } + + static void MapBackground(ISwipeViewHandler handler, SwipeView swipeView) + { + if (swipeView.Content is not null) + { + var contentBackgroundIsNull = Brush.IsNullOrEmpty(swipeView.Content.Background); + var contentBackgroundColorIsNull = swipeView.Content.BackgroundColor == null; + + if (contentBackgroundIsNull && contentBackgroundColorIsNull) + { + if (!Brush.IsNullOrEmpty(swipeView.Background)) + { + swipeView.Content.Background = swipeView.Background; + } + else if (swipeView.BackgroundColor != null) + { + swipeView.Content.BackgroundColor = swipeView.BackgroundColor; + } + } + } + } + } +} diff --git a/src/Controls/src/Core/View/View.cs b/src/Controls/src/Core/View/View.cs index 3d8d52d22de4..48a88634bf16 100644 --- a/src/Controls/src/Core/View/View.cs +++ b/src/Controls/src/Core/View/View.cs @@ -1,10 +1,10 @@ #nullable disable using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; -using Microsoft.Maui; using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Graphics; @@ -96,19 +96,20 @@ protected internal View() _gestureManager = new GestureManager(this); _gestureRecognizers.CollectionChanged += (sender, args) => { - void AddItems(IEnumerable elements) + void AddItems(IList newItems) { - foreach (IElementDefinition item in elements) + foreach (IElementDefinition item in newItems) { - ValidateGesture(item as IGestureRecognizer); + var gestureRecognizer = item as IGestureRecognizer; + ValidateGesture(gestureRecognizer); item.Parent = this; - GestureController.CompositeGestureRecognizers.Add(item as IGestureRecognizer); + GestureController.CompositeGestureRecognizers.Add(gestureRecognizer); } } - void RemoveItems(IEnumerable elements) + void RemoveItems(IList oldItems) { - foreach (IElementDefinition item in elements) + foreach (IElementDefinition item in oldItems) { item.Parent = null; GestureController.CompositeGestureRecognizers.Remove(item as IGestureRecognizer); @@ -118,41 +119,46 @@ void RemoveItems(IEnumerable elements) switch (args.Action) { case NotifyCollectionChangedAction.Add: - AddItems(args.NewItems.OfType()); + AddItems(args.NewItems); break; case NotifyCollectionChangedAction.Remove: - RemoveItems(args.OldItems.OfType()); + RemoveItems(args.OldItems); break; case NotifyCollectionChangedAction.Replace: - AddItems(args.NewItems.OfType()); - RemoveItems(args.OldItems.OfType()); + AddItems(args.NewItems); + RemoveItems(args.OldItems); break; case NotifyCollectionChangedAction.Reset: - List remove = new List(); - List add = new List(); - - foreach (IElementDefinition item in _gestureRecognizers.OfType()) + foreach (IGestureRecognizer gestureRecognizer in _gestureRecognizers) { - if (!_gestureRecognizers.Contains((IGestureRecognizer)item)) - add.Add(item); - item.Parent = this; + if (gestureRecognizer is IElementDefinition item) + { + item.Parent = this; + } } - foreach (IElementDefinition item in GestureController.CompositeGestureRecognizers.OfType()) - { - if (item == _recognizerForPointerOverState) - continue; + HashSet compositeGestureRecognizers = new(GestureController.CompositeGestureRecognizers); - if (_gestureRecognizers.Contains((IGestureRecognizer)item)) - item.Parent = this; - else - remove.Add(item); + foreach (IGestureRecognizer gestureRecognizer in compositeGestureRecognizers) + { + if (gestureRecognizer is IElementDefinition item) + { + if (item == _recognizerForPointerOverState) + continue; + + if (_gestureRecognizers.Contains(gestureRecognizer)) + { + item.Parent = this; + } + else + { + item.Parent = null; + GestureController.CompositeGestureRecognizers.Remove(gestureRecognizer); + } + } } - AddItems(add); - RemoveItems(remove); - break; } }; diff --git a/src/Controls/src/Core/VisualElement/VisualElement.Standard.cs b/src/Controls/src/Core/VisualElement/VisualElement.Standard.cs index 06d69a098931..7cc73f2bcf5a 100644 --- a/src/Controls/src/Core/VisualElement/VisualElement.Standard.cs +++ b/src/Controls/src/Core/VisualElement/VisualElement.Standard.cs @@ -8,13 +8,13 @@ public partial class VisualElement { partial void HandlePlatformUnloadedLoaded() { - if (Window != null) + if (this.IsLoaded) { - SendLoaded(); + SendLoaded(false); } else { - SendUnloaded(); + SendUnloaded(false); } } } diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs index 950db64beaba..38f9eabf7fec 100644 --- a/src/Controls/src/Core/VisualElement/VisualElement.cs +++ b/src/Controls/src/Core/VisualElement/VisualElement.cs @@ -2146,19 +2146,19 @@ public event EventHandler? Loaded { add { - bool watchingLoaded = _watchingPlatformLoaded; + bool loadedAlreadyFired = _isLoadedFired; + _loaded += value; UpdatePlatformUnloadedLoadedWiring(Window); - // The point of this code, is to fire loaded if the element is already loaded. - // - // If this is the first time the user is subscribing to Loaded, - // UpdatePlatformUnloadedLoadedWiring will take care of firing Loaded. - // If we are already wired up to watch loaded, then we'll fire it off if we know this - // view is in a state where it's been determined that it's accurate to fire - // _isLoadedFired. - if (_isLoadedFired && watchingLoaded) + // The first time UpdatePlatformUnloadedLoadedWiring is called it will handle + // invoking _loaded + // This is only to make sure that new subscribers get invoked when the element is already loaded + // and a previous subscriber has already invoked the UpdatePlatformUnloadedLoadedWiring path + if (loadedAlreadyFired && _isLoadedFired) + { value?.Invoke(this, EventArgs.Empty); + } } remove @@ -2212,7 +2212,9 @@ void SendLoaded(bool updateWiring) // unloaded is still correctly being watched for. if (updateWiring) + { UpdatePlatformUnloadedLoadedWiring(Window); + } } void SendUnloaded() => SendUnloaded(true); diff --git a/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs index 5b68f826f76f..d7c8df8dbee6 100644 --- a/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs @@ -241,6 +241,7 @@ internal static MauiAppBuilder RemapForControls(this MauiAppBuilder builder) Window.RemapForControls(); Editor.RemapForControls(); Entry.RemapForControls(); + SwipeView.RemapForControls(); Picker.RemapForControls(); SearchBar.RemapForControls(); TabbedPage.RemapForControls(); diff --git a/src/Controls/src/Xaml/SimplifyTypeExtensionVisitor.cs b/src/Controls/src/Xaml/SimplifyTypeExtensionVisitor.cs index ba4f4ec2d476..22c1117bd8a9 100644 --- a/src/Controls/src/Xaml/SimplifyTypeExtensionVisitor.cs +++ b/src/Controls/src/Xaml/SimplifyTypeExtensionVisitor.cs @@ -55,16 +55,22 @@ static bool IsTargetTypePropertyOfMauiType(INode parentNode, XmlName propertyNam static bool IsTypeExtension(ElementNode node, out ValueNode typeNameValueNode) { - XmlName typeNameXmlName = new("", "TypeName"); - - if (node.XmlType.Name == nameof(TypeExtension) - && node.XmlType.NamespaceUri == XamlParser.X2009Uri - && node.Properties.ContainsKey(typeNameXmlName) - && node.Properties[typeNameXmlName] is ValueNode valueNode - && valueNode.Value is string) + if (node.XmlType.Name == nameof(TypeExtension) && node.XmlType.NamespaceUri == XamlParser.X2009Uri) { - typeNameValueNode = valueNode; - return true; + XmlName typeNameXmlName = new("", "TypeName"); + if (node.Properties.ContainsKey(typeNameXmlName) + && node.Properties[typeNameXmlName] is ValueNode { Value: string } propertyValueNode) + { + typeNameValueNode = propertyValueNode; + return true; + } + + if (node.CollectionItems.Count == 1 + && node.CollectionItems[0] is ValueNode { Value: string } collectionValueNode) + { + typeNameValueNode = collectionValueNode; + return true; + } } typeNameValueNode = null; diff --git a/src/Controls/tests/Core.UnitTests/ListViewTests.cs b/src/Controls/tests/Core.UnitTests/ListViewTests.cs index e1bd22ce486f..78fe5abcbb10 100644 --- a/src/Controls/tests/Core.UnitTests/ListViewTests.cs +++ b/src/Controls/tests/Core.UnitTests/ListViewTests.cs @@ -1649,5 +1649,25 @@ public void DoesNotRetainInRecycleMode() Assert.False(ReferenceEquals(item1, item2)); } + + [Fact] + public void ForceUpdateSizeCalledOnViewCellDoesntCrash() + { + var list = new ListView(){ + HasUnevenRows = true + }; + + list.ItemTemplate = new DataTemplate(() => + { + return new ViewCell { View = new Label() }; + } + ); + + list.ItemsSource = new[] { "Hi" }; + + var element = (ViewCell)list.TemplatedItems[0]; + + element.ForceUpdateSize(); + } } } diff --git a/src/Controls/tests/Core.UnitTests/NavigationPageLifecycleTests.cs b/src/Controls/tests/Core.UnitTests/NavigationPageLifecycleTests.cs index 299e15b8e9b1..c2065b8aad00 100644 --- a/src/Controls/tests/Core.UnitTests/NavigationPageLifecycleTests.cs +++ b/src/Controls/tests/Core.UnitTests/NavigationPageLifecycleTests.cs @@ -60,6 +60,7 @@ public async Task PushLifeCycle(bool useMaui) [InlineData(true)] public async Task PopLifeCycle(bool useMaui) { + bool appearingShouldFireOnInitialPage = false; ContentPage initialPage = new ContentPage(); ContentPage pushedPage = new ContentPage(); @@ -82,14 +83,18 @@ void OnInitialPageAppearing(object sender, EventArgs e) _ = new TestWindow(nav); await waitForFirstAppearing.Task; - initialPage.Appearing += (sender, _) - => rootPageFiresAppearingAfterPop = (ContentPage)sender; + initialPage.Appearing += (sender, _) => + { + Assert.True(appearingShouldFireOnInitialPage); + rootPageFiresAppearingAfterPop = (ContentPage)sender; + }; pushedPage.Disappearing += (sender, _) => pageDisappeared = (ContentPage)sender; await nav.PushAsync(pushedPage); Assert.Null(rootPageFiresAppearingAfterPop); + appearingShouldFireOnInitialPage = true; Assert.Null(pageDisappeared); await nav.PopAsync(); diff --git a/src/Controls/tests/Core.UnitTests/PageLifeCycleTests.cs b/src/Controls/tests/Core.UnitTests/PageLifeCycleTests.cs index 7211de857b1b..f0d728848605 100644 --- a/src/Controls/tests/Core.UnitTests/PageLifeCycleTests.cs +++ b/src/Controls/tests/Core.UnitTests/PageLifeCycleTests.cs @@ -287,6 +287,31 @@ public async Task LoadedUnLoadedEvents() Assert.Equal(1, unLoadedCnt); } + [Fact] + public async Task LoadedFiresOnSecondSubscription() + { + var previousPage = new LCPage(); + var lcPage = new LCPage(); + var navigationPage = + new TestNavigationPage(true, previousPage) + .AddToTestWindow(); + + await navigationPage.PushAsync(lcPage); + + int loadedCnt = 0; + lcPage.Loaded += OnLoaded; + Assert.Equal(1, loadedCnt); + + lcPage.Loaded -= OnLoaded; + lcPage.Loaded += OnLoaded; + Assert.Equal(2, loadedCnt); + + void OnLoaded(object sender, System.EventArgs e) + { + loadedCnt++; + } + } + [Fact] public async Task LoadedFiresOnInitialSubscription() { @@ -299,6 +324,7 @@ public async Task LoadedFiresOnInitialSubscription() await navigationPage.PushAsync(lcPage); int loadedCnt = 0; + int secondLoadedSubscriberCnt = 0; int unLoadedCnt = 0; Assert.True(lcPage.IsLoaded); @@ -309,21 +335,25 @@ public async Task LoadedFiresOnInitialSubscription() loadedCnt++; }; - // Subscribing to loaded should fire the loaded - // event if the page is already loaded + Assert.Equal(1, loadedCnt); + + // Subscribing to loaded a second time + // Should fire the event on the new subsciber; lcPage.Loaded += (_, _) => { - loadedCnt++; + secondLoadedSubscriberCnt++; }; lcPage.Unloaded += (_, _) => unLoadedCnt++; - Assert.Equal(2, loadedCnt); + Assert.Equal(1, loadedCnt); + Assert.Equal(1, secondLoadedSubscriberCnt); Assert.Equal(0, unLoadedCnt); await navigationPage.PopAsync(); - Assert.Equal(2, loadedCnt); + Assert.Equal(1, loadedCnt); + Assert.Equal(1, secondLoadedSubscriberCnt); Assert.Equal(1, unLoadedCnt); } diff --git a/src/Controls/tests/CustomAttributes/Test.cs b/src/Controls/tests/CustomAttributes/Test.cs index 21bb6c59f57d..1eed63091e90 100644 --- a/src/Controls/tests/CustomAttributes/Test.cs +++ b/src/Controls/tests/CustomAttributes/Test.cs @@ -393,6 +393,7 @@ public enum Image { Source, Source_FontImageSource, + IsAnimationPlaying, Aspect, IsOpaque, IsLoading, @@ -743,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/DeviceTests/Elements/ListView/ListViewTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/ListView/ListViewTests.Windows.cs index 44ad92d6f3e0..79e673dd5da6 100644 --- a/src/Controls/tests/DeviceTests/Elements/ListView/ListViewTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/ListView/ListViewTests.Windows.cs @@ -1,9 +1,16 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Handlers.Compatibility; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; +using Microsoft.UI.Xaml; +using Xunit; namespace Microsoft.Maui.DeviceTests { @@ -14,5 +21,38 @@ void ValidatePlatformCells(ListView listView) { } + + [Fact] + public async Task InsertItemWorks() + { + SetupBuilder(); + + var data = new ObservableCollection(); + var listView = new ListView() + { + ItemsSource = data + }; + + var layout = new VerticalStackLayout() + { + listView + }; + + await CreateHandlerAndAddToWindow(layout, async (handler) => + { + await Task.Delay(100); + + data.Add("Item 1"); + data.Add("Item 3"); + data.Insert(1, "Item 2"); + + await Task.Delay(200); + + var actualListView = listView.ToPlatform() as ListViewRenderer; + var textChildren = actualListView.GetChildren(); + + Assert.Contains(textChildren, (x) => x.Text == "Item 3"); + }); + } } -} \ No newline at end of file +} diff --git a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs index ec1bfac0cd6e..43bccb1d2ad7 100644 --- a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs @@ -311,8 +311,7 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async public async Task DoesNotLeak() { SetupBuilder(); - WeakReference pageReference = null; - WeakReference handlerReference = null; + var references = new List(); { var navPage = new NavigationPage(new ContentPage()); @@ -320,15 +319,17 @@ public async Task DoesNotLeak() await CreateHandlerAndAddToWindow(window, (handler) => { - pageReference = new WeakReference(navPage); - handlerReference = new WeakReference(handler); + references.Add(new(navPage)); + references.Add(new(navPage.Handler)); + references.Add(new(navPage.Handler.PlatformView)); + references.Add(new(handler)); // Just replace the page with a new one window.Page = new ContentPage(); }); } - await AssertionExtensions.WaitForGC(pageReference, handlerReference); + await AssertionExtensions.WaitForGC(references.ToArray()); } [Fact(DisplayName = "Child Pages Do Not Leak")] diff --git a/src/Controls/tests/DeviceTests/Elements/RefreshView/RefreshViewTests.cs b/src/Controls/tests/DeviceTests/Elements/RefreshView/RefreshViewTests.cs index ee9dedbe9541..a5120677c4f3 100644 --- a/src/Controls/tests/DeviceTests/Elements/RefreshView/RefreshViewTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/RefreshView/RefreshViewTests.cs @@ -8,6 +8,7 @@ using Microsoft.Maui.Controls.Handlers.Items; using Microsoft.Maui.Handlers; using Microsoft.Maui.Hosting; +using Microsoft.Maui.Platform; using Xunit; namespace Microsoft.Maui.DeviceTests @@ -22,167 +23,29 @@ void SetupBuilder() builder.ConfigureMauiHandlers(handlers => { handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); }); }); } + [Fact(DisplayName = "Setting the content of RefreshView removes previous platform view from visual tree")] + public async Task ChangingRefreshViewContentRemovesPreviousContentsPlatformViewFromVisualTree() + { + SetupBuilder(); + var refreshView = new RefreshView(); + var initialContent = new Label(); + refreshView.Content = initialContent; - // [Fact(DisplayName = "IsRefreshing binding works")] - // public async Task IsRefreshingBindingWorks() - // { - // SetupBuilder(); - - // var vm = new RefreshPageViewModel(); - // var refreshView = new RefreshView() { BindingContext = vm }; - - // refreshView.SetBinding(RefreshView.IsRefreshingProperty, nameof(vm.IsRefreshing), BindingMode.TwoWay); - - // await CreateHandlerAndAddToWindow(refreshView, async handler => - // { - // var platformControl = handler.PlatformView; - // Assert.NotNull(platformControl); - // bool? platformIsRefreshing = null; - // #if WINDOWS - // Deferral refreshCompletionDeferral = null; - // platformControl.RefreshRequested += (s, e) => - // { - // refreshCompletionDeferral = e.GetDeferral(); - // platformIsRefreshing = true; - // }; - // #endif - // vm.IsRefreshing = true; - // #if ANDROID - // platformIsRefreshing = platformControl.Refreshing; - // #elif IOS - // platformIsRefreshing = platformControl.IsRefreshing; - // #elif WINDOWS - - // #endif - // Assert.NotNull(platformIsRefreshing); - // Assert.Equal(vm.IsRefreshing, platformIsRefreshing); - // await Task.Delay(300); - // vm.IsRefreshing = false; - // #if ANDROID - // platformIsRefreshing = platformControl.Refreshing; - // #elif IOS - // platformIsRefreshing = platformControl.IsRefreshing; - // #elif WINDOWS - // if(refreshCompletionDeferral != null) - // { - // refreshCompletionDeferral.Complete(); - // refreshCompletionDeferral = null; - // platformIsRefreshing = false; - // } - // #endif - // Assert.Equal(vm.IsRefreshing, platformIsRefreshing); - // await Task.Delay(1000); - // }); - // } - - // [Fact(DisplayName = "IsRefreshing binding works when started on platform")] - // public async Task IsRefreshingBindingWorksFromPlatform() - // { - // SetupBuilder(); - - // var vm = new RefreshPageViewModel(); - // var refreshView = new RefreshView() { BindingContext = vm }; - - // refreshView.SetBinding(RefreshView.IsRefreshingProperty, nameof(vm.IsRefreshing), BindingMode.TwoWay); - - // await CreateHandlerAndAddToWindow(refreshView, async handler => - // { - // var platformControl = handler.PlatformView; - // Assert.NotNull(platformControl); - // bool? platformIsRefreshing = null; - // #if WINDOWS - // Deferral refreshCompletionDeferral = null; - // platformControl.RefreshRequested += (s, e) => - // { - // refreshCompletionDeferral = e.GetDeferral(); - // platformIsRefreshing = true; - // }; - // platformControl.RequestRefresh(); - // #endif - - // #if ANDROID - // platformIsRefreshing = platformControl.Refreshing = true; - // #elif IOS - // platformIsRefreshing = platformControl.IsRefreshing = true; - // #elif WINDOWS - - // #endif - // Assert.NotNull(platformIsRefreshing); - // Assert.Equal(platformIsRefreshing, vm.IsRefreshing); - // await Task.Delay(300); - // vm.IsRefreshing = false; - // #if ANDROID - // platformIsRefreshing = platformControl.Refreshing; - // #elif IOS - // platformIsRefreshing = platformControl.IsRefreshing; - // #elif WINDOWS - // if (refreshCompletionDeferral != null) - // { - // refreshCompletionDeferral.Complete(); - // refreshCompletionDeferral = null; - // platformIsRefreshing = false; - // } - // #endif - // Assert.Equal(vm.IsRefreshing, platformIsRefreshing); - // await Task.Delay(1000); - // }); - // } - - // class RefreshPageViewModel : BaseViewModel - // { - // public RefreshPageViewModel() - // { - // Data = new ObservableCollection() - // { - // "Item 1", - // "Item 2", - // "Item 3" - // }; - // } - // bool _isRefreshing; - // ObservableCollection _data; - - // public bool IsRefreshing - // { - // get => _isRefreshing; - // set => SetProperty(ref _isRefreshing, value); - // } - - // public ObservableCollection Data - // { - // get => _data; - // set => SetProperty(ref _data, value); - // } - // } - - // public abstract class BaseViewModel : INotifyPropertyChanged - // { - // protected bool SetProperty(ref T backingStore, T value, - // [CallerMemberName] string propertyName = "", - // Action onChanged = null) - // { - // if (EqualityComparer.Default.Equals(backingStore, value)) - // return false; - - // backingStore = value; - // onChanged?.Invoke(); - // OnPropertyChanged(propertyName); - // return true; - // } - - // #region INotifyPropertyChanged - - // public event PropertyChangedEventHandler PropertyChanged; - // protected void OnPropertyChanged([CallerMemberName] string propertyName = "") - // { - // PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - // } - - // #endregion - // } + await AttachAndRun(refreshView, async (handler) => + { + var newContent = new Entry(); + Assert.NotNull(((initialContent as IView).Handler as IPlatformViewHandler).PlatformView.GetParent()); + refreshView.Content = newContent; + Assert.Null(((initialContent as IView).Handler as IPlatformViewHandler).PlatformView.GetParent()); + await Task.Yield(); + Assert.NotNull(((newContent as IView).Handler as IPlatformViewHandler).PlatformView.GetParent()); + }); + } } } diff --git a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs index ae8a6211b8fc..87b47ce0f66a 100644 --- a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading.Tasks; using Microsoft.Maui.Controls; @@ -60,16 +61,27 @@ void SetupBuilder() handlers.AddHandler(); #if IOS || MACCATALYST handlers.AddHandler(); + handlers.AddHandler(); #else handlers.AddHandler(); + handlers.AddHandler(); #endif }); }); } - [Fact("Page Does Not Leak")] - public async Task PageDoesNotLeak() + [Theory("Pages Do Not Leak")] + [InlineData(typeof(ContentPage))] + [InlineData(typeof(NavigationPage))] + [InlineData(typeof(TabbedPage))] + public async Task PagesDoNotLeak(Type type) { +#if WINDOWS + // FIXME: there is still an issue with TabbedPage on Windows + if (type == typeof(TabbedPage)) + return; +#endif + SetupBuilder(); WeakReference viewReference = null; @@ -80,7 +92,22 @@ public async Task PageDoesNotLeak() await CreateHandlerAndAddToWindow(new Window(navPage), async () => { - var page = new ContentPage { Content = new Label() }; + var page = (Page)Activator.CreateInstance(type); + var pageToWaitFor = page; + if (page is ContentPage contentPage) + { + contentPage.Content = new Label(); + } + else if (page is NavigationPage navigationPage) + { + pageToWaitFor = new ContentPage { Content = new Label() }; + await navigationPage.PushAsync(pageToWaitFor); + } + else if (page is TabbedPage tabbedPage) + { + pageToWaitFor = new ContentPage { Content = new Label() }; + tabbedPage.Children.Add(pageToWaitFor); + } await navPage.Navigation.PushModalAsync(page); @@ -89,7 +116,7 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async () => platformViewReference = new WeakReference(page.Handler.PlatformView); // Windows requires Loaded event to fire before unloading - await Task.Delay(500); + await OnLoadedAsync(pageToWaitFor); await navPage.Navigation.PopModalAsync(); }); @@ -256,9 +283,7 @@ public async Task CellsDoNotLeak(Type type) { SetupBuilder(); - WeakReference viewReference = null; - WeakReference handlerReference = null; - + var references = new List(); var observable = new ObservableCollection { 1 }; var navPage = new NavigationPage(new ContentPage { Title = "Page 1" }); @@ -275,18 +300,29 @@ await navPage.Navigation.PushAsync(new ContentPage { viewCell.View = new Label(); } - viewReference = new WeakReference(cell); - handlerReference = new WeakReference(cell.Handler); + references.Add(new(cell)); return cell; }), ItemsSource = observable } }); + Assert.NotEmpty(references); + foreach (var reference in references.ToArray()) + { + if (reference.Target is Cell cell) + { + Assert.NotNull(cell.Handler); + references.Add(new(cell.Handler)); + Assert.NotNull(cell.Handler.PlatformView); + references.Add(new(cell.Handler.PlatformView)); + } + } + await navPage.Navigation.PopAsync(); }); - await AssertionExtensions.WaitForGC(viewReference, handlerReference); + await AssertionExtensions.WaitForGC(references.ToArray()); } #if IOS diff --git a/src/Controls/tests/TestCases.Android.Tests/Controls.TestCases.Android.Tests.csproj b/src/Controls/tests/TestCases.Android.Tests/Controls.TestCases.Android.Tests.csproj index 4327a3a3cb23..050f80101590 100644 --- a/src/Controls/tests/TestCases.Android.Tests/Controls.TestCases.Android.Tests.csproj +++ b/src/Controls/tests/TestCases.Android.Tests/Controls.TestCases.Android.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageUITests_IsAnimationPlaying_No.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageUITests_IsAnimationPlaying_No.png new file mode 100644 index 000000000000..230d0d9cdeef Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageUITests_IsAnimationPlaying_No.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageUITests_IsAnimationPlaying_Yes.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageUITests_IsAnimationPlaying_Yes.png new file mode 100644 index 000000000000..3b9d7495e91b Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageUITests_IsAnimationPlaying_Yes.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/NoScrollbarsTest.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/NoScrollbarsTest.png new file mode 100644 index 000000000000..c93d6a47c329 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/NoScrollbarsTest.png differ diff --git a/src/Controls/tests/TestCases.Mac.Tests/Controls.TestCases.Mac.Tests.csproj b/src/Controls/tests/TestCases.Mac.Tests/Controls.TestCases.Mac.Tests.csproj index 19739d6177d6..408168b2f848 100644 --- a/src/Controls/tests/TestCases.Mac.Tests/Controls.TestCases.Mac.Tests.csproj +++ b/src/Controls/tests/TestCases.Mac.Tests/Controls.TestCases.Mac.Tests.csproj @@ -10,7 +10,7 @@ - + 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.Shared.Tests/Tests/GestureRecognizerUITests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/GestureRecognizerUITests.cs index 5fd085c967df..d74956537c09 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/GestureRecognizerUITests.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/GestureRecognizerUITests.cs @@ -52,5 +52,35 @@ public void DoubleTap() var result = App.FindElement("DoubleTapResults").GetText(); ClassicAssert.AreEqual("Success", result); } + + [Test] + [Category(UITestCategories.Gestures)] + public void SingleTap() + { + App.WaitForElement("TargetView"); + App.EnterText("TargetView", "SingleTapGallery"); + App.Tap("GoButton"); + + App.WaitForElement("SingleTapSurface"); + App.Tap("SingleTapSurface"); + + var result = App.FindElement("SingleTapGestureResults").GetText(); + ClassicAssert.AreEqual("Success", result); + } + + [Test] + [Category(UITestCategories.Gestures)] + public void DisabledSingleTap() + { + App.WaitForElement("TargetView"); + App.EnterText("TargetView", "SingleTapGallery"); + App.Tap("GoButton"); + + App.WaitForElement("DisabledTapSurface"); + App.Tap("DisabledTapSurface"); + + var result = App.FindElement("DisabledTapGestureResults").GetText(); + ClassicAssert.AreNotEqual("Failed", result); + } } } \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/ImageUITests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/ImageUITests.cs index 940c821c5b9d..5a5844353d69 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/ImageUITests.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/ImageUITests.cs @@ -36,4 +36,16 @@ public void Source_FontImageSource() remote.TapStateButton(); VerifyScreenshot("ImageUITests_Source_FontImageSource_FontAwesome"); } + + [Test] + public async Task IsAnimationPlaying() + { + var remote = GoToStateRemote(); + await Task.Delay(500); // make sure the gif is NOT playing + VerifyScreenshot("ImageUITests_IsAnimationPlaying_No"); + + remote.TapStateButton(); + await Task.Delay(500); // make sure the gif IS playing + VerifyScreenshot("ImageUITests_IsAnimationPlaying_Yes"); + } } diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue18526.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue18526.cs new file mode 100644 index 000000000000..f05e62185871 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue18526.cs @@ -0,0 +1,26 @@ +using NUnit.Framework; +using NUnit.Framework.Legacy; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue18526 : _IssuesUITest +{ + public override string Issue => "Border not rendering inside a frame"; + + public Issue18526(TestDevice device) + : base(device) + { } + + [Test] + [Category(UITestCategories.Frame)] + public void BorderShouldRender() + { + var label = App.WaitForElement("label"); + var size = label.GetRect(); + Assert.That(label.GetText(), Is.EqualTo(".NET MAUI")); + Assert.That(size.Width, Is.GreaterThan(0)); + Assert.That(size.Height, Is.GreaterThan(0)); + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19509.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19509.cs index 86404e9db3a1..8eb344c5bf1a 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19509.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19509.cs @@ -14,14 +14,12 @@ public Issue19509(TestDevice device) : base(device) [Test] [FailsOnMac("VerifyScreenshot method not implemented on macOS")] - public async Task EntryTextColorStopsWorkingAfterPropertyIsUpdatedFromBinding() + public void EntryTextColorStopsWorkingAfterPropertyIsUpdatedFromBinding() { App.WaitForElement("WaitForStubControl"); // 1. Click a button to update the text App.Tap("button"); - - await Task.Yield(); // 2. Verify that the Entry TextColor is correct (Green). VerifyScreenshot(); diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue23029.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue23029.cs new file mode 100644 index 000000000000..d6ce1df4bd0f --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue23029.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues +{ + public class Issue23029 : _IssuesUITest + { + public override string Issue => "RefreshView doesn't use correct layout mechanisms"; + + public Issue23029(TestDevice device) : base(device) + { + } + + [Test] + [Category(UITestCategories.RefreshView)] + public void ValidateRefreshViewContentGetsFrameSet() + { + App.WaitForElement("SizeChangedLabel"); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.WinUI.Tests/Controls.TestCases.WinUI.Tests.csproj b/src/Controls/tests/TestCases.WinUI.Tests/Controls.TestCases.WinUI.Tests.csproj index 935c059d6d57..3279ab0fd0a9 100644 --- a/src/Controls/tests/TestCases.WinUI.Tests/Controls.TestCases.WinUI.Tests.csproj +++ b/src/Controls/tests/TestCases.WinUI.Tests/Controls.TestCases.WinUI.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageUITests_IsAnimationPlaying_No.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageUITests_IsAnimationPlaying_No.png new file mode 100644 index 000000000000..02ea4b0fec3e Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageUITests_IsAnimationPlaying_No.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageUITests_IsAnimationPlaying_Yes.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageUITests_IsAnimationPlaying_Yes.png new file mode 100644 index 000000000000..511fab7ea991 Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageUITests_IsAnimationPlaying_Yes.png differ diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/NoScrollbarsTest.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/NoScrollbarsTest.png new file mode 100644 index 000000000000..7587ee3fbff0 Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/NoScrollbarsTest.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/Controls.TestCases.iOS.Tests.csproj b/src/Controls/tests/TestCases.iOS.Tests/Controls.TestCases.iOS.Tests.csproj index 34cd9064784b..bb0f6473a95b 100644 --- a/src/Controls/tests/TestCases.iOS.Tests/Controls.TestCases.iOS.Tests.csproj +++ b/src/Controls/tests/TestCases.iOS.Tests/Controls.TestCases.iOS.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageUITests_IsAnimationPlaying_No.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageUITests_IsAnimationPlaying_No.png new file mode 100644 index 000000000000..a406b28f23b3 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageUITests_IsAnimationPlaying_No.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageUITests_IsAnimationPlaying_Yes.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageUITests_IsAnimationPlaying_Yes.png new file mode 100644 index 000000000000..1e2b649ae33b Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageUITests_IsAnimationPlaying_Yes.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/NoScrollbarsTest.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/NoScrollbarsTest.png new file mode 100644 index 000000000000..8f51ae0ea5e2 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/NoScrollbarsTest.png differ 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