Skip to content

Commit 47b34cb

Browse files
committed
UI Test improvements
1 parent 39bc3bd commit 47b34cb

9 files changed

+220
-10
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace StabilityMatrix.UITests.Attributes;
2+
3+
[AttributeUsage(AttributeTargets.Method)]
4+
public class TestPriorityAttribute : Attribute
5+
{
6+
public int Priority { get; private set; }
7+
8+
public TestPriorityAttribute(int priority)
9+
{
10+
Priority = priority;
11+
}
12+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Avalonia.Controls;
2+
3+
namespace StabilityMatrix.UITests.Extensions;
4+
5+
public static class VisualExtensions
6+
{
7+
public static Rect GetRelativeBounds(this Visual visual, TopLevel topLevel)
8+
{
9+
var origin =
10+
visual.TranslatePoint(new Point(0, 0), topLevel)
11+
?? throw new NullReferenceException("Origin is null");
12+
13+
var bounds = new Rect(origin, visual.Bounds.Size);
14+
15+
return bounds;
16+
}
17+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Avalonia.Controls;
2+
using Avalonia.Threading;
3+
using Avalonia.VisualTree;
4+
5+
namespace StabilityMatrix.UITests.Extensions;
6+
7+
/// <summary>
8+
/// Window extensions for UI tests
9+
/// </summary>
10+
public static class WindowExtensions
11+
{
12+
public static void ClickTarget(this TopLevel topLevel, Control target)
13+
{
14+
// Check target is part of the visual tree
15+
var targetVisualRoot = target.GetVisualRoot();
16+
if (targetVisualRoot is not TopLevel)
17+
{
18+
throw new ArgumentException("Target is not part of the visual tree");
19+
}
20+
if (targetVisualRoot.Equals(topLevel))
21+
{
22+
throw new ArgumentException(
23+
"Target is not part of the same visual tree as the top level"
24+
);
25+
}
26+
27+
var point =
28+
target.TranslatePoint(
29+
new Point(target.Bounds.Width / 2, target.Bounds.Height / 2),
30+
topLevel
31+
) ?? throw new NullReferenceException("Point is null");
32+
33+
topLevel.MouseMove(point);
34+
topLevel.MouseDown(point, MouseButton.Left);
35+
topLevel.MouseUp(point, MouseButton.Left);
36+
37+
// Return mouse to outside of window
38+
topLevel.MouseMove(new Point(-50, -50));
39+
}
40+
41+
public static async Task ClickTargetAsync(this TopLevel topLevel, Control target)
42+
{
43+
// Check target is part of the visual tree
44+
var targetVisualRoot = target.GetVisualRoot();
45+
if (targetVisualRoot is not TopLevel)
46+
{
47+
throw new ArgumentException("Target is not part of the visual tree");
48+
}
49+
if (!targetVisualRoot.Equals(topLevel))
50+
{
51+
throw new ArgumentException(
52+
"Target is not part of the same visual tree as the top level"
53+
);
54+
}
55+
56+
var point =
57+
target.TranslatePoint(
58+
new Point(target.Bounds.Width / 2, target.Bounds.Height / 2),
59+
topLevel
60+
) ?? throw new NullReferenceException("Point is null");
61+
62+
topLevel.MouseMove(point);
63+
topLevel.MouseDown(point, MouseButton.Left);
64+
topLevel.MouseUp(point, MouseButton.Left);
65+
66+
await Task.Delay(40);
67+
68+
// Return mouse to outside of window
69+
topLevel.MouseMove(new Point(-50, -50));
70+
71+
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.RunJobs());
72+
}
73+
}

StabilityMatrix.UITests/MainWindowTests.cs

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Avalonia.Controls;
22
using Avalonia.Controls.Primitives;
3+
using Avalonia.Threading;
34
using Avalonia.VisualTree;
45
using FluentAvalonia.UI.Controls;
56
using FluentAvalonia.UI.Windowing;
@@ -9,15 +10,19 @@
910
using StabilityMatrix.Avalonia.ViewModels;
1011
using StabilityMatrix.Avalonia.Views;
1112
using StabilityMatrix.Avalonia.Views.Dialogs;
13+
using StabilityMatrix.UITests.Extensions;
1214

1315
namespace StabilityMatrix.UITests;
1416

1517
[UsesVerify]
1618
[Collection("TempDir")]
19+
[TestCaseOrderer("StabilityMatrix.UITests.PriorityOrderer", "StabilityMatrix.UITests")]
1720
public class MainWindowTests
1821
{
1922
private static IServiceProvider Services => App.Services;
2023

24+
private static (AppWindow, MainWindowViewModel)? currentMainWindow;
25+
2126
private static VerifySettings Settings
2227
{
2328
get
@@ -28,23 +33,32 @@ private static VerifySettings Settings
2833
vm => vm.FooterPages,
2934
vm => vm.CurrentPage
3035
);
36+
settings.DisableDiff();
3137
return settings;
3238
}
3339
}
3440

