From 97dd7857fd6c686cc831ed9dff26df96ed05d9d0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 26 Aug 2026 22:01:41 +0000 Subject: [PATCH] Implement TopLevel.OpenedPopups (#22074) * Implement TopLevel.OpenedPopups * Delete Headless `GetOpenPopups` * Reorganize popup tests between Headless/Primitives layers Co-Authored-By: Claude Opus 5 * Add Popup.OpenedPopups to keep opened popups in tree structure Co-Authored-By: Claude Opus 5 * Help NUnit suppressing CS8777 * Update suppresions --------- Co-authored-by: Claude Opus 5 --- api/Avalonia.Headless.nupkg.xml | 16 +++ src/Avalonia.Controls/Primitives/Popup.cs | 40 +++++- src/Avalonia.Controls/Primitives/PopupRoot.cs | 3 + src/Avalonia.Controls/TopLevel.cs | 14 +++ .../AvaloniaHeadlessPlatform.cs | 2 - .../HeadlessWindowExtensions.cs | 12 -- .../Avalonia.Headless/HeadlessWindowImpl.cs | 21 ---- .../Avalonia.Headless/IHeadlessWindow.cs | 1 - .../Primitives/PopupTests.cs | 114 ++++++++++++++++- .../AssertHelper.cs | 16 ++- .../MouseDeviceTests.cs | 6 +- .../Avalonia.Headless.UnitTests/PopupTests.cs | 115 +++++++++++------- .../MockWindowingPlatform.cs | 1 + 13 files changed, 276 insertions(+), 85 deletions(-) create mode 100644 api/Avalonia.Headless.nupkg.xml diff --git a/api/Avalonia.Headless.nupkg.xml b/api/Avalonia.Headless.nupkg.xml new file mode 100644 index 0000000000..96c70a7793 --- /dev/null +++ b/api/Avalonia.Headless.nupkg.xml @@ -0,0 +1,16 @@ + + + + + CP0002 + M:Avalonia.Headless.HeadlessWindowExtensions.GetOpenPopups(Avalonia.Controls.TopLevel) + baseline/Avalonia.Headless/lib/net10.0/Avalonia.Headless.dll + current/Avalonia.Headless/lib/net10.0/Avalonia.Headless.dll + + + CP0002 + M:Avalonia.Headless.HeadlessWindowExtensions.GetOpenPopups(Avalonia.Controls.TopLevel) + baseline/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll + current/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll + + diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 75dd723fe3..ba9ca5b572 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using Avalonia.Reactive; @@ -153,6 +154,7 @@ namespace Avalonia.Controls.Primitives private bool _isUsingOverlayLayer; private PopupOpenState? _openState; private Action? _popupHostChangedHandler; + private List? _openedPopups; /// /// Initializes static members of the class. @@ -177,6 +179,11 @@ namespace Avalonia.Controls.Primitives internal IPopupHost? Host => _openState?.PopupHost; + /// + /// Gets the popups that are currently open directly inside this popup, in the order they were opened. + /// + public IReadOnlyList OpenedPopups => _openedPopups ?? (IReadOnlyList)[]; + /// /// Gets or sets a hint to the window manager that a shadow should be added to the popup. /// @@ -575,7 +582,12 @@ namespace Avalonia.Controls.Primitives } } - _openState = new PopupOpenState(placementTarget, topLevel, popupHost, cleanupPopup); + _openState = new PopupOpenState(placementTarget, topLevel, popupHost, cleanupPopup, FindParentPopup(placementTarget)); + + if (_openState.ParentPopup is { } parentPopup) + parentPopup.AddOpenedPopup(this); + else + topLevel.AddOpenedPopup(this); WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint); @@ -844,6 +856,11 @@ namespace Avalonia.Controls.Primitives return; } + if (_openState.ParentPopup is { } parentPopup) + parentPopup.RemoveOpenedPopup(this); + else + _openState.TopLevel.RemoveOpenedPopup(this); + _openState.Dispose(); _openState = null; @@ -1063,22 +1080,41 @@ namespace Avalonia.Controls.Primitives } } + internal void AddOpenedPopup(Popup popup) => (_openedPopups ??= new List(capacity: 2)).Add(popup); + + internal void RemoveOpenedPopup(Popup popup) => _openedPopups?.Remove(popup); + + private static Popup? FindParentPopup(Visual placementTarget) + { + foreach (var visual in placementTarget.GetSelfAndVisualAncestors()) + { + if (visual is IPopupHost) + return (visual as StyledElement)?.Parent as Popup; + } + + return null; + } + private class PopupOpenState : IDisposable { private readonly IDisposable _cleanup; private IDisposable? _presenterCleanup; private Control _placementTarget; - public PopupOpenState(Control placementTarget, TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup) + public PopupOpenState(Control placementTarget, TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup, + Popup? parentPopup) { PlacementTarget = placementTarget; TopLevel = topLevel; + ParentPopup = parentPopup; PopupHost = popupHost; _cleanup = cleanup; } public TopLevel TopLevel { get; } + public Popup? ParentPopup { get; } + public Control PlacementTarget { get => _placementTarget; diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 234f47258d..890bc09cb7 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Diagnostics; @@ -122,6 +123,8 @@ namespace Avalonia.Controls.Primitives public TopLevel ParentTopLevel { get; } + public override IReadOnlyList OpenedPopups => (Parent as Popup)?.OpenedPopups ?? []; + /// public void Dispose() { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 2fd1524cef..721b20ccab 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -132,6 +132,7 @@ namespace Avalonia.Controls private TargetWeakEventSubscriber? _resourcesChangesSubscriber; private IStorageProvider? _storageProvider; private Screens? _screens; + private List? _openedPopups; private readonly PresentationSource _source; private readonly TopLevelHost _topLevelHost; internal TopLevelHost TopLevelHost => _topLevelHost; @@ -559,6 +560,14 @@ namespace Avalonia.Controls // TODO: Un-private private IPlatformSettings? PlatformSettings => AvaloniaLocator.Current.GetService(); + /// + /// Gets the popups that are currently open directly in this top level, in the order they were opened. + /// + /// + /// Use for nested popups. + /// + public virtual IReadOnlyList OpenedPopups => _openedPopups ?? (IReadOnlyList)[]; + /// /// Gets the for which the given is hosted in. /// @@ -708,6 +717,7 @@ namespace Avalonia.Controls LayoutManager.Dispose(); _platformImplBindings.Clear(); + _openedPopups = null; } /// @@ -724,6 +734,10 @@ namespace Avalonia.Controls Renderer.Resized(clientSize); } + internal void AddOpenedPopup(Popup popup) => (_openedPopups ??= new List(capacity: 2)).Add(popup); + + internal void RemoveOpenedPopup(Popup popup) => _openedPopups?.Remove(popup); + /// /// Handles a window scaling change notification from /// . diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 4e14dc0f0b..2d39a9bc6b 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -121,8 +121,6 @@ namespace Avalonia.Headless /// /// Embeds popups to the window when set to true. The default value is true. - /// When disabled, popups are hosted in dedicated headless top-levels that are not part of - /// the parent's visual tree; use to access them. /// // TODO13: Change the default to false to match the other desktop platforms. public bool OverlayPopups { get; set; } = true; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs index 17fe5aa199..7f7232359b 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -115,18 +115,6 @@ public static class HeadlessWindowExtensions DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => RunJobsOnImpl(topLevel, w => w.DragDrop(point, type, data, effects, modifiers)); - /// - /// Returns the popups currently open directly above this toplevel, in z-order (bottom to top). - /// For popups nested in another popup, call this method on that popup's toplevel. - /// - /// - /// Only popups hosted in dedicated headless top-levels are returned, which requires disabling - /// . Popups hosted in the overlay - /// layer are part of the parent's visual tree and are not tracked by the platform. - /// - public static IReadOnlyList GetOpenPopups(this TopLevel topLevel) => - GetImpl(topLevel).GetOpenPopups(); - /// /// Changes the render scaling (DPI) of the headless window/toplevel. /// This simulates a DPI change, triggering scaling changed notifications and a layout pass. diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index 97a24d66b0..1ddaa4165e 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -27,7 +27,6 @@ namespace Avalonia.Headless private readonly AvaloniaHeadlessPlatformOptions _options; private readonly HeadlessWindowImpl? _popupParent; private readonly IPopupPositioner? _popupPositioner; - private readonly List _openPopups = new(); public bool IsPopup { get; } public HeadlessWindowImpl(AvaloniaHeadlessPlatformOptions options) @@ -60,7 +59,6 @@ namespace Avalonia.Headless public void Dispose() { - _popupParent?._openPopups.Remove(this); Closed?.Invoke(); _lastRenderedFrame?.Dispose(); _lastRenderedFrame = null; @@ -101,9 +99,6 @@ namespace Avalonia.Headless public void Show(bool activate, bool isDialog) { - if (_popupParent != null && !_popupParent._openPopups.Contains(this)) - _popupParent._openPopups.Add(this); - if (activate) { ZOrder = _nextGlobalZOrder++; @@ -113,7 +108,6 @@ namespace Avalonia.Headless public void Hide() { - _popupParent?._openPopups.Remove(this); Dispatcher.UIThread.Post(() => Deactivated?.Invoke(), DispatcherPriority.Input); } @@ -403,21 +397,6 @@ namespace Avalonia.Headless public IPopupImpl? CreatePopup() => _options.OverlayPopups ? null : new HeadlessWindowImpl(this); - public IReadOnlyList GetOpenPopups() - { - if (_openPopups.Count == 0) - return Array.Empty(); - - var result = new List(_openPopups.Count); - foreach (var popup in _openPopups) - { - if (popup.InputRoot is PresentationSource { FocusRoot: TopLevel topLevel }) - result.Add(topLevel); - } - - return result; - } - public void SetWindowManagerAddShadowHint(bool enabled) { diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs index 5e2dd6a13d..3fe06d619c 100644 --- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -19,6 +19,5 @@ namespace Avalonia.Headless void MouseWheel(Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None); void DragDrop(Point point, RawDragEventType type, IDataTransfer data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None); void SetRenderScaling(double scaling); - IReadOnlyList GetOpenPopups(); } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 3cca86d3b4..3548cd614f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -1392,6 +1392,109 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Opened_Popup_Should_Be_In_OpenedPopups() + { + using (CreateServices()) + { + var target = new Popup(); + var window = PreparedWindow(target); + + target.Open(); + + Assert.Equal(new[] { target }, window.OpenedPopups); + + target.Close(); + + Assert.Empty(window.OpenedPopups); + } + } + + [Fact] + public void Closing_Popup_With_IsOpen_Should_Remove_It_From_OpenedPopups() + { + using (CreateServices()) + { + var target = new Popup(); + var window = PreparedWindow(target); + + target.IsOpen = true; + + Assert.Equal(new[] { target }, window.OpenedPopups); + + target.IsOpen = false; + + Assert.Empty(window.OpenedPopups); + } + } + + [Fact] + public void Closing_Window_Should_Clear_OpenedPopups() + { + using (CreateServices()) + { + var target = new Popup(); + var window = PreparedWindow(target); + + target.Open(); + window.Close(); + + Assert.Empty(window.OpenedPopups); + } + } + + [Fact] + public void Nested_Popup_Should_Be_In_Parent_Popup_OpenedPopups() + { + using (CreateServices()) + { + var nestedTarget = new Border { Width = 20, Height = 20 }; + var nestedPopup = new Popup + { + PlacementTarget = nestedTarget, + Child = new Border { Width = 10, Height = 10 } + }; + var target = new Border(); + var popup = new Popup + { + PlacementTarget = target, + Child = new Panel { Children = { nestedTarget, nestedPopup } } + }; + var window = PreparedWindow(new Panel { Children = { target, popup } }); + + popup.Open(); + + if (popup.Host is OverlayPopupHost host) + { + //Need to measure/arrange for visual children to show up + //in OverlayPopupHost + host.Measure(Size.Infinity); + host.Arrange(new Rect(host.DesiredSize)); + } + + nestedPopup.Open(); + + Assert.Equal([popup], window.OpenedPopups); + Assert.Equal([nestedPopup], popup.OpenedPopups); + Assert.Empty(nestedPopup.OpenedPopups); + + if (popup.Host is PopupRoot popupRoot) + { + // A popup root exposes the popups opened by its own popup. + Assert.Equal([nestedPopup], popupRoot.OpenedPopups); + } + + nestedPopup.Close(); + + Assert.Equal([popup], window.OpenedPopups); + Assert.Empty(popup.OpenedPopups); + + popup.Close(); + + Assert.Empty(window.OpenedPopups); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With( @@ -1430,13 +1533,22 @@ namespace Avalonia.Controls.UnitTests.Primitives { if (UsePopupHost) return null; - return MockWindowingPlatform.CreatePopupMock(mock.Object).Object; + return CreatePopupMock(mock.Object); }); return mock.Object; }, null); } + private static IPopupImpl CreatePopupMock(IWindowBaseImpl parent) + { + var mock = MockWindowingPlatform.CreatePopupMock(parent); + + mock.Setup(x => x.CreatePopup()).Returns(() => CreatePopupMock(mock.Object)); + + return mock.Object; + } + private static Window PreparedWindow(object? content = null) { var w = new Window { Content = content }; diff --git a/tests/Avalonia.Headless.UnitTests/AssertHelper.cs b/tests/Avalonia.Headless.UnitTests/AssertHelper.cs index 1c98afcb9a..c3ba56d05f 100644 --- a/tests/Avalonia.Headless.UnitTests/AssertHelper.cs +++ b/tests/Avalonia.Headless.UnitTests/AssertHelper.cs @@ -1,5 +1,7 @@ #nullable enable +using System.Diagnostics.CodeAnalysis; + namespace Avalonia.Headless.UnitTests; internal static class AssertHelper @@ -22,14 +24,26 @@ internal static class AssertHelper #endif } - public static void NotNull(object? value) + public static void Null(object? value) + { +#if NUNIT + Assert.That(value, Is.Null); +#elif XUNIT + Assert.Null(value); +#endif + } + + public static void NotNull([NotNull] object? value) { #if NUNIT Assert.That(value, Is.Not.Null); #elif XUNIT Assert.NotNull(value); #endif + // NUnit doesn't suppress CS8777 warning on its own +#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. } +#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. public static void Equal(T expected, T actual) { diff --git a/tests/Avalonia.Headless.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Headless.UnitTests/MouseDeviceTests.cs index d501635fbe..8c2fa92bb7 100644 --- a/tests/Avalonia.Headless.UnitTests/MouseDeviceTests.cs +++ b/tests/Avalonia.Headless.UnitTests/MouseDeviceTests.cs @@ -65,7 +65,11 @@ public class MouseDeviceTests // Pressing captures the pointer implicitly on the window's border. window.MouseDown(new Point(50, 50), MouseButton.Left); - window.GetOpenPopups()[0].MouseMove(new Point(40, 15)); + + var popupRoot = PopupTests.GetPopupTopLevel(popup); + AssertHelper.NotNull(popupRoot); + + popupRoot.MouseMove(new Point(40, 15)); AssertHelper.Same(TestApplication.UsesSharedMouseDevice ? target : popupChild, moveTarget); diff --git a/tests/Avalonia.Headless.UnitTests/PopupTests.cs b/tests/Avalonia.Headless.UnitTests/PopupTests.cs index 2a1536ce50..2eef10a0c4 100644 --- a/tests/Avalonia.Headless.UnitTests/PopupTests.cs +++ b/tests/Avalonia.Headless.UnitTests/PopupTests.cs @@ -32,7 +32,7 @@ public class PopupTests #elif XUNIT [AvaloniaFact] #endif - public void Popup_Uses_Dedicated_TopLevel_And_Is_Discoverable() + public void Popup_Uses_Dedicated_TopLevel() { var target = new Border { Background = Brushes.Red }; var popup = new Popup @@ -49,22 +49,22 @@ public class PopupTests window.Show(); Dispatcher.UIThread.RunJobs(); - AssertHelper.Equal(0, window.GetOpenPopups().Count); + AssertHelper.False(popup.IsOpen); + AssertHelper.False(popup.IsUsingOverlayLayer); + AssertHelper.Null(GetPopupTopLevel(popup)); popup.Open(); Dispatcher.UIThread.RunJobs(); AssertHelper.True(popup.IsOpen); AssertHelper.False(popup.IsUsingOverlayLayer); - AssertHelper.Equal(1, window.GetOpenPopups().Count); - AssertHelper.True(window.GetOpenPopups()[0] is PopupRoot); - - popup.Close(); - Dispatcher.UIThread.RunJobs(); - - AssertHelper.Equal(0, window.GetOpenPopups().Count); + AssertHelper.True(GetPopupTopLevel(popup) is PopupRoot); window.Close(); + + AssertHelper.False(popup.IsOpen); + AssertHelper.False(popup.IsUsingOverlayLayer); + AssertHelper.Null(GetPopupTopLevel(popup)); } #if NUNIT @@ -72,31 +72,40 @@ public class PopupTests #elif XUNIT [AvaloniaFact] #endif - public void Can_Click_Button_Inside_Platform_Popup() + public void Popup_Placement_Respects_Window_Position() { - var clickCount = 0; - var button = new Button { Width = 80, Height = 30 }; - button.Click += (_, _) => clickCount++; - - var target = new Border { Background = Brushes.Red }; - var popup = new Popup { PlacementTarget = target, Child = button }; + var target = new Border + { + Width = 20, + Height = 20, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + Background = Brushes.Red + }; + var popup = new Popup + { + PlacementTarget = target, + Placement = PlacementMode.Bottom, + Child = new Border { Width = 20, Height = 20 } + }; var window = new Window { Width = 100, Height = 100, Content = new Panel { Children = { target, popup } } }; + window.Position = new PixelPoint(100, 200); window.Show(); Dispatcher.UIThread.RunJobs(); popup.Open(); Dispatcher.UIThread.RunJobs(); - var popupRoot = window.GetOpenPopups()[0]; - popupRoot.MouseDown(new Point(40, 15), MouseButton.Left); - popupRoot.MouseUp(new Point(40, 15), MouseButton.Left); + var popupRoot = GetPopupTopLevel(popup); + AssertHelper.NotNull(popupRoot); - AssertHelper.Equal(1, clickCount); + var expected = target.PointToScreen(new Point(0, target.Bounds.Height)); + AssertHelper.Equal(expected, popupRoot.PointToScreen(default)); window.Close(); } @@ -106,38 +115,33 @@ public class PopupTests #elif XUNIT [AvaloniaFact] #endif - public void Popup_Placement_Respects_Window_Position() + public void Can_Click_Button_Inside_Platform_Popup() { - var target = new Border - { - Width = 20, - Height = 20, - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - Background = Brushes.Red - }; - var popup = new Popup - { - PlacementTarget = target, - Placement = PlacementMode.Bottom, - Child = new Border { Width = 20, Height = 20 } - }; + var clickCount = 0; + var button = new Button { Width = 80, Height = 30 }; + button.Click += (_, _) => clickCount++; + + var target = new Border { Background = Brushes.Red }; + var popup = new Popup { PlacementTarget = target, Child = button }; var window = new Window { Width = 100, Height = 100, Content = new Panel { Children = { target, popup } } }; - window.Position = new PixelPoint(100, 200); window.Show(); Dispatcher.UIThread.RunJobs(); popup.Open(); Dispatcher.UIThread.RunJobs(); - var popupRoot = window.GetOpenPopups()[0]; - var expected = target.PointToScreen(new Point(0, target.Bounds.Height)); - AssertHelper.Equal(expected, popupRoot.PointToScreen(default)); + var popupRoot = GetPopupTopLevel(popup); + AssertHelper.NotNull(popupRoot); + + popupRoot.MouseDown(new Point(40, 15), MouseButton.Left); + popupRoot.MouseUp(new Point(40, 15), MouseButton.Left); + + AssertHelper.Equal(1, clickCount); window.Close(); } @@ -147,7 +151,7 @@ public class PopupTests #elif XUNIT [AvaloniaFact] #endif - public void Nested_Popup_Is_Child_Of_Popup_Root() + public void Nested_Popup_Is_Owned_By_Parent_Popup() { var nestedTarget = new Border { Width = 20, Height = 20, Background = Brushes.Green }; var nestedPopup = new Popup @@ -175,11 +179,34 @@ public class PopupTests nestedPopup.Open(); Dispatcher.UIThread.RunJobs(); - var popupRoot = window.GetOpenPopups()[0]; - AssertHelper.Equal(1, window.GetOpenPopups().Count); - AssertHelper.Equal(1, popupRoot.GetOpenPopups().Count); - AssertHelper.True(popupRoot.GetOpenPopups()[0] is PopupRoot); + AssertHelper.Equal(1, window.OpenedPopups.Count); + AssertHelper.Same(popup, window.OpenedPopups[0]); + + AssertHelper.Equal(1, popup.OpenedPopups.Count); + AssertHelper.Same(nestedPopup, popup.OpenedPopups[0]); + AssertHelper.Equal(0, nestedPopup.OpenedPopups.Count); + + // The nested popup is hosted in the parent popup's own top level. + AssertHelper.Same(GetPopupTopLevel(popup), TopLevel.GetTopLevel(nestedTarget)); + + nestedPopup.Close(); + Dispatcher.UIThread.RunJobs(); + + AssertHelper.Equal(0, popup.OpenedPopups.Count); + AssertHelper.Equal(1, window.OpenedPopups.Count); + + popup.Close(); + Dispatcher.UIThread.RunJobs(); + + AssertHelper.Equal(0, window.OpenedPopups.Count); window.Close(); } + + internal static TopLevel GetPopupTopLevel(Popup popup) + { + AssertHelper.NotNull(popup.Child); + var topLevel = TopLevel.GetTopLevel(popup.Child); + return topLevel; + } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 298785df32..eef60186d3 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -101,6 +101,7 @@ namespace Avalonia.UnitTests popupImpl.Setup(x => x.Compositor).Returns(compositor); popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize); popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); + popupImpl.Setup(x => x.DesktopScaling).Returns(1); popupImpl.Setup(x => x.RenderScaling).Returns(1); popupImpl.Setup(x => x.PopupPositioner).Returns(positioner); popupImpl.Setup(x => x.Position).Returns(()=>position);