diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index f133fa34f6..45d78b3926 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -224,7 +224,7 @@ HRESULT WindowBaseImpl::GetFrameSize(AvnSize *ret) { if (ret == nullptr) return E_POINTER; - if(Window != nullptr){ + if(Window != nullptr && _shown){ auto frame = [Window frame]; ret->Width = frame.size.width; ret->Height = frame.size.height; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 6db586f3ca..85a89955f4 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -119,13 +119,16 @@ void WindowImpl::BringToFront() { if(Window != nullptr) { - if(IsDialog()) + if (![Window isMiniaturized]) { - Activate(); - } - else - { - [Window orderFront:nullptr]; + if(IsDialog()) + { + Activate(); + } + else + { + [Window orderFront:nullptr]; + } } [Window invalidateShadow]; @@ -488,6 +491,8 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) { } if (_shown) { + _actualWindowState = _lastWindowState; + switch (state) { case Maximized: if (currentState == FullScreen) { @@ -545,7 +550,6 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) { break; } - _actualWindowState = _lastWindowState; WindowEvents->WindowStateChanged(_actualWindowState); } diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index aa2191c26b..3377979199 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -112,6 +112,7 @@ + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 1aba10ec30..9e180b12c5 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -82,7 +82,7 @@ namespace IntegrationTestApp break; } } - + private void SendToBack() { var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; @@ -92,7 +92,18 @@ namespace IntegrationTestApp window.Activate(); } } - + + private void RestoreAll() + { + var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; + + foreach (var window in lifetime.Windows) + { + if (window.WindowState == WindowState.Minimized) + window.WindowState = WindowState.Normal; + } + } + private void MenuClicked(object? sender, RoutedEventArgs e) { var clickedMenuItemTextBlock = this.FindControl("ClickedMenuItem"); @@ -117,6 +128,8 @@ namespace IntegrationTestApp SendToBack(); if (source?.Name == "ExitFullscreen") WindowState = WindowState.Normal; + if (source?.Name == "RestoreAll") + RestoreAll(); } } } diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index a263d8ab46..40c1642e91 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -3,12 +3,14 @@ x:Class="IntegrationTestApp.ShowWindowTest" Name="SecondaryWindow" Title="Show Window Test"> - + - + - + @@ -21,5 +23,13 @@ + + + + Normal + Minimized + Maximized + Fullscreen + diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index 720f7b1c12..001f186761 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -1,4 +1,5 @@ using System; +using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; @@ -11,6 +12,8 @@ namespace IntegrationTestApp public ShowWindowTest() { InitializeComponent(); + DataContext = this; + PositionChanged += (s, e) => this.GetControl("Position").Text = $"{Position}"; } private void InitializeComponent() @@ -21,17 +24,16 @@ namespace IntegrationTestApp protected override void OnOpened(EventArgs e) { base.OnOpened(e); - this.GetControl("ClientSize").Text = $"{Width}, {Height}"; - this.GetControl("FrameSize").Text = $"{FrameSize}"; + var scaling = PlatformImpl!.DesktopScaling; this.GetControl("Position").Text = $"{Position}"; this.GetControl("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; - this.GetControl("Scaling").Text = $"{PlatformImpl?.DesktopScaling}"; + this.GetControl("Scaling").Text = $"{scaling}"; if (Owner is not null) { var ownerRect = this.GetControl("OwnerRect"); var owner = (Window)Owner; - ownerRect.Text = $"{owner.Position}, {owner.FrameSize}"; + ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}"; } } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 92f74530e2..2dd391945b 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -901,10 +901,10 @@ namespace Avalonia.Controls { if (owner != null) { - // TODO: We really need non-client size here. + var ownerSize = owner.FrameSize ?? owner.ClientSize; var ownerRect = new PixelRect( owner.Position, - PixelSize.FromSize(owner.ClientSize, scaling)); + PixelSize.FromSize(ownerSize, scaling)); Position = ownerRect.CenterRect(rect).Position; } } diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index 16d37e4beb..4b361c6716 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -130,17 +130,10 @@ namespace Avalonia.IntegrationTests.Appium public static void SendClick(this AppiumWebElement element) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - element.Click(); - } - else - { - // The Click() method seems to correspond to accessibilityPerformPress on macOS but certain controls - // such as list items don't support this action, so instead simulate a physical click as VoiceOver - // does. - new Actions(element.WrappedDriver).MoveToElement(element).Click().Perform(); - } + // The Click() method seems to correspond to accessibilityPerformPress on macOS but certain controls + // such as list items don't support this action, so instead simulate a physical click as VoiceOver + // does. On Windows, Click() seems to fail with the WindowState checkbox for some reason. + new Actions(element.WrappedDriver).MoveToElement(element).Click().Perform(); } public static void MovePointerOver(this AppiumWebElement element) diff --git a/tests/Avalonia.IntegrationTests.Appium/PlatformTheoryAttribute.cs b/tests/Avalonia.IntegrationTests.Appium/PlatformTheoryAttribute.cs new file mode 100644 index 0000000000..7ac30ee11b --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/PlatformTheoryAttribute.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + internal class PlatformTheoryAttribute : TheoryAttribute + { + public PlatformTheoryAttribute(TestPlatforms platforms = TestPlatforms.All) => Platforms = platforms; + + public TestPlatforms Platforms { get; } + + public override string? Skip + { + get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}"; + set => throw new NotSupportedException(); + } + + private bool IsSupported() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return Platforms.HasAnyFlag(TestPlatforms.Windows); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return Platforms.HasAnyFlag(TestPlatforms.MacOS); + return false; + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index f1a625dbb4..2b10c302bc 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -1,7 +1,9 @@ using System; using System.Runtime.InteropServices; +using System.Threading; using Avalonia.Controls; using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Interactions; using Xunit; using Xunit.Sdk; @@ -23,35 +25,86 @@ namespace Avalonia.IntegrationTests.Appium [Theory] [MemberData(nameof(StartupLocationData))] - public void StartupLocation(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location) + public void StartupLocation(Size? size, ShowWindowMode mode, WindowStartupLocation location) { using var window = OpenWindow(size, mode, location); - var clientSize = Size.Parse(_session.FindElementByAccessibilityId("ClientSize").Text); - var frameSize = Size.Parse(_session.FindElementByAccessibilityId("FrameSize").Text); - var position = PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text); - var screenRect = PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text); - var scaling = double.Parse(_session.FindElementByAccessibilityId("Scaling").Text); + var info = GetWindowInfo(); - Assert.True(frameSize.Width >= clientSize.Width, "Expected frame width >= client width."); - Assert.True(frameSize.Height > clientSize.Height, "Expected frame height > client height."); + if (size.HasValue) + Assert.Equal(size.Value, info.ClientSize); + + Assert.True(info.FrameSize.Width >= info.ClientSize.Width, "Expected frame width >= client width."); + Assert.True(info.FrameSize.Height > info.ClientSize.Height, "Expected frame height > client height."); - var frameRect = new PixelRect(position, PixelSize.FromSize(frameSize, scaling)); + var frameRect = new PixelRect(info.Position, PixelSize.FromSize(info.FrameSize, info.Scaling)); switch (location) { case WindowStartupLocation.CenterScreen: - { - var expected = screenRect.CenterRect(frameRect); - AssertCloseEnough(expected.Position, frameRect.Position); - break; - } + { + var expected = info.ScreenRect.CenterRect(frameRect); + AssertCloseEnough(expected.Position, frameRect.Position); + break; + } + case WindowStartupLocation.CenterOwner: + { + Assert.NotNull(info.OwnerRect); + var expected = info.OwnerRect!.Value.CenterRect(frameRect); + AssertCloseEnough(expected.Position, frameRect.Position); + break; + } + } + } + + + [Theory] + [InlineData(ShowWindowMode.NonOwned)] + [InlineData(ShowWindowMode.Owned)] + [InlineData(ShowWindowMode.Modal)] + public void WindowState(ShowWindowMode mode) + { + using var window = OpenWindow(null, mode, WindowStartupLocation.Manual); + var windowState = _session.FindElementByAccessibilityId("WindowState"); + var original = GetWindowInfo(); + + Assert.Equal("Normal", windowState.GetComboBoxValue()); + + windowState.Click(); + _session.FindElementByName("Maximized").SendClick(); + Assert.Equal("Maximized", windowState.GetComboBoxValue()); + + windowState.Click(); + _session.FindElementByName("Normal").SendClick(); + + var current = GetWindowInfo(); + Assert.Equal(original.Position, current.Position); + Assert.Equal(original.FrameSize, current.FrameSize); + + // On macOS, only non-owned windows can go fullscreen. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || mode == ShowWindowMode.NonOwned) + { + windowState.Click(); + _session.FindElementByName("Fullscreen").SendClick(); + Assert.Equal("Fullscreen", windowState.GetComboBoxValue()); + + current = GetWindowInfo(); + var clientSize = PixelSize.FromSize(current.ClientSize, current.Scaling); + Assert.True(clientSize.Width >= current.ScreenRect.Width); + Assert.True(clientSize.Height >= current.ScreenRect.Height); + + windowState.Click(); + _session.FindElementByName("Normal").SendClick(); + + current = GetWindowInfo(); + Assert.Equal(original.Position, current.Position); + Assert.Equal(original.FrameSize, current.FrameSize); } } - - public static TheoryData StartupLocationData() + + public static TheoryData StartupLocationData() { - var sizes = new PixelSize?[] { null, new PixelSize(400, 300) }; - var data = new TheoryData(); + var sizes = new Size?[] { null, new Size(400, 300) }; + var data = new TheoryData(); foreach (var size in sizes) { @@ -96,7 +149,7 @@ namespace Avalonia.IntegrationTests.Appium } } - private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location) + private IDisposable OpenWindow(Size? size, ShowWindowMode mode, WindowStartupLocation location) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); @@ -115,11 +168,50 @@ namespace Avalonia.IntegrationTests.Appium return showButton.OpenWindowWithClick(); } + private WindowInfo GetWindowInfo() + { + PixelRect? ReadOwnerRect() + { + var text = _session.FindElementByAccessibilityId("OwnerRect").Text; + return !string.IsNullOrWhiteSpace(text) ? PixelRect.Parse(text) : null; + } + + var retry = 0; + + for (;;) + { + try + { + return new( + Size.Parse(_session.FindElementByAccessibilityId("ClientSize").Text), + Size.Parse(_session.FindElementByAccessibilityId("FrameSize").Text), + PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text), + ReadOwnerRect(), + PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text), + double.Parse(_session.FindElementByAccessibilityId("Scaling").Text)); + } + catch (OpenQA.Selenium.NoSuchElementException e) when (retry++ < 3) + { + // MacOS sometimes seems to need a bit of time to get itself back in order after switching out + // of fullscreen. + Thread.Sleep(1000); + } + } + } + public enum ShowWindowMode { NonOwned, Owned, Modal } + + private record WindowInfo( + Size ClientSize, + Size FrameSize, + PixelPoint Position, + PixelRect? OwnerRect, + PixelRect ScreenRect, + double Scaling); } } diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index a460dc1d68..15ca78fdac 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -17,11 +17,27 @@ namespace Avalonia.IntegrationTests.Appium public WindowTests_MacOS(TestAppFixture fixture) { + var retry = 0; + _session = fixture.Session; - var tabs = _session.FindElementByAccessibilityId("MainTabs"); - var tab = tabs.FindElementByName("Window"); - tab.Click(); + for (;;) + { + try + { + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Window"); + tab.Click(); + return; + } + catch (WebDriverException e) when (retry++ < 3) + { + // MacOS sometimes seems to need a bit of time to get itself back in order after switching out + // of fullscreen. + Thread.Sleep(1000); + } + } + } [PlatformFact(TestPlatforms.MacOS)] @@ -29,7 +45,7 @@ namespace Avalonia.IntegrationTests.Appium { var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual)) { mainWindow.Click(); @@ -41,13 +57,13 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(1, mainWindowIndex); } } - + [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() { var mainWindow = FindWindow(_session, "MainWindow"); - using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual)) { new Actions(_session) .MoveToElement(mainWindow, 100, 1) @@ -57,7 +73,7 @@ namespace Avalonia.IntegrationTests.Appium var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow"); - + new Actions(_session) .MoveToElement(mainWindow, 100, 1) .Release() @@ -67,20 +83,20 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(1, mainWindowIndex); } } - + [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_In_Fullscreen() { var mainWindow = FindWindow(_session, "MainWindow"); var buttons = mainWindow.GetChromeButtons(); - + buttons.maximize.Click(); Thread.Sleep(500); try { - using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual)) { var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); @@ -88,6 +104,8 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(0, secondaryWindowIndex); Assert.Equal(1, mainWindowIndex); + + Thread.Sleep(5000); } } finally @@ -95,13 +113,13 @@ namespace Avalonia.IntegrationTests.Appium _session.FindElementByAccessibilityId("ExitFullscreen").Click(); } } - + [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_Owned_Dialog_Stays_InFront_Of_Parent() { var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.CenterOwner)) + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual)) { mainWindow.Click(); @@ -119,7 +137,7 @@ namespace Avalonia.IntegrationTests.Appium { var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - using (OpenWindow(new PixelSize(1400, 100), ShowWindowMode.NonOwned, WindowStartupLocation.CenterOwner)) + using (OpenWindow(new PixelSize(1400, 100), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) { mainWindow.Click(); @@ -140,7 +158,7 @@ namespace Avalonia.IntegrationTests.Appium { var window = FindWindow(_session, "MainWindow"); var (closeButton, miniaturizeButton, zoomButton) = window.GetChromeButtons(); - + Assert.True(closeButton.Enabled); Assert.True(zoomButton.Enabled); Assert.True(miniaturizeButton.Enabled); @@ -152,7 +170,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.False(miniaturizeButton.Enabled); } } - + [PlatformFact(TestPlatforms.MacOS)] public void Minimize_Button_Is_Disabled_On_Modal_Dialog() { @@ -160,13 +178,39 @@ namespace Avalonia.IntegrationTests.Appium { var secondaryWindow = FindWindow(_session, "SecondaryWindow"); var (closeButton, miniaturizeButton, zoomButton) = secondaryWindow.GetChromeButtons(); - + Assert.True(closeButton.Enabled); Assert.True(zoomButton.Enabled); Assert.False(miniaturizeButton.Enabled); } } + [PlatformTheory(TestPlatforms.MacOS)] + [InlineData(ShowWindowMode.NonOwned)] + [InlineData(ShowWindowMode.Owned)] + public void Minimize_Button_Minimizes_Window(ShowWindowMode mode) + { + using (OpenWindow(new PixelSize(200, 100), mode, WindowStartupLocation.Manual)) + { + var secondaryWindow = FindWindow(_session, "SecondaryWindow"); + var (_, miniaturizeButton, _) = secondaryWindow.GetChromeButtons(); + + miniaturizeButton.Click(); + Thread.Sleep(1000); + + var hittable = _session.FindElementsByXPath("/XCUIElementTypeApplication/XCUIElementTypeWindow") + .Select(x => x.GetAttribute("hittable")).ToList(); + Assert.Equal(new[] { "true", "false" }, hittable); + + _session.FindElementByAccessibilityId("RestoreAll").Click(); + Thread.Sleep(1000); + + hittable = _session.FindElementsByXPath("/XCUIElementTypeApplication/XCUIElementTypeWindow") + .Select(x => x.GetAttribute("hittable")).ToList(); + Assert.Equal(new[] { "true", "true" }, hittable); + } + } + private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); @@ -195,7 +239,7 @@ namespace Avalonia.IntegrationTests.Appium private static AppiumWebElement FindWindow(AppiumDriver session, string identifier) { var windows = session.FindElementsByXPath("XCUIElementTypeWindow"); - return windows.First(x => + return windows.First(x => x.FindElementsByXPath("XCUIElementTypeWindow") .Any(y => y.GetAttribute("identifier") == identifier)); } diff --git a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh new file mode 100755 index 0000000000..30a4a79f4a --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh @@ -0,0 +1,8 @@ +# Cleans, builds, and runs integration tests on macOS. +# Can be used by `git bisect run` to automatically find the commit which introduced a problem. +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +cd "$SCRIPT_DIR"/../.. || exit +git clean -xdf +./build.sh CompileNative +./samples/IntegrationTestApp/bundle.sh +dotnet test tests/Avalonia.IntegrationTests.Appium/ -l "console;verbosity=detailed"