3541
private static (AppWindow, MainWindowViewModel) GetMainWindow()
3642
{
43+
if (currentMainWindow is not null)
44+
{
45+
return currentMainWindow.Value;
46+
}
47+
3748
var window = Services.GetRequiredService<MainWindow>();
3849
var viewModel = Services.GetRequiredService<MainWindowViewModel>();
3950
window.DataContext = viewModel;
4051

4152
window.SetDefaultFonts();
53+
window.Width = 1400;
54+
window.Height = 900;
4255

4356
App.VisualRoot = window;
4457
App.StorageProvider = window.StorageProvider;
4558
App.Clipboard = window.Clipboard ?? throw new NullReferenceException("Clipboard is null");
4659

47-
return (window, viewModel);
60+
currentMainWindow = (window, viewModel);
61+
return currentMainWindow.Value;
4862
}
4963

5064
private static BetterContentDialog? GetWindowDialog(Visual window)
@@ -58,22 +72,46 @@ private static (AppWindow, MainWindowViewModel) GetMainWindow()
5872
?.FindDescendantOfType<BetterContentDialog>();
5973
}
6074

61-
[AvaloniaFact]
62-
public Task MainWindowViewModel_ShouldOk()
75+
private static IEnumerable<BetterContentDialog> EnumerateWindowDialogs(Visual window)
6376
{
64-
var viewModel = Services.GetRequiredService<MainWindowViewModel>();
77+
return window
78+
.FindDescendantOfType<VisualLayerManager>()
79+
?.FindDescendantOfType<OverlayLayer>()
80+
?.FindDescendantOfType<DialogHost>()
81+
?.FindDescendantOfType<LayoutTransformControl>()
82+
?.FindDescendantOfType<VisualLayerManager>()
83+
?.GetVisualDescendants()
84+
.OfType<BetterContentDialog>() ?? Enumerable.Empty<BetterContentDialog>();
85+
}
6586

66-
return Verify(viewModel, Settings);
87+
private async Task<(BetterContentDialog, T)> WaitForDialog<T>(Visual window)
88+
where T : Control
89+
{
90+
var dialogs = await WaitHelper.WaitForConditionAsync(
91+
() => EnumerateWindowDialogs(window).ToList(),
92+
list => list.Any(dialog => dialog.Content is T)
93+
);
94+
95+
if (dialogs.Count == 0)
96+
{
97+
throw new InvalidOperationException("No dialogs found");
98+
}
99+
100+
var contentDialog = dialogs.First(dialog => dialog.Content is T);
101+
102+
return (contentDialog, contentDialog.Content as T)!;
67103
}
68104

