Ionite
1 year ago
committed by
GitHub
25 changed files with 1194 additions and 48 deletions
@ -0,0 +1,27 @@
|
||||
name: UI Tests |
||||
|
||||
on: |
||||
workflow_dispatch: |
||||
|
||||
concurrency: |
||||
group: build-${{ github.event.pull_request.number || github.ref }} |
||||
cancel-in-progress: true |
||||
|
||||
jobs: |
||||
build: |
||||
if: github.repository == 'LykosAI/StabilityMatrix' || github.event_name == 'workflow_dispatch' |
||||
runs-on: windows-latest |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
|
||||
- name: Set up .NET |
||||
uses: actions/setup-dotnet@v3 |
||||
with: |
||||
dotnet-version: '7.0.x' |
||||
|
||||
- name: Install dependencies |
||||
run: dotnet restore |
||||
|
||||
- name: Test |
||||
run: dotnet test StabilityMatrix.UITests |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -0,0 +1,73 @@
|
||||
using Avalonia.Controls; |
||||
using Avalonia.Threading; |
||||
using Avalonia.VisualTree; |
||||
|
||||
namespace StabilityMatrix.UITests.Extensions; |
||||
|
||||
/// <summary> |
||||
/// Window extensions for UI tests |
||||
/// </summary> |
||||
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()); |
||||
} |
||||
} |
@ -0,0 +1,149 @@
|
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Primitives; |
||||
using Avalonia.Threading; |
||||
using Avalonia.VisualTree; |
||||
using FluentAvalonia.UI.Controls; |
||||
using FluentAvalonia.UI.Windowing; |
||||
using Microsoft.Extensions.DependencyInjection; |
||||
using StabilityMatrix.Avalonia; |
||||
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 |
||||
{ |
||||
var settings = new VerifySettings(); |
||||
settings.IgnoreMembers<MainWindowViewModel>( |
||||
vm => vm.Pages, |
||||
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<MainWindow>(); |
||||
var viewModel = Services.GetRequiredService<MainWindowViewModel>(); |
||||
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"); |
||||
|
||||
currentMainWindow = (window, viewModel); |
||||
return currentMainWindow.Value; |
||||
} |
||||
|
||||
private static BetterContentDialog? GetWindowDialog(Visual window) |
||||
{ |
||||
return window |
||||
.FindDescendantOfType<VisualLayerManager>() |
||||
?.FindDescendantOfType<OverlayLayer>() |
||||
?.FindDescendantOfType<DialogHost>() |
||||
?.FindDescendantOfType<LayoutTransformControl>() |
||||
?.FindDescendantOfType<VisualLayerManager>() |
||||
?.FindDescendantOfType<BetterContentDialog>(); |
||||
} |
||||
|
||||
private static IEnumerable<BetterContentDialog> EnumerateWindowDialogs(Visual window) |
||||
{ |
||||
return window |
||||
.FindDescendantOfType<VisualLayerManager>() |
||||
?.FindDescendantOfType<OverlayLayer>() |
||||
?.FindDescendantOfType<DialogHost>() |
||||
?.FindDescendantOfType<LayoutTransformControl>() |
||||
?.FindDescendantOfType<VisualLayerManager>() |
||||
?.GetVisualDescendants() |
||||
.OfType<BetterContentDialog>() ?? Enumerable.Empty<BetterContentDialog>(); |
||||
} |
||||
|
||||
private async Task<(BetterContentDialog, T)> WaitForDialog<T>(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, TestPriority(1)] |
||||
public async Task MainWindow_ShouldOpen() |
||||
{ |
||||
var (window, _) = GetMainWindow(); |
||||
|
||||
window.Show(); |
||||
|
||||
await Task.Delay(300); |
||||
|
||||
Dispatcher.UIThread.RunJobs(); |
||||
|
||||
// Find the select data directory dialog |
||||
var selectDataDirectoryDialog = await WaitHelper.WaitForNotNullAsync( |
||||
() => GetWindowDialog(window) |
||||
); |
||||
Assert.NotNull(selectDataDirectoryDialog); |
||||
|
||||
// Click continue button |
||||
var continueButton = selectDataDirectoryDialog |
||||
.GetVisualDescendants() |
||||
.OfType<Button>() |
||||
.First(b => b.Content as string == "Continue"); |
||||
|
||||
await window.ClickTargetAsync(continueButton); |
||||
|
||||
// Find the one click install dialog |
||||
var oneClickDialog = await WaitHelper.WaitForConditionAsync( |
||||
() => GetWindowDialog(window), |
||||
d => d?.Content is OneClickInstallDialog |
||||
); |
||||
Assert.NotNull(oneClickDialog); |
||||
|
||||
await Task.Delay(1800); |
||||
|
||||
await Verify(window, Settings); |
||||
} |
||||
|
||||
[AvaloniaFact, TestPriority(2)] |
||||
public async Task MainWindowViewModel_ShouldOk() |
||||
{ |
||||
var viewModel = Services.GetRequiredService<MainWindowViewModel>(); |
||||
|
||||
await Verify(viewModel, Settings); |
||||
} |
||||
} |
@ -0,0 +1,29 @@
|
||||
using System.Runtime.CompilerServices; |
||||
|
||||
[assembly: CollectionBehavior(DisableTestParallelization = true)] |
||||
|
||||
namespace StabilityMatrix.UITests; |
||||
|
||||
public static class ModuleInit |
||||
{ |
||||
[ModuleInitializer] |
||||
public static void Init() => VerifyAvalonia.Initialize(); |
||||
|
||||
[ModuleInitializer] |
||||
public static void InitOther() => VerifierSettings.InitializePlugins(); |
||||
|
||||
[ModuleInitializer] |
||||
public static void ConfigureVerify() |
||||
{ |
||||
VerifyPhash.RegisterComparer("png"); |
||||
|
||||
DerivePathInfo( |
||||
(sourceFile, projectDirectory, type, method) => |
||||
new PathInfo( |
||||
directory: Path.Combine(projectDirectory, "Snapshots"), |
||||
typeName: type.Name, |
||||
methodName: method.Name |
||||
) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,57 @@
|
||||
using StabilityMatrix.UITests.Attributes; |
||||
using Xunit.Abstractions; |
||||
using Xunit.Sdk; |
||||
|
||||
namespace StabilityMatrix.UITests; |
||||
|
||||
public class PriorityOrderer : ITestCaseOrderer |
||||
{ |
||||
public IEnumerable<TTestCase> OrderTestCases<TTestCase>(IEnumerable<TTestCase> testCases) |
||||
where TTestCase : ITestCase |
||||
{ |
||||
var sortedMethods = new SortedDictionary<int, List<TTestCase>>(); |
||||
|
||||
foreach (var testCase in testCases) |
||||
{ |
||||
var priority = 0; |
||||
|
||||
foreach ( |
||||
var attr in testCase.TestMethod.Method.GetCustomAttributes( |
||||
typeof(TestPriorityAttribute).AssemblyQualifiedName |
||||
) |
||||
) |
||||
{ |
||||
priority = attr.GetNamedArgument<int>("Priority"); |
||||
} |
||||
|
||||
GetOrCreate(sortedMethods, priority).Add(testCase); |
||||
} |
||||
|
||||
foreach (var list in sortedMethods.Keys.Select(priority => sortedMethods[priority])) |
||||
{ |
||||
list.Sort( |
||||
(x, y) => |
||||
StringComparer.OrdinalIgnoreCase.Compare( |
||||
x.TestMethod.Method.Name, |
||||
y.TestMethod.Method.Name |
||||
) |
||||
); |
||||
foreach (var testCase in list) |
||||
{ |
||||
yield return testCase; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static TValue GetOrCreate<TKey, TValue>(IDictionary<TKey, TValue> dictionary, TKey key) |
||||
where TValue : new() |
||||
{ |
||||
if (dictionary.TryGetValue(key, out var result)) |
||||
return result; |
||||
|
||||
result = new TValue(); |
||||
dictionary[key] = result; |
||||
|
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,39 @@
|
||||
{ |
||||
Greeting: Welcome to Avalonia!, |
||||
ProgressManagerViewModel: { |
||||
Title: Download Manager, |
||||
IconSource: { |
||||
Type: SymbolIconSource |
||||
}, |
||||
IsOpen: false, |
||||
CanNavigateNext: false, |
||||
CanNavigatePrevious: false, |
||||
HasErrors: false |
||||
}, |
||||
UpdateViewModel: { |
||||
IsUpdateAvailable: true, |
||||
UpdateInfo: { |
||||
Version: { |
||||
Major: 2, |
||||
Minor: 999, |
||||
Prerelease: , |
||||
IsPrerelease: false, |
||||
IsRelease: true, |
||||
Metadata: |
||||
}, |
||||
ReleaseDate: DateTimeOffset_1, |
||||
Channel: Stable, |
||||
Type: Normal, |
||||
DownloadUrl: https://example.org, |
||||
ChangelogUrl: https://example.org, |
||||
HashBlake3: 46e11a5216c55d4c9d3c54385f62f3e1022537ae191615237f05e06d6f8690d0, |
||||
Signature: IX5/CCXWJQG0oGkYWVnuF34gTqF/dJSrDrUd6fuNMYnncL39G3HSvkXrjvJvR18MA2rQNB5z13h3/qBSf9c7DA== |
||||
}, |
||||
ShowProgressBar: false, |
||||
CurrentVersionText: v1.0.0, |
||||
NewVersionText: v2.999.0, |
||||
InstallUpdateCommand: UpdateViewModel.InstallUpdate(), |
||||
HasErrors: false |
||||
}, |
||||
HasErrors: false |
||||
} |
After Width: | Height: | Size: 202 KiB |
@ -0,0 +1,451 @@
|
||||
{ |
||||
Type: MainWindow, |
||||
Title: Stability Matrix, |
||||
Icon: {}, |
||||
TransparencyLevelHint: [ |
||||
{}, |
||||
{}, |
||||
{} |
||||
], |
||||
TransparencyBackgroundFallback: Transparent, |
||||
Content: { |
||||
Type: Grid, |
||||
Children: [ |
||||
{ |
||||
Type: Grid, |
||||
Background: Transparent, |
||||
Height: 32.0, |
||||
Name: TitleBarHost, |
||||
Children: [ |
||||
{ |
||||
Type: Image, |
||||
Source: { |
||||
Dpi: { |
||||
X: 96.0, |
||||
Y: 96.0, |
||||
Length: 135.7645019878171, |
||||
SquaredLength: 18432.0 |
||||
}, |
||||
Size: { |
||||
AspectRatio: 1.0, |
||||
Width: 256.0, |
||||
Height: 256.0 |
||||
}, |
||||
PixelSize: { |
||||
AspectRatio: 1.0, |
||||
Width: 256, |
||||
Height: 256 |
||||
}, |
||||
Format: { |
||||
BitsPerPixel: 32 |
||||
} |
||||
}, |
||||
IsHitTestVisible: false, |
||||
Width: 18.0, |
||||
Height: 18.0, |
||||
Margin: 12,4,12,4, |
||||
IsVisible: true, |
||||
Name: WindowIcon |
||||
}, |
||||
{ |
||||
Type: TextBlock, |
||||
FontSize: 12.0, |
||||
Text: Stability Matrix, |
||||
IsHitTestVisible: false, |
||||
VerticalAlignment: Center, |
||||
IsVisible: true |
||||
}, |
||||
{ |
||||
Type: Border, |
||||
Padding: 6 |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
Type: NavigationView, |
||||
Content: { |
||||
Type: Frame, |
||||
Content: { |
||||
Type: LaunchPageView, |
||||
Content: { |
||||
Type: Grid, |
||||
Children: [ |
||||
{ |
||||
Type: Grid, |
||||
Margin: 0,8,0,8, |
||||
Children: [ |
||||
{ |
||||
Type: Grid, |
||||
Margin: 16,8,0,0, |
||||
Children: [ |
||||
{ |
||||
Type: Grid, |
||||
Column: 0, |
||||
Row: 0, |
||||
Name: LaunchButtonGrid, |
||||
Children: [ |
||||
{ |
||||
Type: Button, |
||||
Command: LaunchPageViewModel.LaunchAsync(string command), |
||||
Content: Launch, |
||||
Width: 95.0, |
||||
HorizontalAlignment: Left, |
||||
VerticalAlignment: Stretch, |
||||
IsVisible: false |
||||
}, |
||||
{ |
||||
Type: SplitButton, |
||||
Command: LaunchPageViewModel.LaunchAsync(string command), |
||||
Flyout: { |
||||
Type: FAMenuFlyout |
||||
}, |
||||
Content: Launch, |
||||
Width: 104.0, |
||||
HorizontalAlignment: Left, |
||||
VerticalAlignment: Stretch, |
||||
IsVisible: false |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
Type: TeachingTip, |
||||
Name: TeachingTip1 |
||||
}, |
||||
{ |
||||
Type: Grid, |
||||
Column: 0, |
||||
Row: 0, |
||||
IsVisible: false, |
||||
Name: StopButtonGrid, |
||||
Children: [ |
||||
{ |
||||
Type: Button, |
||||
Command: {}, |
||||
Content: Stop, |
||||
Width: 95.0, |
||||
HorizontalAlignment: Left, |
||||
VerticalAlignment: Stretch, |
||||
IsVisible: false |
||||
}, |
||||
{ |
||||
Type: Button, |
||||
Command: {}, |
||||
Content: Stop, |
||||
Width: 104.0, |
||||
HorizontalAlignment: Left, |
||||
VerticalAlignment: Stretch, |
||||
IsVisible: false |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
Type: Button, |
||||
Command: LaunchPageViewModel.Config(), |
||||
Content: { |
||||
Type: SymbolIcon |
||||
}, |
||||
FontSize: 16.0, |
||||
Width: 48.0, |
||||
Margin: 8,0,0,0, |
||||
HorizontalAlignment: Left, |
||||
VerticalAlignment: Stretch |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
Type: ComboBox, |
||||
SelectedIndex: -1, |
||||
Selection: { |
||||
SingleSelect: true, |
||||
SelectedIndex: -1, |
||||
AnchorIndex: -1 |
||||
}, |
||||
SelectionMode: Single, |
||||
ItemTemplate: { |
||||
DataType: InstalledPackage, |
||||
Content: { |
||||
Type: Func<IServiceProvider, object>, |
||||
Target: XamlIlRuntimeHelpers.<>c__DisplayClass1_0<Control>, |
||||
Method: System.Object DeferredTransformationFactoryV2(System.IServiceProvider) |
||||
} |
||||
}, |
||||
IsEnabled: true, |
||||
Margin: 8,8,0,0, |
||||
HorizontalAlignment: Stretch, |
||||
VerticalAlignment: Top, |
||||
Name: SelectPackageComboBox |
||||
}, |
||||
{ |
||||
Type: ToggleButton, |
||||
IsChecked: true, |
||||
Content: { |
||||
Type: Icon, |
||||
Template: { |
||||
Content: { |
||||
Type: Func<IServiceProvider, object>, |
||||
Target: XamlIlRuntimeHelpers.<>c__DisplayClass1_0<Control>, |
||||
Method: System.Object DeferredTransformationFactoryV2(System.IServiceProvider) |
||||
} |
||||
}, |
||||
RenderTransform: { |
||||
Type: TransformGroup, |
||||
Children: [ |
||||
{ |
||||
Type: RotateTransform |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
FontSize: 16.0, |
||||
Width: 48.0, |
||||
Margin: 8,8,0,0, |
||||
HorizontalAlignment: Left, |
||||
VerticalAlignment: Stretch |
||||
}, |
||||
{ |
||||
Type: ToggleButton, |
||||
IsChecked: false, |
||||
Content: { |
||||
Type: SymbolIcon |
||||
}, |
||||
FontSize: 16.0, |
||||
Width: 48.0, |
||||
Margin: 8,8,16,0, |
||||
HorizontalAlignment: Left, |
||||
VerticalAlignment: Stretch |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
Type: TextEditor, |
||||
FontFamily: Cascadia Code, |
||||
Margin: 8,8,16,10, |
||||
DataContext: { |
||||
IsUpdatesRunning: false, |
||||
WriteCursorLockTimeout: 00:00:00.1000000, |
||||
Document: { |
||||
_undoStack: { |
||||
IsOriginalFile: true, |
||||
AcceptChanges: true, |
||||
CanUndo: false, |
||||
CanRedo: false, |
||||
SizeLimit: 2147483647 |
||||
}, |
||||
Text: , |
||||
Version: {}, |
||||
IsInUpdate: false, |
||||
Lines: [ |
||||
{ |
||||
IsDeleted: false, |
||||
LineNumber: 1 |
||||
} |
||||
], |
||||
LineTrackers: [ |
||||
{} |
||||
], |
||||
UndoStack: { |
||||
IsOriginalFile: true, |
||||
AcceptChanges: true, |
||||
CanUndo: false, |
||||
CanRedo: false, |
||||
SizeLimit: 2147483647 |
||||
}, |
||||
LineCount: 1 |
||||
} |
||||
}, |
||||
Name: Console |
||||
}, |
||||
{ |
||||
Type: Grid, |
||||
Row: 1, |
||||
Children: [ |
||||
{ |
||||
Type: StackPanel, |
||||
Spacing: 4.0, |
||||
Margin: 8, |
||||
Children: [ |
||||
{ |
||||
Type: InfoBar, |
||||
Margin: 0 |
||||
}, |
||||
{ |
||||
Type: InfoBar, |
||||
Margin: 0 |
||||
} |
||||
] |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
Type: Button, |
||||
Command: {}, |
||||
Content: Open Web UI, |
||||
FontSize: 12.0, |
||||
Margin: 24,0,24,8, |
||||
HorizontalAlignment: Stretch, |
||||
IsVisible: false |
||||
} |
||||
] |
||||
}, |
||||
DataContext: { |
||||
Title: Launch, |
||||
IconSource: { |
||||
Type: SymbolIconSource |
||||
}, |
||||
Console: { |
||||
IsUpdatesRunning: false, |
||||
WriteCursorLockTimeout: 00:00:00.1000000, |
||||
Document: { |
||||
_undoStack: { |
||||
IsOriginalFile: true, |
||||
AcceptChanges: true, |
||||
CanUndo: false, |
||||
CanRedo: false, |
||||
SizeLimit: 2147483647 |
||||
}, |
||||
Text: , |
||||
Version: {}, |
||||
IsInUpdate: false, |
||||
Lines: [ |
||||
{ |
||||
IsDeleted: false, |
||||
LineNumber: 1 |
||||
} |
||||
], |
||||
LineTrackers: [ |
||||
{} |
||||
], |
||||
UndoStack: { |
||||
IsOriginalFile: true, |
||||
AcceptChanges: true, |
||||
CanUndo: false, |
||||
CanRedo: false, |
||||
SizeLimit: 2147483647 |
||||
}, |
||||
LineCount: 1 |
||||
} |
||||
}, |
||||
LaunchButtonVisibility: false, |
||||
StopButtonVisibility: false, |
||||
IsLaunchTeachingTipsOpen: false, |
||||
ShowWebUiButton: false, |
||||
AutoScrollToEnd: true, |
||||
ShowManualInputPrompt: false, |
||||
ShowConfirmInputPrompt: false, |
||||
LaunchCommand: LaunchPageViewModel.LaunchAsync(string command), |
||||
ConfigCommand: LaunchPageViewModel.Config(), |
||||
SendConfirmInputCommand: LaunchPageViewModel.SendConfirmInput(bool value), |
||||
SendManualInputCommand: LaunchPageViewModel.SendManualInput(string input), |
||||
CanNavigateNext: false, |
||||
CanNavigatePrevious: false, |
||||
HasErrors: false |
||||
} |
||||
}, |
||||
Name: FrameView |
||||
}, |
||||
Name: NavigationView |
||||
}, |
||||
{ |
||||
Type: TeachingTip, |
||||
Name: UpdateAvailableTeachingTip |
||||
} |
||||
] |
||||
}, |
||||
Background: #ff101010, |
||||
FontFamily: Segoe UI Variable Text, |
||||
Width: 1400.0, |
||||
Height: 900.0, |
||||
IsVisible: true, |
||||
DataContext: { |
||||
Greeting: Welcome to Avalonia!, |
||||
ProgressManagerViewModel: { |
||||
Title: Download Manager, |
||||
IconSource: { |
||||
Type: SymbolIconSource |
||||
}, |
||||
IsOpen: false, |
||||
CanNavigateNext: false, |
||||
CanNavigatePrevious: false, |
||||
HasErrors: false |
||||
}, |
||||
UpdateViewModel: { |
||||
IsUpdateAvailable: true, |
||||
UpdateInfo: { |
||||
Version: { |
||||
Major: 2, |
||||
Minor: 999, |
||||
Prerelease: , |
||||
IsPrerelease: false, |
||||
IsRelease: true, |
||||
Metadata: |
||||
}, |
||||
ReleaseDate: DateTimeOffset_1, |
||||
Channel: Stable, |
||||
Type: Normal, |
||||
DownloadUrl: https://example.org, |
||||
ChangelogUrl: https://example.org, |
||||
HashBlake3: 46e11a5216c55d4c9d3c54385f62f3e1022537ae191615237f05e06d6f8690d0, |
||||
Signature: IX5/CCXWJQG0oGkYWVnuF34gTqF/dJSrDrUd6fuNMYnncL39G3HSvkXrjvJvR18MA2rQNB5z13h3/qBSf9c7DA== |
||||
}, |
||||
ShowProgressBar: false, |
||||
CurrentVersionText: v1.0.0, |
||||
NewVersionText: v2.999.0, |
||||
InstallUpdateCommand: UpdateViewModel.InstallUpdate(), |
||||
HasErrors: false |
||||
}, |
||||
SelectedCategory: { |
||||
Title: Launch, |
||||
IconSource: { |
||||
Type: SymbolIconSource |
||||
}, |
||||
Console: { |
||||
IsUpdatesRunning: false, |
||||
WriteCursorLockTimeout: 00:00:00.1000000, |
||||
Document: { |
||||
_undoStack: { |
||||
IsOriginalFile: true, |
||||
AcceptChanges: true, |
||||
CanUndo: false, |
||||
CanRedo: false, |
||||
SizeLimit: 2147483647 |
||||
}, |
||||
Text: , |
||||
Version: {}, |
||||
IsInUpdate: false, |
||||
Lines: [ |
||||
{ |
||||
IsDeleted: false, |
||||
LineNumber: 1 |
||||
} |
||||
], |
||||
LineTrackers: [ |
||||
{} |
||||
], |
||||
UndoStack: { |
||||
IsOriginalFile: true, |
||||
AcceptChanges: true, |
||||
CanUndo: false, |
||||
CanRedo: false, |
||||
SizeLimit: 2147483647 |
||||
}, |
||||
LineCount: 1 |
||||
} |
||||
}, |
||||
LaunchButtonVisibility: false, |
||||
StopButtonVisibility: false, |
||||
IsLaunchTeachingTipsOpen: false, |
||||
ShowWebUiButton: false, |
||||
AutoScrollToEnd: true, |
||||
ShowManualInputPrompt: false, |
||||
ShowConfirmInputPrompt: false, |
||||
LaunchCommand: LaunchPageViewModel.LaunchAsync(string command), |
||||
ConfigCommand: LaunchPageViewModel.Config(), |
||||
SendConfirmInputCommand: LaunchPageViewModel.SendConfirmInput(bool value), |
||||
SendManualInputCommand: LaunchPageViewModel.SendManualInput(string input), |
||||
CanNavigateNext: false, |
||||
CanNavigatePrevious: false, |
||||
HasErrors: false |
||||
}, |
||||
HasErrors: false |
||||
} |
||||
} |
@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
||||
<PropertyGroup> |
||||
<TargetFramework>net7.0</TargetFramework> |
||||
<ImplicitUsings>enable</ImplicitUsings> |
||||
<Nullable>enable</Nullable> |
||||
|
||||
<IsPackable>false</IsPackable> |
||||
<IsTestProject>true</IsTestProject> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<PackageReference Include="Avalonia.Headless.XUnit" Version="11.0.5" /> |
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0"/> |
||||
<PackageReference Include="NSubstitute" Version="5.1.0" /> |
||||
<PackageReference Include="Verify" Version="22.1.4" /> |
||||
<PackageReference Include="Verify.CommunityToolkit.Mvvm" Version="0.1.0" /> |
||||
<PackageReference Include="Verify.Avalonia" Version="1.0.1" /> |
||||
<PackageReference Include="Verify.Phash" Version="3.1.0" /> |
||||
<PackageReference Include="Verify.Xunit" Version="22.1.4" /> |
||||
<PackageReference Include="xunit" Version="2.6.1"/> |
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"> |
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |
||||
<PrivateAssets>all</PrivateAssets> |
||||
</PackageReference> |
||||
<PackageReference Include="coverlet.collector" Version="3.2.0"> |
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |
||||
<PrivateAssets>all</PrivateAssets> |
||||
</PackageReference> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<ProjectReference Include="..\StabilityMatrix.Avalonia\StabilityMatrix.Avalonia.csproj" /> |
||||
<ProjectReference Include="..\StabilityMatrix.Core\StabilityMatrix.Core.csproj" /> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
@ -0,0 +1,40 @@
|
||||
using System.Runtime.CompilerServices; |
||||
|
||||
namespace StabilityMatrix.UITests; |
||||
|
||||
public class TempDirFixture : IDisposable |
||||
{ |
||||
public static string ModuleTempDir { get; set; } |
||||
|
||||
static TempDirFixture() |
||||
{ |
||||
var tempDir = Path.Combine(Path.GetTempPath(), "StabilityMatrixTest"); |
||||
|
||||
if (Directory.Exists(tempDir)) |
||||
{ |
||||
Directory.Delete(tempDir, true); |
||||
} |
||||
Directory.CreateDirectory(tempDir); |
||||
|
||||
ModuleTempDir = tempDir; |
||||
|
||||
// ReSharper disable once LocalizableElement |
||||
Console.WriteLine($"Using temp dir: {ModuleTempDir}"); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public void Dispose() |
||||
{ |
||||
if (Directory.Exists(ModuleTempDir)) |
||||
{ |
||||
// ReSharper disable once LocalizableElement |
||||
Console.WriteLine($"Deleting temp dir: {ModuleTempDir}"); |
||||
Directory.Delete(ModuleTempDir, true); |
||||
} |
||||
|
||||
GC.SuppressFinalize(this); |
||||
} |
||||
} |
||||
|
||||
[CollectionDefinition("TempDir")] |
||||
public class TempDirCollection : ICollectionFixture<TempDirFixture> { } |
@ -0,0 +1,78 @@
|
||||
using Microsoft.Extensions.DependencyInjection; |
||||
using NSubstitute; |
||||
using NSubstitute.Extensions; |
||||
using Semver; |
||||
using StabilityMatrix.Avalonia; |
||||
using StabilityMatrix.Avalonia.ViewModels.Dialogs; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Models.Update; |
||||
using StabilityMatrix.Core.Services; |
||||
using StabilityMatrix.Core.Updater; |
||||
using StabilityMatrix.UITests; |
||||
|
||||
[assembly: AvaloniaTestApplication(typeof(TestAppBuilder))] |
||||
|
||||
namespace StabilityMatrix.UITests; |
||||
|
||||
public static class TestAppBuilder |
||||
{ |
||||
public static AppBuilder BuildAvaloniaApp() |
||||
{ |
||||
ConfigureGlobals(); |
||||
|
||||
Program.SetupAvaloniaApp(); |
||||
|
||||
App.BeforeBuildServiceProvider += (_, x) => ConfigureAppServices(x); |
||||
|
||||
return AppBuilder |
||||
.Configure<App>() |
||||
.UseSkia() |
||||
.UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false }); |
||||
} |
||||
|
||||
private static void ConfigureGlobals() |
||||
{ |
||||
var tempDir = TempDirFixture.ModuleTempDir; |
||||
var globalSettings = Path.Combine(tempDir, "AppDataHome"); |
||||
|
||||
Compat.SetAppDataHome(globalSettings); |
||||
} |
||||
|
||||
private static void ConfigureAppServices(IServiceCollection serviceCollection) |
||||
{ |
||||
// ISettingsManager |
||||
var settingsManager = Substitute.ForPartsOf<SettingsManager>(); |
||||
serviceCollection.AddSingleton<ISettingsManager>(settingsManager); |
||||
|
||||
// IUpdateHelper |
||||
var mockUpdateInfo = new UpdateInfo( |
||||
SemVersion.Parse("2.999.0"), |
||||
DateTimeOffset.UnixEpoch, |
||||
UpdateChannel.Stable, |
||||
UpdateType.Normal, |
||||
"https://example.org", |
||||
"https://example.org", |
||||
"46e11a5216c55d4c9d3c54385f62f3e1022537ae191615237f05e06d6f8690d0", |
||||
"IX5/CCXWJQG0oGkYWVnuF34gTqF/dJSrDrUd6fuNMYnncL39G3HSvkXrjvJvR18MA2rQNB5z13h3/qBSf9c7DA==" |
||||
); |
||||
|
||||
var updateHelper = Substitute.For<IUpdateHelper>(); |
||||
updateHelper |
||||
.Configure() |
||||
.StartCheckingForUpdates() |
||||
.Returns(Task.CompletedTask) |
||||
.AndDoes(_ => EventManager.Instance.OnUpdateAvailable(mockUpdateInfo)); |
||||
|
||||
serviceCollection.AddSingleton(updateHelper); |
||||
|
||||
// UpdateViewModel |
||||
var updateViewModel = Substitute.ForPartsOf<UpdateViewModel>( |
||||
settingsManager, |
||||
null, |
||||
updateHelper |
||||
); |
||||
updateViewModel.Configure().GetReleaseNotes("").Returns("Test"); |
||||
|
||||
serviceCollection.AddSingleton(updateViewModel); |
||||
} |
||||
} |
@ -0,0 +1,6 @@
|
||||
global using Xunit; |
||||
global using Avalonia; |
||||
global using Avalonia.Headless; |
||||
global using Avalonia.Headless.XUnit; |
||||
global using Avalonia.Input; |
||||
global using StabilityMatrix.UITests.Attributes; |
@ -0,0 +1,15 @@
|
||||
using AsyncAwaitBestPractices; |
||||
|
||||
namespace StabilityMatrix.UITests; |
||||
|
||||
internal static class VerifyConfig |
||||
{ |
||||
public static VerifySettings Default { get; } |
||||
|
||||
static VerifyConfig() |
||||
{ |
||||
Default = new VerifySettings(); |
||||
Default.IgnoreMembersWithType<WeakEventManager>(); |
||||
Default.DisableDiff(); |
||||
} |
||||
} |
@ -0,0 +1,51 @@
|
||||
namespace StabilityMatrix.UITests; |
||||
|
||||
public static class WaitHelper |
||||
{ |
||||
public static async Task<T> WaitForConditionAsync<T>( |
||||
Func<T> getter, |
||||
Func<T, bool> condition, |
||||
int delayMs = 50, |
||||
int maxAttempts = 20, |
||||
int initialDelayMs = 100 |
||||
) |
||||
{ |
||||
await Task.Delay(initialDelayMs); |
||||
|
||||
for (var i = 0; i < maxAttempts; i++) |
||||
{ |
||||
await Task.Delay(delayMs); |
||||
|
||||
var result = getter(); |
||||
|
||||
if (condition(result)) |
||||
{ |
||||
return result; |
||||
} |
||||
} |
||||
|
||||
throw new TimeoutException("Waited too long for a condition to be met"); |
||||
} |
||||
|
||||
public static async Task<T> WaitForNotNullAsync<T>( |
||||
Func<T?> getter, |
||||
int delayMs = 50, |
||||
int maxAttempts = 20, |
||||
int initialDelayMs = 100 |
||||
) |
||||
{ |
||||
await Task.Delay(initialDelayMs); |
||||
|
||||
for (var i = 0; i < maxAttempts; i++) |
||||
{ |
||||
await Task.Delay(delayMs); |
||||
|
||||
if (getter() is { } result) |
||||
{ |
||||
return result; |
||||
} |
||||
} |
||||
|
||||
throw new TimeoutException("Waited too long for a non-null value"); |
||||
} |
||||
} |
Loading…
Reference in new issue