Browse Source

Merge pull request #8232 from AvaloniaUI/feature/window-integration-tests

Feature/window integration tests
Steven Kirk 3 years ago
parent
commit
b74a2bd8ac

+ 20 - 0
samples/IntegrationTestApp/MainWindow.axaml

@@ -4,6 +4,7 @@
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
         x:Class="IntegrationTestApp.MainWindow"
+        Name="MainWindow"
         Title="IntegrationTestApp">
   <NativeMenu.Menu>
     <NativeMenu>
@@ -94,6 +95,25 @@
           </StackPanel>
         </DockPanel>
       </TabItem>
+      
+      <TabItem Header="Window">
+        <StackPanel>
+          <TextBox Name="ShowWindowSize" Watermark="Window Size"/>
+          <ComboBox Name="ShowWindowMode" SelectedIndex="0">
+            <ComboBoxItem>NonOwned</ComboBoxItem>
+            <ComboBoxItem>Owned</ComboBoxItem>
+            <ComboBoxItem>Modal</ComboBoxItem>
+          </ComboBox>
+          <ComboBox Name="ShowWindowLocation" SelectedIndex="0">
+            <ComboBoxItem>Manual</ComboBoxItem>
+            <ComboBoxItem>CenterScreen</ComboBoxItem>
+            <ComboBoxItem>CenterOwner</ComboBoxItem>
+          </ComboBox>
+          <Button Name="ShowWindow">Show Window</Button>
+          <Button Name="SendToBack">Send to Back</Button>
+          <Button Name="ExitFullscreen">Exit Fullscreen</Button>
+        </StackPanel>
+      </TabItem>
     </TabControl>
   </DockPanel>
 </Window>

+ 53 - 0
samples/IntegrationTestApp/MainWindow.axaml.cs

@@ -3,8 +3,10 @@ using System.Collections.Generic;
 using System.Linq;
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
 
 namespace IntegrationTestApp
 {
@@ -46,6 +48,51 @@ namespace IntegrationTestApp
             }
         }
 
+        private void ShowWindow()
+        {
+            var sizeTextBox = this.GetControl<TextBox>("ShowWindowSize");
+            var modeComboBox = this.GetControl<ComboBox>("ShowWindowMode");
+            var locationComboBox = this.GetControl<ComboBox>("ShowWindowLocation");
+            var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null;
+            var owner = (Window)this.GetVisualRoot()!;
+
+            var window = new ShowWindowTest
+            {
+                WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex,
+            };
+
+            if (size.HasValue)
+            {
+                window.Width = size.Value.Width;
+                window.Height = size.Value.Height;
+            }
+
+            sizeTextBox.Text = string.Empty;
+
+            switch (modeComboBox.SelectedIndex)
+            {
+                case 0:
+                    window.Show();
+                    break;
+                case 1:
+                    window.Show(owner);
+                    break;
+                case 2:
+                    window.ShowDialog(owner);
+                    break;
+            }
+        }
+        
+        private void SendToBack()
+        {
+            var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
+
+            foreach (var window in lifetime.Windows)
+            {
+                window.Activate();
+            }
+        }
+        
         private void MenuClicked(object? sender, RoutedEventArgs e)
         {
             var clickedMenuItemTextBlock = this.FindControl<TextBlock>("ClickedMenuItem");
@@ -64,6 +111,12 @@ namespace IntegrationTestApp
                 this.FindControl<ListBox>("BasicListBox").SelectedIndex = -1;
             if (source?.Name == "MenuClickedMenuItemReset")
                 this.FindControl<TextBlock>("ClickedMenuItem").Text = "None";
+            if (source?.Name == "ShowWindow")
+                ShowWindow();
+            if (source?.Name == "SendToBack")
+                SendToBack();
+            if (source?.Name == "ExitFullscreen")
+                WindowState = WindowState.Normal;
         }
     }
 }

+ 25 - 0
samples/IntegrationTestApp/ShowWindowTest.axaml

