From ac882ad3a1021179a6280d94e2c2ba69a47e92ea Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 23 Feb 2023 14:08:11 +0100 Subject: [PATCH 1/8] Add failing integration tests for overlay popups. Integration tests for #10420: - Adds an `--overlayPopups` command-line argument to IntegrationTestApp - Renames `TestAppFixture` -> `DefaultAppFixture` - Adds additional `OverlayPopupsAppFixture` - Runs ComboBox and Menu tests in both default and overlay popups mode - VS keeps changing the `.sln` file --- Avalonia.sln | 11 +++--- samples/IntegrationTestApp/MainWindow.axaml | 1 + .../IntegrationTestApp/MainWindow.axaml.cs | 12 +++--- samples/IntegrationTestApp/Program.cs | 22 +++++++++-- .../AutomationTests.cs | 2 +- .../ButtonTests.cs | 2 +- .../CheckBoxTests.cs | 2 +- .../CollectionDefinitions.cs | 14 +++++++ .../ComboBoxTests.cs | 17 +++++++-- ...TestAppFixture.cs => DefaultAppFixture.cs} | 38 +++++++++++-------- .../DefaultCollection.cs | 9 ----- .../GestureTests.cs | 2 +- .../ListBoxTests.cs | 2 +- .../MenuTests.cs | 16 +++++++- .../NativeMenuTests.cs | 2 +- .../OverlayPopupsAppFixture.cs | 19 ++++++++++ .../SliderTests.cs | 2 +- .../WindowTests.cs | 2 +- .../WindowTests_MacOS.cs | 2 +- 19 files changed, 125 insertions(+), 52 deletions(-) create mode 100644 tests/Avalonia.IntegrationTests.Appium/CollectionDefinitions.cs rename tests/Avalonia.IntegrationTests.Appium/{TestAppFixture.cs => DefaultAppFixture.cs} (62%) delete mode 100644 tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs create mode 100644 tests/Avalonia.IntegrationTests.Appium/OverlayPopupsAppFixture.cs diff --git a/Avalonia.sln b/Avalonia.sln index 56847bae31..539c39f63d 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -555,9 +555,14 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -566,10 +571,6 @@ Global {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -635,8 +636,8 @@ Global {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 353e01dca7..f6abf543b9 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -25,6 +25,7 @@ WindowState: + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 087f25666b..19eb1d64b0 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,19 +1,17 @@ -using System; using System.Collections.Generic; using System.Linq; using Avalonia; using Avalonia.Automation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Media; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.VisualTree; using Microsoft.CodeAnalysis; -using Avalonia.Controls.Primitives; -using Avalonia.Threading; -using Avalonia.Controls.Primitives.PopupPositioning; namespace IntegrationTestApp { @@ -25,6 +23,10 @@ namespace IntegrationTestApp InitializeViewMenu(); InitializeGesturesTab(); this.AttachDevTools(); + + var overlayPopups = this.Get("AppOverlayPopups"); + overlayPopups.Text = Program.OverlayPopups ? "Overlay Popups" : "Native Popups"; + AddHandler(Button.ClickEvent, OnButtonClick); ListBoxItems = Enumerable.Range(0, 100).Select(x => "Item " + x).ToList(); DataContext = this; diff --git a/samples/IntegrationTestApp/Program.cs b/samples/IntegrationTestApp/Program.cs index c09b249cfa..6603450b85 100644 --- a/samples/IntegrationTestApp/Program.cs +++ b/samples/IntegrationTestApp/Program.cs @@ -1,17 +1,31 @@ using System; +using System.Linq; using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; namespace IntegrationTestApp { class Program { + public static bool OverlayPopups { get; private set; } + // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + public static void Main(string[] args) + { + OverlayPopups = args.Contains("--overlayPopups"); + + BuildAvaloniaApp() + .With(new Win32PlatformOptions + { + OverlayPopups = OverlayPopups, + }) + .With(new AvaloniaNativePlatformOptions + { + OverlayPopups = OverlayPopups, + }) + .StartWithClassicDesktopLifetime(args); + } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() diff --git a/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs b/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs index bad015506f..4d8760ad61 100644 --- a/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public AutomationTests(TestAppFixture fixture) + public AutomationTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs index 6c630ae782..c0a5414ee3 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs @@ -9,7 +9,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public ButtonTests(TestAppFixture fixture) + public ButtonTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs index 02e7ac60c4..6c154fa268 100644 --- a/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public CheckBoxTests(TestAppFixture fixture) + public CheckBoxTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/CollectionDefinitions.cs b/tests/Avalonia.IntegrationTests.Appium/CollectionDefinitions.cs new file mode 100644 index 0000000000..1e9fa22d9e --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/CollectionDefinitions.cs @@ -0,0 +1,14 @@ +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [CollectionDefinition("Default")] + public class DefaultCollection : ICollectionFixture + { + } + + [CollectionDefinition("OverlayPopups")] + public class OverlayPopupsCollection : ICollectionFixture + { + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index 8df7873582..9e35d366d2 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -4,12 +4,11 @@ using Xunit; namespace Avalonia.IntegrationTests.Appium { - [Collection("Default")] - public class ComboBoxTests + public abstract class ComboBoxTests { private readonly AppiumDriver _session; - public ComboBoxTests(TestAppFixture fixture) + public ComboBoxTests(DefaultAppFixture fixture) { _session = fixture.Session; @@ -153,5 +152,17 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(string.Empty, comboBox.GetComboBoxValue()); } + + [Collection("Default")] + public class Default : ComboBoxTests + { + public Default(DefaultAppFixture fixture) : base(fixture) { } + } + + [Collection("OverlayPopups")] + public class OverlayPopups : ComboBoxTests + { + public OverlayPopups(OverlayPopupsAppFixture fixture) : base(fixture) { } + } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Appium/DefaultAppFixture.cs similarity index 62% rename from tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs rename to tests/Avalonia.IntegrationTests.Appium/DefaultAppFixture.cs index d71f9e9bcc..bb08cc0514 100644 --- a/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs +++ b/tests/Avalonia.IntegrationTests.Appium/DefaultAppFixture.cs @@ -9,25 +9,21 @@ using OpenQA.Selenium.Appium.Windows; namespace Avalonia.IntegrationTests.Appium { - public class TestAppFixture : IDisposable + public class DefaultAppFixture : IDisposable { private const string TestAppPath = @"..\..\..\..\..\samples\IntegrationTestApp\bin\Debug\net7.0\IntegrationTestApp.exe"; private const string TestAppBundleId = "net.avaloniaui.avalonia.integrationtestapp"; - public TestAppFixture() + public DefaultAppFixture() { - var opts = new AppiumOptions(); - var path = Path.GetFullPath(TestAppPath); + var options = new AppiumOptions(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - opts.AddAdditionalCapability(MobileCapabilityType.App, path); - opts.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.Windows); - opts.AddAdditionalCapability(MobileCapabilityType.DeviceName, "WindowsPC"); - + ConfigureWin32Options(options); Session = new WindowsDriver( new Uri("http://127.0.0.1:4723"), - opts); + options); // https://github.com/microsoft/WinAppDriver/issues/1025 SetForegroundWindow(new IntPtr(int.Parse( @@ -36,14 +32,10 @@ namespace Avalonia.IntegrationTests.Appium } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - opts.AddAdditionalCapability("appium:bundleId", TestAppBundleId); - opts.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.MacOS); - opts.AddAdditionalCapability(MobileCapabilityType.AutomationName, "mac2"); - opts.AddAdditionalCapability("appium:showServerLogs", true); - + ConfigureMacOptions(options); Session = new MacDriver( new Uri("http://127.0.0.1:4723/wd/hub"), - opts); + options); } else { @@ -51,6 +43,22 @@ namespace Avalonia.IntegrationTests.Appium } } + protected virtual void ConfigureWin32Options(AppiumOptions options) + { + var path = Path.GetFullPath(TestAppPath); + options.AddAdditionalCapability(MobileCapabilityType.App, path); + options.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.Windows); + options.AddAdditionalCapability(MobileCapabilityType.DeviceName, "WindowsPC"); + } + + protected virtual void ConfigureMacOptions(AppiumOptions options) + { + options.AddAdditionalCapability("appium:bundleId", TestAppBundleId); + options.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.MacOS); + options.AddAdditionalCapability(MobileCapabilityType.AutomationName, "mac2"); + options.AddAdditionalCapability("appium:showServerLogs", true); + } + public AppiumDriver Session { get; } public void Dispose() diff --git a/tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs b/tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs deleted file mode 100644 index bb2dd1fbec..0000000000 --- a/tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Xunit; - -namespace Avalonia.IntegrationTests.Appium -{ - [CollectionDefinition("Default")] - public class DefaultCollection : ICollectionFixture - { - } -} diff --git a/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs b/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs index 9745f993cb..65864cc649 100644 --- a/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs @@ -11,7 +11,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public GestureTests(TestAppFixture fixture) + public GestureTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs index e2943b3349..5c81c20af1 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs @@ -11,7 +11,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public ListBoxTests(TestAppFixture fixture) + public ListBoxTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs index 3f1fe7de12..5f57dfbc19 100644 --- a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs @@ -7,11 +7,11 @@ using Xunit; namespace Avalonia.IntegrationTests.Appium { [Collection("Default")] - public class MenuTests + public abstract class MenuTests { private readonly AppiumDriver _session; - public MenuTests(TestAppFixture fixture) + public MenuTests(DefaultAppFixture fixture) { _session = fixture.Session; @@ -181,5 +181,17 @@ namespace Avalonia.IntegrationTests.Appium var tab = tabs.FindElementByName("Menu"); tab.MovePointerOver(); } + + [Collection("Default")] + public class Default : MenuTests + { + public Default(DefaultAppFixture fixture) : base(fixture) { } + } + + [Collection("OverlayPopups")] + public class OverlayPopups : MenuTests + { + public OverlayPopups(OverlayPopupsAppFixture fixture) : base(fixture) { } + } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs index 7858c4cc81..20594a9774 100644 --- a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public NativeMenuTests(TestAppFixture fixture) + public NativeMenuTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/OverlayPopupsAppFixture.cs b/tests/Avalonia.IntegrationTests.Appium/OverlayPopupsAppFixture.cs new file mode 100644 index 0000000000..1f8646888d --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/OverlayPopupsAppFixture.cs @@ -0,0 +1,19 @@ +using OpenQA.Selenium.Appium; + +namespace Avalonia.IntegrationTests.Appium +{ + public class OverlayPopupsAppFixture : DefaultAppFixture + { + protected override void ConfigureWin32Options(AppiumOptions options) + { + base.ConfigureWin32Options(options); + options.AddAdditionalCapability("appArguments", "--overlayPopups"); + } + + protected override void ConfigureMacOptions(AppiumOptions options) + { + base.ConfigureMacOptions(options); + options.AddAdditionalCapability("appium:arguments", new[] { "--overlayPopups" }); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs index 7fa5eb83ee..9371a49ade 100644 --- a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs @@ -10,7 +10,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public SliderTests(TestAppFixture fixture) + public SliderTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index ec24caa18c..a2bfb618d6 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -19,7 +19,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public WindowTests(TestAppFixture fixture) + public WindowTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 55812d8df7..2eaaf2e0a8 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -16,7 +16,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public WindowTests_MacOS(TestAppFixture fixture) + public WindowTests_MacOS(DefaultAppFixture fixture) { var retry = 0; From f87148dbbcba0f44987bc69595dcec2e8195d629 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 23 Feb 2023 14:42:45 +0100 Subject: [PATCH 2/8] Add failing unit test for #10420. --- .../Primitives/PopupTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 4804b29fee..bc1225e0e8 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -18,6 +18,7 @@ using Avalonia.Input; using Avalonia.Rendering; using System.Threading.Tasks; using Avalonia.Threading; +using Avalonia.Interactivity; namespace Avalonia.Controls.UnitTests.Primitives { @@ -1048,6 +1049,30 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Events_Should_Be_Routed_To_Popup_Parent() + { + using (CreateServices()) + { + var popupContent = new Border(); + var popup = new Popup { Child = popupContent }; + var popupParent = new Border { Child = popup }; + var root = PreparedWindow(popupParent); + var raised = 0; + + root.LayoutManager.ExecuteInitialLayoutPass(); + popup.Open(); + root.LayoutManager.ExecuteLayoutPass(); + + var ev = new RoutedEventArgs(Button.ClickEvent); + + popupParent.AddHandler(Button.ClickEvent, (s, e) => ++raised); + popupContent.RaiseEvent(ev); + + Assert.Equal(1, raised); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: From a7711a3b4af6f95a90d537a56d648e69ffdf1e07 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 23 Feb 2023 14:43:19 +0100 Subject: [PATCH 3/8] Route events to parent, not visual parent. Fixes #10420. --- src/Avalonia.Controls/Primitives/OverlayPopupHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index e16633483b..7ed055f2e5 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -51,7 +51,7 @@ namespace Avalonia.Controls.Primitives } /// - protected internal override Interactive? InteractiveParent => (Interactive?)VisualParent; + protected internal override Interactive? InteractiveParent => Parent as Interactive; /// public void Dispose() => Hide(); From f1759ab23eab6a6183922d4177394e3343388cd5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 23 Feb 2023 18:38:12 +0100 Subject: [PATCH 4/8] Fix merge error. --- .../RadioButtonTests.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/RadioButtonTests.cs b/tests/Avalonia.IntegrationTests.Appium/RadioButtonTests.cs index 5bd0a05155..26a8577cb0 100644 --- a/tests/Avalonia.IntegrationTests.Appium/RadioButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/RadioButtonTests.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium; using Xunit; namespace Avalonia.IntegrationTests.Appium @@ -14,7 +8,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public RadioButtonTests(TestAppFixture fixture) + public RadioButtonTests(DefaultAppFixture fixture) { _session = fixture.Session; From 034064a45f2d7510c289c9cb6d7d87c02378be1f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 23 Feb 2023 22:48:03 +0100 Subject: [PATCH 5/8] Use logical tree to detect if we're in a menu. Visual tree does not work when using overlay popups because the popups are displayed as visual children of the overlay layer. This is not a problem for native popups as each popup has its own `AccessKeyHandler`. --- src/Avalonia.Base/Input/AccessKeyHandler.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs index 59c66ed505..13ca140565 100644 --- a/src/Avalonia.Base/Input/AccessKeyHandler.cs +++ b/src/Avalonia.Base/Input/AccessKeyHandler.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Avalonia.Interactivity; -using Avalonia.VisualTree; +using Avalonia.LogicalTree; namespace Avalonia.Input { @@ -190,7 +189,7 @@ namespace Avalonia.Input // If the menu is open, only match controls in the menu's visual tree. if (menuIsOpen) { - matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsVisualAncestorOf((Visual)x)); + matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsLogicalAncestorOf((Visual)x)); } var match = matches.FirstOrDefault(); From d5a68b4b22c9719251830feb217e5b16a51f5385 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 23 Feb 2023 23:38:38 +0100 Subject: [PATCH 6/8] Ensure layout before trying to move selection. --- src/Avalonia.Controls/MenuItem.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 5588bde7c0..5c9dd4e193 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -309,7 +309,12 @@ namespace Avalonia.Controls protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; /// - bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); + bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) + { + if (Presenter?.Panel is null) + (VisualRoot as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass(); + return MoveSelection(direction, wrap); + } /// IMenuItem? IMenuElement.SelectedItem From f745fe178833efc87829e6cd9e5459939a99e67e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 23 Feb 2023 23:51:49 +0100 Subject: [PATCH 7/8] Use simpler event args type. The `PointerEnteredItem` and `PointerExitedItem` were using an obsolete API to create the event args. We don't need a `PointerEventArgs` anyway, just use `RoutedEventArgs`. --- src/Avalonia.Controls/MenuItem.cs | 23 +++++++---------- .../Platform/DefaultMenuInteractionHandler.cs | 4 +-- .../DefaultMenuInteractionHandlerTests.cs | 25 ++++++++----------- 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 5c9dd4e193..45fc2ed859 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -13,6 +13,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -85,16 +86,16 @@ namespace Avalonia.Controls /// /// Defines the event. /// - public static readonly RoutedEvent PointerEnteredItemEvent = - RoutedEvent.Register( + public static readonly RoutedEvent PointerEnteredItemEvent = + RoutedEvent.Register( nameof(PointerEnteredItem), RoutingStrategies.Bubble); /// /// Defines the event. /// - public static readonly RoutedEvent PointerExitedItemEvent = - RoutedEvent.Register( + public static readonly RoutedEvent PointerExitedItemEvent = + RoutedEvent.Register( nameof(PointerExitedItem), RoutingStrategies.Bubble); @@ -184,7 +185,7 @@ namespace Avalonia.Controls /// /// A bubbling version of the event for menu items. /// - public event EventHandler? PointerEnteredItem + public event EventHandler? PointerEnteredItem { add { AddHandler(PointerEnteredItemEvent, value); } remove { RemoveHandler(PointerEnteredItemEvent, value); } @@ -196,7 +197,7 @@ namespace Avalonia.Controls /// /// A bubbling version of the event for menu items. /// - public event EventHandler? PointerExitedItem + public event EventHandler? PointerExitedItem { add { AddHandler(PointerExitedItemEvent, value); } remove { RemoveHandler(PointerExitedItemEvent, value); } @@ -442,20 +443,14 @@ namespace Avalonia.Controls protected override void OnPointerEntered(PointerEventArgs e) { base.OnPointerEntered(e); - - var point = e.GetCurrentPoint(null); - RaiseEvent(new PointerEventArgs(PointerEnteredItemEvent, this, e.Pointer, (Visual?)VisualRoot, point.Position, - e.Timestamp, point.Properties, e.KeyModifiers)); + RaiseEvent(new RoutedEventArgs(PointerEnteredItemEvent)); } /// protected override void OnPointerExited(PointerEventArgs e) { base.OnPointerExited(e); - - var point = e.GetCurrentPoint(null); - RaiseEvent(new PointerEventArgs(PointerExitedItemEvent, this, e.Pointer, (Visual?)VisualRoot, point.Position, - e.Timestamp, point.Properties, e.KeyModifiers)); + RaiseEvent(new RoutedEventArgs(PointerExitedItemEvent)); } /// diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 4dd868253e..d2b23a7ac3 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -301,7 +301,7 @@ namespace Avalonia.Controls.Platform e.Handled = true; } - protected internal virtual void PointerEntered(object? sender, PointerEventArgs e) + protected internal virtual void PointerEntered(object? sender, RoutedEventArgs e) { var item = GetMenuItem(e.Source as Control); @@ -368,7 +368,7 @@ namespace Avalonia.Controls.Platform } } - protected internal virtual void PointerExited(object? sender, PointerEventArgs e) + protected internal virtual void PointerExited(object? sender, RoutedEventArgs e) { var item = GetMenuItem(e.Source as Control); diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index 15ff6e68e3..e5c96dcab6 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -11,9 +11,6 @@ namespace Avalonia.Controls.UnitTests.Platform { public class DefaultMenuInteractionHandlerTests { - static PointerEventArgs CreateArgs(RoutedEvent ev, object source) - => new PointerEventArgs(ev, source, new FakePointer(), (Visual)source, default, 0, PointerPointProperties.None, default); - static PointerPressedEventArgs CreatePressed(object source) => new PointerPressedEventArgs(source, new FakePointer(), (Visual)source, default,0, new PointerPointProperties (RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), default); @@ -171,7 +168,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, isSubMenuOpen: true, parent: menu.Object); var nextItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, nextItem.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, nextItem.Object); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); @@ -191,7 +188,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); target.PointerExited(item, e); @@ -206,7 +203,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); menu.SetupGet(x => x.IsOpen).Returns(true); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); @@ -365,7 +362,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); target.PointerEntered(item.Object, e); @@ -381,7 +378,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); target.PointerEntered(item.Object, e); item.Verify(x => x.Open(), Times.Never); @@ -401,7 +398,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); var sibling = CreateMockMenuItem(hasSubMenu: true, isSubMenuOpen: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); parentItem.SetupGet(x => x.SubItems).Returns(new[] { item.Object, sibling.Object }); @@ -421,7 +418,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); parentItem.SetupGet(x => x.SelectedItem).Returns(item.Object); target.PointerExited(item, e); @@ -438,7 +435,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); var sibling = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); parentItem.SetupGet(x => x.SelectedItem).Returns(sibling.Object); target.PointerExited(item, e); @@ -454,7 +451,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); item.Setup(x => x.IsPointerOverSubMenu).Returns(true); target.PointerExited(item, e); @@ -488,8 +485,8 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); var childItem = CreateMockMenuItem(parent: item.Object); - var enter = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); - var leave = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var enter = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var leave = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); // Pointer enters item; item is selected. target.PointerEntered(item, enter); From 150c4c01d0652bda4ee26f37bcbbda43bbabc50b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 24 Feb 2023 11:55:52 +0100 Subject: [PATCH 8/8] Ensure layout a little earlier. Fixes all integration tests with overlay popups. --- src/Avalonia.Controls/MenuItem.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 45fc2ed859..1670e496b4 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -310,12 +310,7 @@ namespace Avalonia.Controls protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; /// - bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) - { - if (Presenter?.Panel is null) - (VisualRoot as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass(); - return MoveSelection(direction, wrap); - } + bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); /// IMenuItem? IMenuElement.SelectedItem @@ -686,6 +681,12 @@ namespace Avalonia.Controls /// The event args. private void PopupOpened(object? sender, EventArgs e) { + // If we're using overlay popups, there's a chance we need to do a layout pass before + // the child items are added to the visual tree. If we don't do this here, then + // selection breaks. + if (Presenter?.IsAttachedToVisualTree == false) + UpdateLayout(); + var selected = SelectedIndex; if (selected != -1) @@ -705,6 +706,11 @@ namespace Avalonia.Controls SelectedItem = null; } + private void UpdateLayout() + { + (VisualRoot as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass(); + } + void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); void IClickableControl.RaiseClick()