Browse Source

Add Verify UI Tests

pull/240/head
Ionite 1 year ago
parent
commit
44990e9520
No known key found for this signature in database
  1. 17
      StabilityMatrix.Avalonia/App.axaml.cs
  2. 12
      StabilityMatrix.Avalonia/Program.cs
  3. 1
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  4. 16
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  5. 9
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  6. 25
      StabilityMatrix.Core/Helper/Compat.cs
  7. 21
      StabilityMatrix.Core/Helper/ContextManager.cs
  8. 1
      StabilityMatrix.Core/StabilityMatrix.Core.csproj
  9. 97
      StabilityMatrix.UITests/MainWindowTests.cs
  10. 25
      StabilityMatrix.UITests/ModuleInit.cs
  11. 39
      StabilityMatrix.UITests/Snapshots/MainWindowTests.MainWindowViewModel_ShouldOk.verified.txt
  12. BIN
      StabilityMatrix.UITests/Snapshots/MainWindowTests.MainWindow_ShouldOpen.received.png
  13. 444
      StabilityMatrix.UITests/Snapshots/MainWindowTests.MainWindow_ShouldOpen.received.txt
  14. 1
      StabilityMatrix.UITests/Snapshots/MainWindowTests.MainWindow_ShouldOpen.verified.txt
  15. 37
      StabilityMatrix.UITests/StabilityMatrix.UITests.csproj
  16. 35
      StabilityMatrix.UITests/TempDirFixture.cs
  17. 78
      StabilityMatrix.UITests/TestAppBuilder.cs
  18. 5
      StabilityMatrix.UITests/Usings.cs
  19. 14
      StabilityMatrix.UITests/VerifyConfig.cs
  20. 51
      StabilityMatrix.UITests/WaitHelper.cs

17
StabilityMatrix.Avalonia/App.axaml.cs