69-
[AvaloniaFact]
105+
[AvaloniaFact, TestPriority(1)]
70106
public async Task MainWindow_ShouldOpen()
71107
{
72-
var (window, vm) = GetMainWindow();
108+
var (window, _) = GetMainWindow();
73109

74110
window.Show();
75111

76-
await Task.Delay(800);
112+
await Task.Delay(300);
113+
114+
Dispatcher.UIThread.RunJobs();
77115

78116
// Find the select data directory dialog
79117
var selectDataDirectoryDialog = await WaitHelper.WaitForNotNullAsync(
@@ -86,7 +124,8 @@ public async Task MainWindow_ShouldOpen()
86124
.GetVisualDescendants()
87125
.OfType<Button>()
88126
.First(b => b.Content as string == "Continue");
89-
continueButton.Command?.Execute(null);
127+
128+
await window.ClickTargetAsync(continueButton);
90129

91130
// Find the one click install dialog
92131
var oneClickDialog = await WaitHelper.WaitForConditionAsync(
@@ -95,8 +134,16 @@ public async Task MainWindow_ShouldOpen()
95134
);
96135
Assert.NotNull(oneClickDialog);
97136

98-
await Task.Delay(1000);
137+
await Task.Delay(1800);
99138

100139
await Verify(window, Settings);
101140
}
141+
142+
[AvaloniaFact, TestPriority(2)]
143+
public async Task MainWindowViewModel_ShouldOk()
144+
{
145+
var viewModel = Services.GetRequiredService<MainWindowViewModel>();
146+
147+
await Verify(viewModel, Settings);
148+
}
102149
}

StabilityMatrix.UITests/ModuleInit.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Runtime.CompilerServices;
22

3+
[assembly: CollectionBehavior(DisableTestParallelization = true)]
4+
35
namespace StabilityMatrix.UITests;
46

57
public static class ModuleInit
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using StabilityMatrix.UITests.Attributes;
2+
using Xunit.Abstractions;
3+
using Xunit.Sdk;
4+
5+
namespace StabilityMatrix.UITests;
6+
7+
public class PriorityOrderer : ITestCaseOrderer
8+
{
9+
public IEnumerable<TTestCase> OrderTestCases<TTestCase>(IEnumerable<TTestCase> testCases)
10+
where TTestCase : ITestCase
11+
{
12+
var sortedMethods = new SortedDictionary<int, List<TTestCase>>();
13+
14+
foreach (var testCase in testCases)
15+
{
16+
var priority = 0;
17+
18+
foreach (
19+
var attr in testCase.TestMethod.Method.GetCustomAttributes(
20+
typeof(TestPriorityAttribute).AssemblyQualifiedName
21+
)
22+
)
23+
{
24+
priority = attr.GetNamedArgument<int>("Priority");
25+
}
26+
27+
GetOrCreate(sortedMethods, priority).Add(testCase);
28+
}
29+
30+
foreach (var list in sortedMethods.Keys.Select(priority => sortedMethods[priority]))
31+
{
32+
list.Sort(
33+
(x, y) =>
34+
StringComparer.OrdinalIgnoreCase.Compare(
35+
x.TestMethod.Method.Name,
36+
y.TestMethod.Method.Name
37+
)
38+
);
39+
foreach (var testCase in list)
40+
{
41+
yield return testCase;
42+
}
43+
}
44+
}
45+
46+
private static TValue GetOrCreate<TKey, TValue>(IDictionary<TKey, TValue> dictionary, TKey key)
47+
where TValue : new()
48+
{
49+
if (dictionary.TryGetValue(key, out var result))
50+
return result;
51+
52+
result = new TValue();
53+
dictionary[key] = result;
54+
55+
return result;
56+
}
57+
}
Loading

StabilityMatrix.UITests/Usings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
global using Avalonia.Headless;
44
global using Avalonia.Headless.XUnit;
55
global using Avalonia.Input;
6+
global using StabilityMatrix.UITests.Attributes;

StabilityMatrix.UITests/VerifyConfig.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ static VerifyConfig()
1010
{
1111
Default = new VerifySettings();
1212
Default.IgnoreMembersWithType<WeakEventManager>();
13+
Default.DisableDiff();
1314
}
1415
}

0 commit comments

Comments
 (0)