Browse Source

Merge pull request #8405 from AvaloniaUI/fixes/8335-more-macos-window-issues

Fix more macos window issues and add more integration tests.
pull/8416/head
Dan Walmsley 4 years ago
committed by GitHub
parent
commit
2e1f65cc71
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  2. 18
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  3. 1
      samples/IntegrationTestApp/MainWindow.axaml
  4. 17
      samples/IntegrationTestApp/MainWindow.axaml.cs
  5. 16
      samples/IntegrationTestApp/ShowWindowTest.axaml
  6. 10
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  7. 4
      src/Avalonia.Controls/Window.cs
  8. 15
      tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs
  9. 29
      tests/Avalonia.IntegrationTests.Appium/PlatformTheoryAttribute.cs
  10. 130
      tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
  11. 78
      tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
  12. 8
      tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh

2
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;

18
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);
}

1
samples/IntegrationTestApp/MainWindow.axaml

@ -112,6 +112,7 @@
<Button Name="ShowWindow">Show Window</Button>
<Button Name="SendToBack">Send to Back</Button>
<Button Name="ExitFullscreen">Exit Fullscreen</Button>
<Button Name="RestoreAll">Restore All</Button>
</StackPanel>
</TabItem>
</TabControl>

17
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<TextBlock>("ClickedMenuItem");
@ -117,6 +128,8 @@ namespace IntegrationTestApp
SendToBack();
if (source?.Name == "ExitFullscreen")
WindowState = WindowState.Normal;
if (source?.Name == "RestoreAll")
RestoreAll();
}
}
}

16
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -3,12 +3,14 @@
x:Class="IntegrationTestApp.ShowWindowTest"
Name="SecondaryWindow"
Title="Show Window Test">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,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"/>
<TextBox Name="ClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}"/>
<Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
<TextBox Name="FrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"/>
<TextBox Name="FrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
Text="{Binding FrameSize, Mode=OneWay}"/>
<Label Grid.Column="0" Grid.Row="3">Position</Label>
<TextBox Name="Position" Grid.Column="1" Grid.Row="3" IsReadOnly="True"/>
@ -21,5 +23,13 @@
<Label Grid.Column="0" Grid.Row="6">Scaling</Label>
<TextBox Name="Scaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="7">WindowState</Label>
<ComboBox Name="WindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
<ComboBoxItem>Normal</ComboBoxItem>
<ComboBoxItem>Minimized</ComboBoxItem>
<ComboBoxItem>Maximized</ComboBoxItem>
<ComboBoxItem>Fullscreen</ComboBoxItem>
</ComboBox>
</Grid>
</Window>

10
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<TextBox>("Position").Text = $"{Position}";
}
private void InitializeComponent()
@ -21,17 +24,16 @@ namespace IntegrationTestApp
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
this.GetControl<TextBox>("ClientSize").Text = $"{Width}, {Height}";
this.GetControl<TextBox>("FrameSize").Text = $"{FrameSize}";
var scaling = PlatformImpl!.DesktopScaling;
this.GetControl<TextBox>("Position").Text = $"{Position}";
this.GetControl<TextBox>("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}";
this.GetControl<TextBox>("Scaling").Text = $"{PlatformImpl?.DesktopScaling}";
this.GetControl<TextBox>("Scaling").Text = $"{scaling}";
if (Owner is not null)
{
var ownerRect = this.GetControl<TextBox>("OwnerRect");
var owner = (Window)Owner;
ownerRect.Text = $"{owner.Position}, {owner.FrameSize}";
ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}";
}
}
}

4
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;
}
}

15
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)

29
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;
}
}
}

130
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<PixelSize?, ShowWindowMode, WindowStartupLocation> StartupLocationData()
public static TheoryData<Size?, ShowWindowMode, WindowStartupLocation> StartupLocationData()
{
var sizes = new PixelSize?[] { null, new PixelSize(400, 300) };
var data = new TheoryData<PixelSize?, ShowWindowMode, WindowStartupLocation>();
var sizes = new Size?[] { null, new Size(400, 300) };
var data = new TheoryData<Size?, ShowWindowMode, WindowStartupLocation>();
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);
}
}

78
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<AppiumWebElement> 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));
}

8
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"
Loading…
Cancel
Save