diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 19ac68b15b..5151d80932 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -94,6 +94,23 @@ + + + + + + NonOwned + Owned + Modal + + + Manual + CenterScreen + CenterOwner + + + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 9a612aa94d..580548a433 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -5,6 +5,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; namespace IntegrationTestApp { @@ -46,6 +47,41 @@ namespace IntegrationTestApp } } + private void ShowWindow() + { + var sizeTextBox = this.GetControl("ShowWindowSize"); + var modeComboBox = this.GetControl("ShowWindowMode"); + var locationComboBox = this.GetControl("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 MenuClicked(object? sender, RoutedEventArgs e) { var clickedMenuItemTextBlock = this.FindControl("ClickedMenuItem"); @@ -64,6 +100,8 @@ namespace IntegrationTestApp this.FindControl("BasicListBox").SelectedIndex = -1; if (source?.Name == "MenuClickedMenuItemReset") this.FindControl("ClickedMenuItem").Text = "None"; + if (source?.Name == "ShowWindow") + ShowWindow(); } } } diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml new file mode 100644 index 0000000000..e87ae0f32c --- /dev/null +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs new file mode 100644 index 0000000000..9b55864caa --- /dev/null +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -0,0 +1,40 @@ +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("ClientSize").Text = $"{Width}, {Height}"; + this.GetControl("FrameSize").Text = $"{FrameSize}"; + this.GetControl("Position").Text = $"{Position}"; + this.GetControl("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; + this.GetControl("Scaling").Text = $"{((IRenderRoot)this).RenderScaling}"; + + if (Owner is not null) + { + var ownerRect = this.GetControl("OwnerRect"); + var owner = (Window)Owner; + ownerRect.Text = $"{owner.Position}, {owner.FrameSize}"; + } + } + + private void CloseWindow_Click(object sender, RoutedEventArgs e) => Close(); + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj index 095f0e63e0..03d9332051 100644 --- a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj +++ b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj @@ -5,6 +5,10 @@ enable + + + + diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs new file mode 100644 index 0000000000..771faa1925 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -0,0 +1,164 @@ +using System; +using System.Linq; +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 _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(string? size, ShowWindowMode mode, WindowStartupLocation location) + { + var mainWindowHandle = GetCurrentWindowHandleHack(); + + try + { + var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); + var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); + var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); + var showButton = _session.FindElementByAccessibilityId("ShowWindow"); + + if (size is not null) + sizeTextBox.SendKeys(size); + + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + + showButton.Click(); + SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); + + 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; + } + } + } + finally + { + try + { + var closeButton = _session.FindElementByAccessibilityId("CloseWindow"); + closeButton.Click(); + SwitchToMainWindowHack(mainWindowHandle); + } + catch { } + } + } + + public static TheoryData StartupLocationData() + { + var sizes = new[] { null, "400,300" }; + var data = new TheoryData(); + + foreach (var size in sizes) + { + foreach (var mode in Enum.GetValues()) + { + foreach (var location in Enum.GetValues()) + { + if (!(location == WindowStartupLocation.CenterOwner && mode == ShowWindowMode.NonOwned)) + { + data.Add(size, mode, location); + } + } + } + } + + return data; + } + + private string? GetCurrentWindowHandleHack() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // HACK: WinAppDriver only seems to switch to a newly opened window if the window has an owner, + // otherwise the session remains targeting the previous window. Return the handle for the + // current window so we know which window to switch to when another is opened. + return _session.WindowHandles.Single(); + } + + return null; + } + + private void SwitchToNewWindowHack(string? oldWindowHandle) + { + if (oldWindowHandle is not null) + { + var newWindowHandle = _session.WindowHandles.FirstOrDefault(x => x != oldWindowHandle); + + // HACK: Looks like WinAppDriver only adds window handles for non-owned windows, but luckily + // non-owned windows is where we're having the problem, so if we find a window handle that + // isn't the main window handle then switch to it. + if (newWindowHandle is not null) + _session.SwitchTo().Window(newWindowHandle); + } + } + + private void SwitchToMainWindowHack(string? mainWindowHandle) + { + if (mainWindowHandle is not null) + _session.SwitchTo().Window(mainWindowHandle); + } + + 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 + { + Assert.Equal(expected, actual); + } + } + + public enum ShowWindowMode + { + NonOwned, + Owned, + Modal + } + } +}