@@ -0,0 +1,25 @@
+<Window xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        x:Class="IntegrationTestApp.ShowWindowTest"
+        Name="SecondaryWindow"
+        Title="Show Window Test">
+  <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
+    <Label Grid.Column="0" Grid.Row="1">Client Size</Label>
+    <TextBox Name="ClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"/>
+    
+    <Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
+    <TextBox Name="FrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"/>
+
+    <Label Grid.Column="0" Grid.Row="3">Position</Label>
+    <TextBox Name="Position" Grid.Column="1" Grid.Row="3" IsReadOnly="True"/>
+
+    <Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
+    <TextBox Name="OwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True"/>
+    
+    <Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
+    <TextBox Name="ScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True"/>
+
+    <Label Grid.Column="0" Grid.Row="6">Scaling</Label>
+    <TextBox Name="Scaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True"/>
+  </Grid>
+</Window>

+ 38 - 0
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@@ -0,0 +1,38 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.Rendering;
+
+namespace IntegrationTestApp
+{
+    public class ShowWindowTest : Window
+    {
+        public ShowWindowTest()
+        {
+            InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        protected override void OnOpened(EventArgs e)
+        {
+            base.OnOpened(e);
+            this.GetControl<TextBox>("ClientSize").Text = $"{Width}, {Height}";
+            this.GetControl<TextBox>("FrameSize").Text = $"{FrameSize}";
+            this.GetControl<TextBox>("Position").Text = $"{Position}";
+            this.GetControl<TextBox>("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}";
+            this.GetControl<TextBox>("Scaling").Text = $"{PlatformImpl?.DesktopScaling}";
+
+            if (Owner is not null)
+            {
+                var ownerRect = this.GetControl<TextBox>("OwnerRect");
+                var owner = (Window)Owner;
+                ownerRect.Text = $"{owner.Position}, {owner.FrameSize}";
+            }
+        }
+    }
+}

+ 5 - 1
src/Avalonia.Controls/TopLevel.cs

@@ -484,7 +484,11 @@ namespace Avalonia.Controls
         /// Raises the <see cref="Opened"/> event.
         /// </summary>
         /// <param name="e">The event args.</param>
-        protected virtual void OnOpened(EventArgs e) => Opened?.Invoke(this, e);
+        protected virtual void OnOpened(EventArgs e)
+        {
+            FrameSize = PlatformImpl?.FrameSize;
+            Opened?.Invoke(this, e);  
+        } 
 
         /// <summary>
         /// Raises the <see cref="Closed"/> event.

+ 24 - 24
src/Avalonia.Controls/Window.cs

@@ -871,10 +871,10 @@ namespace Avalonia.Controls
 
             var scaling = owner?.DesktopScaling ?? PlatformImpl?.DesktopScaling ?? 1;
 
-            // TODO: We really need non-client size here.
-            var rect = new PixelRect(
-                PixelPoint.Origin,
-                PixelSize.FromSize(ClientSize, scaling));
+            // Use frame size, falling back to client size if the platform can't give it to us.
+            var rect = FrameSize.HasValue ?
+                new PixelRect(PixelSize.FromSize(FrameSize.Value, scaling)) :
+                new PixelRect(PixelSize.FromSize(ClientSize, scaling));
 
             if (startupLocation == WindowStartupLocation.CenterScreen)
             {
@@ -991,28 +991,28 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         protected sealed override void HandleResized(Size clientSize, PlatformResizeReason reason)
         {
-            if (ClientSize == clientSize)
-                return;
-
-            var sizeToContent = SizeToContent;
-
-            // If auto-sizing is enabled, and the resize came from a user resize (or the reason was
-            // unspecified) then turn off auto-resizing for any window dimension that is not equal
-            // to the requested size.
-            if (sizeToContent != SizeToContent.Manual &&
-                CanResize &&
-                reason == PlatformResizeReason.Unspecified ||  
-                reason == PlatformResizeReason.User)
+            if (ClientSize != clientSize || double.IsNaN(Width) || double.IsNaN(Height))
             {
-                if (clientSize.Width != ClientSize.Width)
-                    sizeToContent &= ~SizeToContent.Width;
-                if (clientSize.Height != ClientSize.Height)
-                    sizeToContent &= ~SizeToContent.Height;
-                SizeToContent = sizeToContent;
-            }
+                var sizeToContent = SizeToContent;
+
+                // If auto-sizing is enabled, and the resize came from a user resize (or the reason was
+                // unspecified) then turn off auto-resizing for any window dimension that is not equal
+                // to the requested size.
+                if (sizeToContent != SizeToContent.Manual &&
+                    CanResize &&
+                    reason == PlatformResizeReason.Unspecified ||
+                    reason == PlatformResizeReason.User)
+                {
+                    if (clientSize.Width != ClientSize.Width)
+                        sizeToContent &= ~SizeToContent.Width;
+                    if (clientSize.Height != ClientSize.Height)
+                        sizeToContent &= ~SizeToContent.Height;
+                    SizeToContent = sizeToContent;
+                }
 
-            Width = clientSize.Width;
-            Height = clientSize.Height;
+                Width = clientSize.Width;
+                Height = clientSize.Height;
+            }
 
             base.HandleResized(clientSize, reason);
         }

+ 7 - 3
src/Avalonia.Controls/WindowBase.cs

@@ -236,10 +236,14 @@ namespace Avalonia.Controls
         /// <param name="reason">The reason for the resize.</param>
         protected override void HandleResized(Size clientSize, PlatformResizeReason reason)
         {
-            ClientSize = clientSize;
             FrameSize = PlatformImpl?.FrameSize;
-            LayoutManager.ExecuteLayoutPass();
-            Renderer?.Resized(clientSize);
+
+            if (ClientSize != clientSize)
+            {
+                ClientSize = clientSize;
+                LayoutManager.ExecuteLayoutPass();
+                Renderer?.Resized(clientSize);
+            }
         }
 
         /// <summary>

+ 27 - 0
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -695,6 +695,31 @@ namespace Avalonia.Controls.UnitTests
                 }
             }
 