@ -70,13 +70,15 @@ public sealed class App : Application
public static IServiceProvider? Services { get; private set; }
[NotNull]
public static Visual? VisualRoot { get; private set; }
public static Visual? VisualRoot { get; internal set; }
public static TopLevel TopLevel => TopLevel.GetTopLevel(VisualRoot)!;
[NotNull]
public static IStorageProvider? StorageProvider { get; private set; }
public static IStorageProvider? StorageProvider { get; internal set; }
[NotNull]
public static IClipboard? Clipboard { get; private set; }
public static IClipboard? Clipboard { get; internal set; }
// ReSharper disable once MemberCanBePrivate.Global
[NotNull]
@ -86,6 +88,12 @@ public sealed class App : Application
public IClassicDesktopStyleApplicationLifetime? DesktopLifetime =>
ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
/// <summary>
/// Called before <see cref="Services"/> is built.
/// Can be used by UI tests to override services.
/// </summary>
internal static event EventHandler<IServiceCollection>? BeforeBuildServiceProvider;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@ -214,6 +222,9 @@ public sealed class App : Application
private static void ConfigureServiceProvider()
{
var services = ConfigureServices();
BeforeBuildServiceProvider?.Invoke(null, services);
Services = services.BuildServiceProvider();
var settingsManager = Services.GetRequiredService<ISettingsManager>();

12
StabilityMatrix.Avalonia/Program.cs

@ -83,13 +83,21 @@ public class Program
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
/// <summary>
/// Called in <see cref="BuildAvaloniaApp"/> and UI tests to setup static configurations
/// </summary>
internal static void SetupAvaloniaApp()
{
IconProvider.Current.Register<FontAwesomeIconProvider>();
// Use our custom image loader for custom local load error handling
ImageLoader.AsyncImageLoader.Dispose();
ImageLoader.AsyncImageLoader = new FallbackRamCachedWebImageLoader();
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
SetupAvaloniaApp();
var app = AppBuilder.Configure<App>().UsePlatformDetect().WithInterFont().LogToTrace();

1
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -16,6 +16,7 @@
<ItemGroup>
<InternalsVisibleTo Include="StabilityMatrix.Tests" />
<InternalsVisibleTo Include="StabilityMatrix.UITests" />
</ItemGroup>
<ItemGroup>

16
StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs

@ -124,24 +124,30 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
if (UpdateInfo is null)
return;
ReleaseNotes = await GetReleaseNotes(UpdateInfo.ChangelogUrl);
}
internal async Task<string> GetReleaseNotes(string changelogUrl)
{
using var client = httpClientFactory.CreateClient();
var response = await client.GetAsync(UpdateInfo.ChangelogUrl);
var response = await client.GetAsync(changelogUrl);
if (response.IsSuccessStatusCode)
{
var changelog = await response.Content.ReadAsStringAsync();
// Formatting for new changelog format
// https://keepachangelog.com/en/1.1.0/
if (UpdateInfo.ChangelogUrl.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
if (changelogUrl.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
{
ReleaseNotes =
FormatChangelog(changelog, Compat.AppVersion)
return FormatChangelog(changelog, Compat.AppVersion)
?? "## Unable to format release notes";
}
return changelog;
}
else
{
ReleaseNotes = "## Unable to load release notes";
return "## Unable to load release notes";
}
}

9
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -52,6 +52,8 @@ public partial class MainWindowViewModel : ViewModelBase
public ProgressManagerViewModel ProgressManagerViewModel { get; init; }
public UpdateViewModel UpdateViewModel { get; init; }
internal BetterContentDialog? CurrentDialog { get; private set; }
public MainWindowViewModel(
ISettingsManager settingsManager,
IDiscordRichPresenceService discordRichPresenceService,
@ -128,7 +130,7 @@ public partial class MainWindowViewModel : ViewModelBase
EventManager.Instance.OnTeachingTooltipNeeded();
};
await dialog.ShowAsync();
await dialog.ShowAsync(App.TopLevel);
}
}
@ -239,7 +241,10 @@ public partial class MainWindowViewModel : ViewModelBase
Content = new SelectDataDirectoryDialog { DataContext = viewModel }
};
var result = await dialog.ShowAsync();
CurrentDialog = dialog;
dialog.Closed += (_, _) => CurrentDialog = null;
var result = await dialog.ShowAsync(App.TopLevel);
if (result == ContentDialogResult.Primary)
{
// 1. For portable mode, call settings.SetPortableMode()

25
StabilityMatrix.Core/Helper/Compat.cs

@ -78,8 +78,10 @@ public static class Compat
static Compat()
{
var infoVersion = Assembly.GetCallingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
var infoVersion = Assembly
.GetCallingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion;
AppVersion = SemVersion.Parse(infoVersion ?? "0.0.0", SemVersionStyles.Strict);
@ -102,10 +104,11 @@ public static class Compat
Platform = PlatformKind.Linux | PlatformKind.Unix;
// For AppImage builds, the path is in `$APPIMAGE`
var appPath = Environment.GetEnvironmentVariable("APPIMAGE") ??
AppContext.BaseDirectory;
AppCurrentDir = Path.GetDirectoryName(appPath) ??
throw new Exception("Could not find application directory");
var appPath =
Environment.GetEnvironmentVariable("APPIMAGE") ?? AppContext.BaseDirectory;
AppCurrentDir =
Path.GetDirectoryName(appPath)
?? throw new Exception("Could not find application directory");
ExeExtension = "";
DllExtension = ".so";
}
@ -144,8 +147,9 @@ public static class Compat
}
throw new PlatformNotSupportedException(
$"Platform {Platform.ToString()} is not in supported targets: " +
$"{string.Join(", ", targets.Select(t => t.platform.ToString()))}");
$"Platform {Platform.ToString()} is not in supported targets: "
+ $"{string.Join(", ", targets.Select(t => t.platform.ToString()))}"
);
}
/// <summary>
@ -175,7 +179,10 @@ public static class Compat
public static string GetEnvPathWithExtensions(params string[] paths)
{
var currentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process);
var currentPath = Environment.GetEnvironmentVariable(
"PATH",
EnvironmentVariableTarget.Process
);
var newPath = string.Join(PathDelimiter, paths);
if (string.IsNullOrEmpty(currentPath))

21
StabilityMatrix.Core/Helper/ContextManager.cs

@ -0,0 +1,21 @@
using System.Linq.Expressions;
namespace StabilityMatrix.Core.Helper;
/*/// <summary>
/// Context helper for setting properties to one value on entry and another on dispose.
/// </summary>
public class ContextManager<T, TProperty> : IDisposable
{
private Accessor accessor;
public ContextManager(Expression<Func<T, TProperty>> expression, T context, TProperty value)
{
var accessorInfo = ((MemberExpression) expression.Body).Member;
accessor = Accessors.Find(accessorInfo) ?? throw new ArgumentException("Accessor not found", nameof(expression));
originalValue = (TProperty)propertyInfo.GetValue(context);
propertyInfo.SetValue(context, value);
}
}*/

1
StabilityMatrix.Core/StabilityMatrix.Core.csproj

@ -11,6 +11,7 @@
<ItemGroup>
<InternalsVisibleTo Include="StabilityMatrix.Tests" />
<InternalsVisibleTo Include="StabilityMatrix.UITests" />
</ItemGroup>
<ItemGroup>

