From 0cc0443d34567f0d20082eaee29625a732b93849 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Sep 2022 22:33:03 +0200 Subject: [PATCH 1/4] Display window order in integration test app. The previous method of relying on the order of windows returned by appium to determine window z-order proved not to be reliable. Instead use p/invoke to read `NSWindow.orderedIndex` on a timer and display it in a text box in the `ShowWindowtest` window. This commit doesn't update the tests to use this new info though. --- .../IntegrationTestApp/MacOSIntegration.cs | 27 ++++++++++++++++++ .../IntegrationTestApp/ShowWindowTest.axaml | 8 ++++-- .../ShowWindowTest.axaml.cs | 28 +++++++++++++++++-- 3 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 samples/IntegrationTestApp/MacOSIntegration.cs diff --git a/samples/IntegrationTestApp/MacOSIntegration.cs b/samples/IntegrationTestApp/MacOSIntegration.cs new file mode 100644 index 0000000000..f700a5b4e2 --- /dev/null +++ b/samples/IntegrationTestApp/MacOSIntegration.cs @@ -0,0 +1,27 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Controls; + +namespace IntegrationTestApp +{ + public static class MacOSIntegration + { + [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "sel_registerName")] + private static extern IntPtr GetHandle(string name); + + [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")] + private static extern long Int64_objc_msgSend(IntPtr receiver, IntPtr selector); + + private static readonly IntPtr s_orderedIndexSelector; + + static MacOSIntegration() + { + s_orderedIndexSelector = GetHandle("orderedIndex");; + } + + public static long GetOrderedIndex(Window window) + { + return Int64_objc_msgSend(window.PlatformImpl!.Handle.Handle, s_orderedIndexSelector); + } + } +} diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 4001bac7e2..17c359df51 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -3,7 +3,7 @@ x:Class="IntegrationTestApp.ShowWindowTest" Name="SecondaryWindow" Title="Show Window Test"> - + @@ -31,6 +31,10 @@ Maximized Fullscreen - + + + + + diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index 001f186761..43875dd990 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -1,21 +1,32 @@ using System; +using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; -using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using Avalonia.Rendering; +using Avalonia.Threading; namespace IntegrationTestApp { public class ShowWindowTest : Window { + private readonly DispatcherTimer? _timer; + private readonly TextBox? _orderTextBox; + public ShowWindowTest() { InitializeComponent(); DataContext = this; PositionChanged += (s, e) => this.GetControl("Position").Text = $"{Position}"; - } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + _orderTextBox = this.GetControl("Order"); + _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; + _timer.Tick += TimerOnTick; + _timer.Start(); + } + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); @@ -36,5 +47,16 @@ namespace IntegrationTestApp ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}"; } } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + _timer?.Stop(); + } + + private void TimerOnTick(object? sender, EventArgs e) + { + _orderTextBox!.Text = MacOSIntegration.GetOrderedIndex(this).ToString(); + } } } From d1b652213a944e1e2b254b475b5d5c6915ca2694 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Sep 2022 22:33:39 +0200 Subject: [PATCH 2/4] Make sure child windows have unique titles/ids. --- samples/IntegrationTestApp/MainWindow.axaml.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 2085b5da2b..f72f83fcb8 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.Linq; using Avalonia; +using Avalonia.Automation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; +using Microsoft.CodeAnalysis; namespace IntegrationTestApp { @@ -63,6 +65,17 @@ namespace IntegrationTestApp WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex, }; + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + // Make sure the windows have unique names and AutomationIds. + var existing = lifetime.Windows.OfType().Count(); + if (existing > 0) + { + AutomationProperties.SetAutomationId(window, window.Name + (existing + 1)); + window.Title += $" {existing + 1}"; + } + } + if (size.HasValue) { window.Width = size.Value.Width; From 1c4a994e2c8796096821c92c8137db0f85e8249f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Sep 2022 22:36:12 +0200 Subject: [PATCH 3/4] Add failing test for #8878. And update the tests to use the value of `NSWindow.orderedIndex` exposed in 0cc0443d34567f0d20082eaee29625a732b93849 instead of relying on appium to give us a z-ordered list of windows. --- .../WindowTests_MacOS.cs | 89 +++++++++---------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 4e5344dd25..7eeab2f7f2 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -30,7 +30,7 @@ namespace Avalonia.IntegrationTests.Appium tab.Click(); return; } - catch (WebDriverException e) when (retry++ < 3) + catch (WebDriverException) when (retry++ < 3) { // MacOS sometimes seems to need a bit of time to get itself back in order after switching out // of fullscreen. @@ -49,19 +49,16 @@ namespace Avalonia.IntegrationTests.Appium { mainWindow.Click(); - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); - var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow"); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); + Assert.Equal(1, secondaryWindowIndex); } } [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() { - var mainWindow = FindWindow(_session, "MainWindow"); + var mainWindow = GetWindow("MainWindow"); using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual)) { @@ -70,24 +67,21 @@ namespace Avalonia.IntegrationTests.Appium .ClickAndHold() .Perform(); - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); - var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow"); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); new Actions(_session) .MoveToElement(mainWindow, 100, 1) .Release() .Perform(); - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); + Assert.Equal(1, secondaryWindowIndex); } } [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_In_Fullscreen() { - var mainWindow = FindWindow(_session, "MainWindow"); + var mainWindow = GetWindow("MainWindow"); var buttons = mainWindow.GetChromeButtons(); buttons.maximize.Click(); @@ -98,14 +92,8 @@ namespace Avalonia.IntegrationTests.Appium { using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual)) { - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); - var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow"); - - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); - - Thread.Sleep(5000); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); + Assert.Equal(1, secondaryWindowIndex); } } finally @@ -122,13 +110,8 @@ namespace Avalonia.IntegrationTests.Appium using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual)) { mainWindow.Click(); - - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); - var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow"); - - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); + Assert.Equal(1, secondaryWindowIndex); } } @@ -141,22 +124,35 @@ namespace Avalonia.IntegrationTests.Appium { mainWindow.Click(); - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); - var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow"); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); - Assert.Equal(1, secondaryWindowIndex); - Assert.Equal(0, mainWindowIndex); + Assert.Equal(2, secondaryWindowIndex); var sendToBack = _session.FindElementByAccessibilityId("SendToBack"); sendToBack.Click(); } } + [PlatformFact(TestPlatforms.MacOS)] + public void WindowOrder_Owned_Is_Correct_After_Closing_Window() + { + using (OpenWindow(new PixelSize(300, 500), ShowWindowMode.Owned, WindowStartupLocation.CenterOwner)) + { + // Open a second child window, and close it. + using (OpenWindow(new PixelSize(200, 200), ShowWindowMode.Owned, WindowStartupLocation.CenterOwner)) + { + } + + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); + + Assert.Equal(1, secondaryWindowIndex); + } + } + [PlatformFact(TestPlatforms.MacOS)] public void Parent_Window_Has_Disabled_ChromeButtons_When_Modal_Dialog_Shown() { - var window = FindWindow(_session, "MainWindow"); + var window = GetWindow("MainWindow"); var (closeButton, miniaturizeButton, zoomButton) = window.GetChromeButtons(); Assert.True(closeButton.Enabled); @@ -176,7 +172,7 @@ namespace Avalonia.IntegrationTests.Appium { using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { - var secondaryWindow = FindWindow(_session, "SecondaryWindow"); + var secondaryWindow = GetWindow("SecondaryWindow"); var (closeButton, miniaturizeButton, zoomButton) = secondaryWindow.GetChromeButtons(); Assert.True(closeButton.Enabled); @@ -192,7 +188,7 @@ namespace Avalonia.IntegrationTests.Appium { using (OpenWindow(new PixelSize(200, 100), mode, WindowStartupLocation.Manual)) { - var secondaryWindow = FindWindow(_session, "SecondaryWindow"); + var secondaryWindow = GetWindow("SecondaryWindow"); var (_, miniaturizeButton, _) = secondaryWindow.GetChromeButtons(); miniaturizeButton.Click(); @@ -220,7 +216,7 @@ namespace Avalonia.IntegrationTests.Appium // causes Appium to think it's a different window. OpenWindow(null, ShowWindowMode.Owned, WindowStartupLocation.Manual); - var secondaryWindow = FindWindow(_session, "SecondaryWindow"); + var secondaryWindow = GetWindow("SecondaryWindow"); var hideButton = secondaryWindow.FindElementByAccessibilityId("HideButton"); hideButton.Click(); @@ -236,7 +232,7 @@ namespace Avalonia.IntegrationTests.Appium _session.FindElementByAccessibilityId("RestoreAll").Click(); // Close the window manually. - secondaryWindow = FindWindow(_session, "SecondaryWindow"); + secondaryWindow = GetWindow("SecondaryWindow"); secondaryWindow.GetChromeButtons().close.Click(); } @@ -259,18 +255,19 @@ namespace Avalonia.IntegrationTests.Appium return showButton.OpenWindowWithClick(); } - private static int GetWindowOrder(IReadOnlyCollection elements, string identifier) + private AppiumWebElement GetWindow(string identifier) { - return elements.TakeWhile(x => - x.FindElementByXPath("XCUIElementTypeWindow")?.GetAttribute("identifier") != identifier).Count(); + // The Avalonia a11y tree currently exposes two nested Window elements, this is a bug and should be fixed + // but in the meantime use the `parent::' selector to return the parent "real" window. + return _session.FindElementByXPath( + $"XCUIElementTypeWindow//*[@identifier='{identifier}']/parent::XCUIElementTypeWindow"); } - private static AppiumWebElement FindWindow(AppiumDriver session, string identifier) + private int GetWindowOrder(string identifier) { - var windows = session.FindElementsByXPath("XCUIElementTypeWindow"); - return windows.First(x => - x.FindElementsByXPath("XCUIElementTypeWindow") - .Any(y => y.GetAttribute("identifier") == identifier)); + var window = GetWindow(identifier); + var order = window.FindElementByXPath("//*[@identifier='Order']"); + return int.Parse(order.Text); } public enum ShowWindowMode From b83b795f57f625b8d9e794008ea14c612dbb79f8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Sep 2022 22:38:47 +0200 Subject: [PATCH 4/4] Don't bring old parent window to front. When a window is closed, its parent window is set to null, which caused this code to run. This line caused the parent window to be brought to front, and should have also caused any other child windows to be brought to front, but it seems OSX ignored those requests, causing #8878. Simply don't bring the parent window to the front when a child window is closed. I couldn't work out why this code was necessary anyway, and all integration tests still pass. Fixes #8878. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 -- 1 file changed, 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index af8c53cb33..ddc50c26b6 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -91,8 +91,6 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) { if(_parent != nullptr) { _parent->_children.remove(this); - - _parent->BringToFront(); } auto cparent = dynamic_cast(parent);