diff --git a/StabilityMatrix.UITests/Attributes/TestPriorityAttribute.cs b/StabilityMatrix.UITests/Attributes/TestPriorityAttribute.cs new file mode 100644 index 00000000..5736d2d1 --- /dev/null +++ b/StabilityMatrix.UITests/Attributes/TestPriorityAttribute.cs @@ -0,0 +1,12 @@ +namespace StabilityMatrix.UITests.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +public class TestPriorityAttribute : Attribute +{ + public int Priority { get; private set; } + + public TestPriorityAttribute(int priority) + { + Priority = priority; + } +} diff --git a/StabilityMatrix.UITests/Extensions/VisualExtensions.cs b/StabilityMatrix.UITests/Extensions/VisualExtensions.cs new file mode 100644 index 00000000..dc57d373 --- /dev/null +++ b/StabilityMatrix.UITests/Extensions/VisualExtensions.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; + +namespace StabilityMatrix.UITests.Extensions; + +public static class VisualExtensions +{ + public static Rect GetRelativeBounds(this Visual visual, TopLevel topLevel) + { + var origin = + visual.TranslatePoint(new Point(0, 0), topLevel) + ?? throw new NullReferenceException("Origin is null"); + + var bounds = new Rect(origin, visual.Bounds.Size); + + return bounds; + } +} diff --git a/StabilityMatrix.UITests/Extensions/WindowExtensions.cs b/StabilityMatrix.UITests/Extensions/WindowExtensions.cs new file mode 100644 index 00000000..922ea147 --- /dev/null +++ b/StabilityMatrix.UITests/Extensions/WindowExtensions.cs @@ -0,0 +1,73 @@ +using Avalonia.Controls; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace StabilityMatrix.UITests.Extensions; + +/// +/// Window extensions for UI tests +/// +public static class WindowExtensions +{ + public static void ClickTarget(this TopLevel topLevel, Control target) + { + // Check target is part of the visual tree + var targetVisualRoot = target.GetVisualRoot(); + if (targetVisualRoot is not TopLevel) + { + throw new ArgumentException("Target is not part of the visual tree"); + } + if (targetVisualRoot.Equals(topLevel)) + { + throw new ArgumentException( + "Target is not part of the same visual tree as the top level" + ); + } + + var point = + target.TranslatePoint( + new Point(target.Bounds.Width / 2, target.Bounds.Height / 2), + topLevel + ) ?? throw new NullReferenceException("Point is null"); + + topLevel.MouseMove(point); + topLevel.MouseDown(point, MouseButton.Left); + topLevel.MouseUp(point, MouseButton.Left); + + // Return mouse to outside of window + topLevel.MouseMove(new Point(-50, -50)); + } + + public static async Task ClickTargetAsync(this TopLevel topLevel, Control target) + { + // Check target is part of the visual tree + var targetVisualRoot = target.GetVisualRoot(); + if (targetVisualRoot is not TopLevel) + { + throw new ArgumentException("Target is not part of the visual tree"); + } + if (!targetVisualRoot.Equals(topLevel)) + { + throw new ArgumentException( + "Target is not part of the same visual tree as the top level" + ); + } + + var point = + target.TranslatePoint( + new Point(target.Bounds.Width / 2, target.Bounds.Height / 2), + topLevel + ) ?? throw new NullReferenceException("Point is null"); + + topLevel.MouseMove(point); + topLevel.MouseDown(point, MouseButton.Left); + topLevel.MouseUp(point, MouseButton.Left); + + await Task.Delay(40); + + // Return mouse to outside of window + topLevel.MouseMove(new Point(-50, -50)); + + Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.RunJobs()); + } +} diff --git a/StabilityMatrix.UITests/MainWindowTests.cs b/StabilityMatrix.UITests/MainWindowTests.cs index 50614ed0..4c890b7a 100644 --- a/StabilityMatrix.UITests/MainWindowTests.cs +++ b/StabilityMatrix.UITests/MainWindowTests.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Threading; using Avalonia.VisualTree; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Windowing; @@ -9,15 +10,19 @@ using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views.Dialogs; +using StabilityMatrix.UITests.Extensions; namespace StabilityMatrix.UITests; [UsesVerify] [Collection("TempDir")] +[TestCaseOrderer("StabilityMatrix.UITests.PriorityOrderer", "StabilityMatrix.UITests")] public class MainWindowTests { private static IServiceProvider Services => App.Services; + private static (AppWindow, MainWindowViewModel)? currentMainWindow; + private static VerifySettings Settings { get @@ -28,23 +33,32 @@ public class MainWindowTests vm => vm.FooterPages, vm => vm.CurrentPage ); + settings.DisableDiff(); return settings; } } private static (AppWindow, MainWindowViewModel) GetMainWindow() { + if (currentMainWindow is not null) + { + return currentMainWindow.Value; + } + var window = Services.GetRequiredService(); var viewModel = Services.GetRequiredService(); window.DataContext = viewModel; window.SetDefaultFonts(); + window.Width = 1400; + window.Height = 900; App.VisualRoot = window; App.StorageProvider = window.StorageProvider; App.Clipboard = window.Clipboard ?? throw new NullReferenceException("Clipboard is null"); - return (window, viewModel); + currentMainWindow = (window, viewModel); + return currentMainWindow.Value; } private static BetterContentDialog? GetWindowDialog(Visual window) @@ -58,22 +72,46 @@ public class MainWindowTests ?.FindDescendantOfType(); } - [AvaloniaFact] - public Task MainWindowViewModel_ShouldOk() + private static IEnumerable EnumerateWindowDialogs(Visual window) { - var viewModel = Services.GetRequiredService(); + return window + .FindDescendantOfType() + ?.FindDescendantOfType() + ?.FindDescendantOfType() + ?.FindDescendantOfType() + ?.FindDescendantOfType() + ?.GetVisualDescendants() + .OfType() ?? Enumerable.Empty(); + } - return Verify(viewModel, Settings); + private async Task<(BetterContentDialog, T)> WaitForDialog(Visual window) + where T : Control + { + var dialogs = await WaitHelper.WaitForConditionAsync( + () => EnumerateWindowDialogs(window).ToList(), + list => list.Any(dialog => dialog.Content is T) + ); + + if (dialogs.Count == 0) + { + throw new InvalidOperationException("No dialogs found"); + } + + var contentDialog = dialogs.First(dialog => dialog.Content is T); + + return (contentDialog, contentDialog.Content as T)!; } - [AvaloniaFact] + [AvaloniaFact, TestPriority(1)] public async Task MainWindow_ShouldOpen() { - var (window, vm) = GetMainWindow(); + var (window, _) = GetMainWindow(); window.Show(); - await Task.Delay(800); + await Task.Delay(300); + + Dispatcher.UIThread.RunJobs(); // Find the select data directory dialog var selectDataDirectoryDialog = await WaitHelper.WaitForNotNullAsync( @@ -86,7 +124,8 @@ public class MainWindowTests .GetVisualDescendants() .OfType