+            [Fact]
+            public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_Manual()
+            {
+                using (UnitTestApplication.Start(TestServices.StyledWindow))
+                {
+                    var child = new Canvas
+                    {
+                        Width = 400,
+                        Height = 800,
+                    };
+
+                    var target = new Window()
+                    {
+                        SizeToContent = SizeToContent.Manual,
+                        Content = child
+                    };
+
+                    Show(target);
+
+                    // Values come from MockWindowingPlatform defaults.
+                    Assert.Equal(800, target.Width);
+                    Assert.Equal(600, target.Height);
+                }
+            }
+
             [Fact]
             public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight()
             {
@@ -712,6 +737,8 @@ namespace Avalonia.Controls.UnitTests
                         Content = child
                     };
 
+                    target.GetObservable(Window.WidthProperty).Subscribe(x => { });
+
                     Show(target);
 
                     Assert.Equal(400, target.Width);

+ 4 - 0
tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj

@@ -5,6 +5,10 @@
     <Nullable>enable</Nullable>
   </PropertyGroup>
 
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
+  </ItemGroup>
+  
   <ItemGroup>
     <PackageReference Include="Appium.WebDriver" Version="4.3.1" />
     <PackageReference Include="Xunit.Extensions.Ordering" Version="1.4.5" />

+ 1 - 1
tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs

@@ -44,7 +44,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("Button with TextBlock", button.Text);
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void ButtonWithAcceleratorKey()
         {
             var button = _session.FindElementByAccessibilityId("ButtonWithAcceleratorKey");

+ 3 - 3
tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs

@@ -46,7 +46,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("Item 0", comboBox.GetComboBoxValue());
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void Can_Change_Selection_With_Keyboard()
         {
             var comboBox = _session.FindElementByAccessibilityId("BasicComboBox");
@@ -63,7 +63,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("Item 1", comboBox.GetComboBoxValue());
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void Can_Change_Selection_With_Keyboard_From_Unselected()
         {
             var comboBox = _session.FindElementByAccessibilityId("BasicComboBox");
@@ -80,7 +80,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("Item 0", comboBox.GetComboBoxValue());
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void Can_Cancel_Keyboard_Selection_With_Escape()
         {
             var comboBox = _session.FindElementByAccessibilityId("BasicComboBox");

+ 85 - 0
tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs

@@ -1,8 +1,12 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Disposables;
 using System.Runtime.InteropServices;
+using OpenQA.Selenium;
 using OpenQA.Selenium.Appium;
 using OpenQA.Selenium.Interactions;
+using Xunit;
 
 namespace Avalonia.IntegrationTests.Appium
 {
@@ -11,6 +15,19 @@ namespace Avalonia.IntegrationTests.Appium
         public static IReadOnlyList<AppiumWebElement> GetChildren(this AppiumWebElement element) =>
             element.FindElementsByXPath("*/*");
 
+        public static (AppiumWebElement close, AppiumWebElement minimize, AppiumWebElement maximize) GetChromeButtons(this AppiumWebElement window)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                var closeButton = window.FindElementByXPath("//XCUIElementTypeButton[1]");
+                var fullscreenButton = window.FindElementByXPath("//XCUIElementTypeButton[2]");
+                var minimizeButton = window.FindElementByXPath("//XCUIElementTypeButton[3]");
+                return (closeButton, minimizeButton, fullscreenButton);
+            }
+
+            throw new NotSupportedException("GetChromeButtons not supported on this platform.");
+        }
+
         public static string GetComboBoxValue(this AppiumWebElement element)
         {
             return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
@@ -43,6 +60,74 @@ namespace Avalonia.IntegrationTests.Appium
             }
         }
 
+        /// <summary>
+        /// Clicks a button which is expected to open a new window.
+        /// </summary>
+        /// <param name="element">The button to click.</param>
+        /// <returns>
+        /// An object which when disposed will cause the newly opened window to close.
+        /// </returns>
+        public static IDisposable OpenWindowWithClick(this AppiumWebElement element)
+        {
+            var session = element.WrappedDriver;
+
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                var oldHandle = session.CurrentWindowHandle;
+                var oldHandles = session.WindowHandles.ToList();
+                var oldChildWindows = session.FindElements(By.XPath("//Window"));
+
+                element.Click();
+
+                var newHandle = session.WindowHandles.Except(oldHandles).SingleOrDefault();
+
+                if (newHandle is not null)
+                {
+                    // A new top-level window was opened. We need to switch to it.
+                    session.SwitchTo().Window(newHandle);
+
+                    return Disposable.Create(() =>
+                    {
+                        session.Close();
+                        session.SwitchTo().Window(oldHandle);
+                    });
+                }
+                else
+                {
+                    // If a new window handle hasn't been added to the session then it's likely
+                    // that a child window was opened. These don't appear in session.WindowHandles
+                    // so we have to use an XPath query to get hold of it.
+                    var newChildWindows = session.FindElements(By.XPath("//Window"));
+                    var childWindow = Assert.Single(newChildWindows.Except(oldChildWindows));
+
+                    return Disposable.Create(() =>
+                    {
+                        childWindow.SendKeys(Keys.Alt + Keys.F4 + Keys.Alt);
+                    });
+                }
+            }
+            else
+            {
+                var oldWindows = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow"));
+                var oldWindowTitles = oldWindows.ToDictionary(x => x.Text);
+                
+                element.Click();
+
+                var newWindows = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow"));
+                var newWindowTitles = newWindows.ToDictionary(x => x.Text);
+                var newWindowTitle = Assert.Single(newWindowTitles.Keys.Except(oldWindowTitles.Keys));
+                var newWindow = (AppiumWebElement)newWindowTitles[newWindowTitle]; 
+                
+                return Disposable.Create(() =>
+                {
+                    // TODO: We should be able to use Cmd+W here but Avalonia apps don't seem to have this shortcut
+                    // set up by default.
+                    var (close, _, _) = newWindow.GetChromeButtons();
+                    close!.Click();
+                });
+            }
+        }
+
         public static void SendClick(this AppiumWebElement element)
         {
             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

+ 1 - 1
tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs

@@ -61,7 +61,7 @@ namespace Avalonia.IntegrationTests.Appium
         }
 
         // appium-mac2-driver just hangs
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void Can_Select_Range_By_Shift_Clicking()
         {
             var listBox = GetTarget();

+ 8 - 8
tests/Avalonia.IntegrationTests.Appium/MenuTests.cs

@@ -57,7 +57,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("_Grandchild", clickedMenuItem.Text);
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void Select_Child_With_Alt_Arrow_Keys()
         {
             new Actions(_session)
@@ -69,7 +69,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("_Child 1", clickedMenuItem.Text);
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void Select_Grandchild_With_Alt_Arrow_Keys()
         {
             new Actions(_session)
@@ -81,7 +81,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("_Grandchild", clickedMenuItem.Text);
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void Select_Child_With_Alt_Access_Keys()
         {
             new Actions(_session)
@@ -93,7 +93,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("_Child 1", clickedMenuItem.Text);
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void Select_Grandchild_With_Alt_Access_Keys()
         {
             new Actions(_session)
@@ -105,7 +105,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("_Grandchild", clickedMenuItem.Text);
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void Select_Child_With_Click_Arrow_Keys()
         {
             var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem");
@@ -119,7 +119,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("_Child 1", clickedMenuItem.Text);
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void Select_Grandchild_With_Click_Arrow_Keys()
         {
             var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem");
@@ -133,7 +133,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("_Grandchild", clickedMenuItem.Text);
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void Child_AcceleratorKey()
         {
             var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem");
@@ -145,7 +145,7 @@ namespace Avalonia.IntegrationTests.Appium
             Assert.Equal("Ctrl+O", childMenuItem.GetAttribute("AcceleratorKey"));
         }
 
-        [PlatformFact(SkipOnOSX = true)]
+        [PlatformFact(TestPlatforms.Windows)]
         public void PointerOver_Does_Not_Steal_Focus()
         {
             // Issue #7906

+ 1 - 1
tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs

@@ -17,7 +17,7 @@ namespace Avalonia.IntegrationTests.Appium
             tab.Click();
         }
 
-        [PlatformFact(SkipOnWindows = true)]
+        [PlatformFact(TestPlatforms.MacOS)]
         public void View_Menu_Select_Button_Tab()
         {
             var tabs = _session.FindElementByAccessibilityId("MainTabs");

+ 22 - 10
tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs

@@ -5,21 +5,33 @@ using Xunit;
 
 namespace Avalonia.IntegrationTests.Appium
 {
+    [Flags]
+    internal enum TestPlatforms
+    {
+        Windows = 0x01,
+        MacOS = 0x02,
+        All = Windows | MacOS,
+    }
+    
     internal class PlatformFactAttribute : FactAttribute
     {
+        public PlatformFactAttribute(TestPlatforms platforms = TestPlatforms.All) => Platforms = platforms;
+        
+        public TestPlatforms Platforms { get; }
+        
         public override string? Skip
         {
-            get
-            {
-                if (SkipOnWindows && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-                    return "Ignored on Windows";
-                if (SkipOnOSX && RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
-                    return "Ignored on MacOS";
-                return null;
-            }
+            get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}";
             set => throw new NotSupportedException();
         }
-        public bool SkipOnOSX { get; set; }
-        public bool SkipOnWindows { get; set; }
+
+        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;
+        }
     }
 }

+ 125 - 0
tests/Avalonia.IntegrationTests.Appium/WindowTests.cs

@@ -0,0 +1,125 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Controls;
+using OpenQA.Selenium.Appium;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Avalonia.IntegrationTests.Appium
+{
+    [Collection("Default")]
+    public class WindowTests
+    {
+        private readonly AppiumDriver<AppiumWebElement> _session;
+
+        public WindowTests(TestAppFixture fixture)
+        {
+            _session = fixture.Session;
+
+            var tabs = _session.FindElementByAccessibilityId("MainTabs");
+            var tab = tabs.FindElementByName("Window");
+            tab.Click();
+        }
+
+        [Theory]
+        [MemberData(nameof(StartupLocationData))]
+        public void StartupLocation(PixelSize? 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);
+
+            Assert.True(frameSize.Width >= clientSize.Width, "Expected frame width >= client width.");
+            Assert.True(frameSize.Height > clientSize.Height, "Expected frame height > client height.");
+
+            var frameRect = new PixelRect(position, PixelSize.FromSize(frameSize, scaling));
+
+            switch (location)
+            {
+                case WindowStartupLocation.CenterScreen:
+                    {
+                        var expected = screenRect.CenterRect(frameRect);
+                        AssertCloseEnough(expected.Position, frameRect.Position);
+                        break;
+                    }
+            }
+        }
+        
+        public static TheoryData<PixelSize?, ShowWindowMode, WindowStartupLocation> StartupLocationData()
+        {
+            var sizes = new PixelSize?[] { null, new PixelSize(400, 300) };
+            var data = new TheoryData<PixelSize?, ShowWindowMode, WindowStartupLocation>();
+
+            foreach (var size in sizes)
+            {
+                foreach (var mode in Enum.GetValues<ShowWindowMode>())
+                {
+                    foreach (var location in Enum.GetValues<WindowStartupLocation>())
+                    {
+                        if (!(location == WindowStartupLocation.CenterOwner && mode == ShowWindowMode.NonOwned))
+                        {
+                            data.Add(size, mode, location);
+                        }
+                    }
+                }
+            }
+
+            return data;
+        }
+
+        private static void AssertCloseEnough(PixelPoint expected, PixelPoint actual)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                // On win32, accurate frame information cannot be obtained until a window is shown but
+                // WindowStartupLocation needs to be calculated before the window is shown, meaning that
+                // the position of a centered window can be off by a bit. From initial testing, looks
+                // like this shouldn't be more than 10 pixels.
+                if (Math.Abs(expected.X - actual.X) > 10)
+                    throw new EqualException(expected, actual);
+                if (Math.Abs(expected.Y - actual.Y) > 10)
+                    throw new EqualException(expected, actual);
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                if (Math.Abs(expected.X - actual.X) > 15)
+                    throw new EqualException(expected, actual);
+                if (Math.Abs(expected.Y - actual.Y) > 15)
+                    throw new EqualException(expected, actual);
+            }
+            else
+            {
+                Assert.Equal(expected, actual);
+            }
+        }
+
+        private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location)
+        {
+            var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize");
+            var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode");
+            var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation");
+            var showButton = _session.FindElementByAccessibilityId("ShowWindow");
+
+            if (size.HasValue)
+                sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}");
+
+            modeComboBox.Click();
+            _session.FindElementByName(mode.ToString()).SendClick();
+
+            locationComboBox.Click();
+            _session.FindElementByName(location.ToString()).SendClick();
+
+            return showButton.OpenWindowWithClick();
+        }
+
+        public enum ShowWindowMode
+        {
+            NonOwned,
+            Owned,
+            Modal
+        }
+    }
+}

+ 210 - 0
tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs

@@ -0,0 +1,210 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Avalonia.Controls;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Appium;
+using OpenQA.Selenium.Interactions;
+using Xunit;
+
+namespace Avalonia.IntegrationTests.Appium
+{
+    [Collection("Default")]
+    public class WindowTests_MacOS
+    {
+        private readonly AppiumDriver<AppiumWebElement> _session;
+
+        public WindowTests_MacOS(TestAppFixture fixture)
+        {
+            _session = fixture.Session;
+
+            var tabs = _session.FindElementByAccessibilityId("MainTabs");
+            var tab = tabs.FindElementByName("Window");
+            tab.Click();
+        }
+
+        [PlatformFact(TestPlatforms.MacOS)]
+        public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent()
+        {
+            var mainWindow = _session.FindElementByAccessibilityId("MainWindow");
+
+            using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner))
+            {
+                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);
+            }
+        }
+        
+        [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))
+            {
+                new Actions(_session)
+                    .MoveToElement(mainWindow, 100, 1)
+                    .ClickAndHold()
+                    .Perform();
+
+                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()
+                    .Perform();
+
+                Assert.Equal(0, secondaryWindowIndex);
+                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))
+                {
+                    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);
+                }
+            }
+            finally
+            {
+                _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))
+            {
+                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);
+            }
+        }
+
+        [PlatformFact(TestPlatforms.MacOS)]
+        public void WindowOrder_NonOwned_Window_Does_Not_Stay_InFront_Of_Parent()
+        {
+            var mainWindow = _session.FindElementByAccessibilityId("MainWindow");
+
+            using (OpenWindow(new PixelSize(1400, 100), ShowWindowMode.NonOwned, WindowStartupLocation.CenterOwner))
+            {
+                mainWindow.Click();
+
+                var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow"));
+                var mainWindowIndex = GetWindowOrder(windows, "MainWindow");
+                var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow");
+
+                Assert.Equal(1, secondaryWindowIndex);
+                Assert.Equal(0, mainWindowIndex);
+
+                var sendToBack = _session.FindElementByAccessibilityId("SendToBack");
+                sendToBack.Click();
+            }
+        }
+
+        [PlatformFact(TestPlatforms.MacOS)]
+        public void Parent_Window_Has_Disabled_ChromeButtons_When_Modal_Dialog_Shown()
+        {
+            var window = FindWindow(_session, "MainWindow");
+            var (closeButton, miniaturizeButton, zoomButton) = window.GetChromeButtons();
+                
+            Assert.True(closeButton.Enabled);
+            Assert.True(zoomButton.Enabled);
+            Assert.True(miniaturizeButton.Enabled);
+
+            using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner))
+            {
+                Assert.False(closeButton.Enabled);
+                Assert.False(zoomButton.Enabled);
+                Assert.False(miniaturizeButton.Enabled);
+            }
+        }
+        
+        [PlatformFact(TestPlatforms.MacOS)]
+        public void Minimize_Button_Is_Disabled_On_Modal_Dialog()
+        {
+            using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner))
+            {
+                var secondaryWindow = FindWindow(_session, "SecondaryWindow");
+                var (closeButton, miniaturizeButton, zoomButton) = secondaryWindow.GetChromeButtons();
+                    
+                Assert.True(closeButton.Enabled);
+                Assert.True(zoomButton.Enabled);
+                Assert.False(miniaturizeButton.Enabled);
+            }
+        }
+
+        private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location)
+        {
+            var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize");
+            var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode");
+            var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation");
+            var showButton = _session.FindElementByAccessibilityId("ShowWindow");
+
+            if (size.HasValue)
+                sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}");
+
+            modeComboBox.Click();
+            _session.FindElementByName(mode.ToString()).SendClick();
+
+            locationComboBox.Click();
+            _session.FindElementByName(location.ToString()).SendClick();
+
+            return showButton.OpenWindowWithClick();
+        }
+
+        private static int GetWindowOrder(IReadOnlyCollection<AppiumWebElement> elements, string identifier)
+        {
+            return elements.TakeWhile(x =>
+                x.FindElementByXPath("XCUIElementTypeWindow")?.GetAttribute("identifier") != identifier).Count();
+        }
+
+        private static AppiumWebElement FindWindow(AppiumDriver<AppiumWebElement> session, string identifier)
+        {
+            var windows = session.FindElementsByXPath("XCUIElementTypeWindow");
+            return windows.First(x => 
+                x.FindElementsByXPath("XCUIElementTypeWindow")
+                    .Any(y => y.GetAttribute("identifier") == identifier));
+        }
+
+        public enum ShowWindowMode
+        {
+            NonOwned,
+            Owned,
+            Modal
+        }
+    }
+}