97
StabilityMatrix.UITests/MainWindowTests.cs

@ -0,0 +1,97 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
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;
namespace StabilityMatrix.UITests;
[UsesVerify]
public class MainWindowTests
{
private static IServiceProvider Services => App.Services;
private static VerifySettings Settings
{
get
{
var settings = new VerifySettings();
settings.IgnoreMembers<MainWindowViewModel>(
vm => vm.Pages,
vm => vm.FooterPages,
vm => vm.CurrentPage
);
return settings;
}
}
private static (AppWindow, MainWindowViewModel) GetMainWindow()
{
var window = Services.GetRequiredService<MainWindow>();
var viewModel = Services.GetRequiredService<MainWindowViewModel>();
window.DataContext = viewModel;
window.SetDefaultFonts();
App.VisualRoot = window;
App.StorageProvider = window.StorageProvider;
App.Clipboard = window.Clipboard ?? throw new NullReferenceException("Clipboard is null");
return (window, viewModel);
}
private static BetterContentDialog? GetWindowDialog(Visual window)
{
return window
.FindDescendantOfType<VisualLayerManager>()
?.FindDescendantOfType<OverlayLayer>()
?.FindDescendantOfType<DialogHost>()
?.FindDescendantOfType<LayoutTransformControl>()
?.FindDescendantOfType<VisualLayerManager>()
?.FindDescendantOfType<BetterContentDialog>();
}
[AvaloniaFact]
public Task MainWindowViewModel_ShouldOk()
{
var viewModel = Services.GetRequiredService<MainWindowViewModel>();
return Verify(viewModel, Settings);
}
[AvaloniaFact]
public async Task MainWindow_ShouldOpen()
{
var (window, vm) = GetMainWindow();
window.Show();
// 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");
continueButton.Command?.Execute(null);
// Find the one click install dialog
var oneClickDialog = await WaitHelper.WaitForConditionAsync(
() => GetWindowDialog(window),
d => d?.Content is OneClickInstallDialog
);
Assert.NotNull(oneClickDialog);
await Verify(window, Settings);
}
}

25
StabilityMatrix.UITests/ModuleInit.cs

@ -0,0 +1,25 @@
using System.Runtime.CompilerServices;
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()
{
DerivePathInfo(
(sourceFile, projectDirectory, type, method) =>
new PathInfo(
directory: Path.Combine(projectDirectory, "Snapshots"),
typeName: type.Name,
methodName: method.Name
)
);
}
}

39
StabilityMatrix.UITests/Snapshots/MainWindowTests.MainWindowViewModel_ShouldOk.verified.txt

@ -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
}

BIN
StabilityMatrix.UITests/Snapshots/MainWindowTests.MainWindow_ShouldOpen.received.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

444
StabilityMatrix.UITests/Snapshots/MainWindowTests.MainWindow_ShouldOpen.received.txt

@ -0,0 +1,444 @@
{
Type: MainWindow,
Title: Stability Matrix,
Icon: {},
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
}
]
},
FontFamily: Segoe UI Variable Text,
Width: 1100.0,
Height: 750.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
}
}

1
StabilityMatrix.UITests/Snapshots/MainWindowTests.MainWindow_ShouldOpen.verified.txt

@ -0,0 +1 @@


37
StabilityMatrix.UITests/StabilityMatrix.UITests.csproj

@ -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.CommunityToolkit.Mvvm" Version="0.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>

35
StabilityMatrix.UITests/TempDirFixture.cs

@ -0,0 +1,35 @@
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");
Directory.CreateDirectory(tempDir);
ModuleTempDir = tempDir;
// ReSharper disable once LocalizableElement
Console.WriteLine($"Using temp dir: {ModuleTempDir}");
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.Synchronized)]
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> { }

78
StabilityMatrix.UITests/TestAppBuilder.cs

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

5
StabilityMatrix.UITests/Usings.cs

@ -0,0 +1,5 @@
global using Xunit;
global using Avalonia;
global using Avalonia.Headless;
global using Avalonia.Headless.XUnit;
global using Avalonia.Input;

14
StabilityMatrix.UITests/VerifyConfig.cs

@ -0,0 +1,14 @@
using AsyncAwaitBestPractices;
namespace StabilityMatrix.UITests;
internal static class VerifyConfig
{
public static VerifySettings Default { get; }
static VerifyConfig()
{
Default = new VerifySettings();
Default.IgnoreMembersWithType<WeakEventManager>();
}
}

51
StabilityMatrix.UITests/WaitHelper.cs

@ -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…
Cancel
Save