diff --git a/.github/workflows/build-test-publish.yml b/.github/workflows/build-test-publish.yml index 9280fb2..e16c41d 100644 --- a/.github/workflows/build-test-publish.yml +++ b/.github/workflows/build-test-publish.yml @@ -31,6 +31,13 @@ jobs: run: dotnet build ${{ env.SOLUTION_NAME }} --configuration Release /p:TreatWarningsAsErrors=true - name: Test run: dotnet test ${{ env.SOLUTION_NAME }} --no-build --configuration Release + - name: Upload Test Results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: verify-test-results + path: | + **/*.received.* - name: Check formatting using csharpier run: | diff --git a/Directory.Packages.props b/Directory.Packages.props index eeb7838..4ce324c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + @@ -26,8 +27,10 @@ + + diff --git a/src/AvaloniaExampleProject/App.axaml.cs b/src/AvaloniaExampleProject/App.axaml.cs index dbca0df..0fe4d0d 100644 --- a/src/AvaloniaExampleProject/App.axaml.cs +++ b/src/AvaloniaExampleProject/App.axaml.cs @@ -1,10 +1,9 @@ -using System.Data; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; +using Avalonia.Input; using Avalonia.Markup.Xaml; using AvaloniaExampleProject.Views; using Microsoft.Extensions.DependencyInjection; @@ -30,10 +29,6 @@ public App(IServiceProvider provider) [MemberNotNullWhen(true, nameof(IsDesignMode))] public static IServiceProvider? ServiceProvider { get; private set; } - public static string Version { get; } = - typeof(App).Assembly.GetCustomAttribute()?.InformationalVersion - ?? throw new VersionNotFoundException("Could not get version"); - public override void Initialize() { AvaloniaXamlLoader.Load(this); @@ -45,8 +40,10 @@ public override void OnFrameworkInitializationCompleted() { #if DEBUG this.AttachDeveloperTools(options => - options.AddMicrosoftLoggerObservable(_provider.GetRequiredService()) - ); + { + options.Gesture = new KeyGesture(Key.F11); + options.AddMicrosoftLoggerObservable(_provider.GetRequiredService()); + }); #endif DisableAvaloniaDataAnnotationValidation(); desktop.MainWindow = new MainWindow(_provider); diff --git a/src/AvaloniaExampleProject/AvaloniaExampleProject.csproj b/src/AvaloniaExampleProject/AvaloniaExampleProject.csproj index ec9703d..c7c80d7 100644 --- a/src/AvaloniaExampleProject/AvaloniaExampleProject.csproj +++ b/src/AvaloniaExampleProject/AvaloniaExampleProject.csproj @@ -15,6 +15,8 @@ + + None All @@ -30,4 +32,7 @@ + + + diff --git a/src/AvaloniaExampleProject/Bootstrapper.cs b/src/AvaloniaExampleProject/Bootstrapper.cs index d3b9c26..26557eb 100644 --- a/src/AvaloniaExampleProject/Bootstrapper.cs +++ b/src/AvaloniaExampleProject/Bootstrapper.cs @@ -20,6 +20,7 @@ public static IServiceCollection AddAppServices(this IServiceCollection serviceC .AddConfigurationFile("config.json", JsonContext.Default.MainConfig) .AddLocalization() .AddSingleton() + .AddSingleton() // Configure ViewModels .AddTransient() .AddTransient() diff --git a/src/AvaloniaExampleProject/Business/AppInformationService.cs b/src/AvaloniaExampleProject/Business/AppInformationService.cs new file mode 100644 index 0000000..b85b2ad --- /dev/null +++ b/src/AvaloniaExampleProject/Business/AppInformationService.cs @@ -0,0 +1,25 @@ +using System.Data; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace AvaloniaExampleProject.Business; + +public interface IAppInformationService +{ + string Version { get; } +} + +public sealed class AppInformationService : IAppInformationService +{ + [field: AllowNull, MaybeNull] + public string Version + { + get + { + field ??= + typeof(App).Assembly.GetCustomAttribute()?.InformationalVersion + ?? throw new VersionNotFoundException("Could not get version"); + return field!; + } + } +} diff --git a/src/AvaloniaExampleProject/Business/ThemeService.cs b/src/AvaloniaExampleProject/Business/ThemeService.cs index a591201..149a7ac 100644 --- a/src/AvaloniaExampleProject/Business/ThemeService.cs +++ b/src/AvaloniaExampleProject/Business/ThemeService.cs @@ -1,5 +1,3 @@ -using System.ComponentModel; -using System.Reactive.Disposables; using Avalonia.Styling; using AvaloniaExampleProject.Models; using Darp.Utils.Configuration; diff --git a/src/AvaloniaExampleProject/Models/DesignData.cs b/src/AvaloniaExampleProject/Models/DesignData.cs index 53b2c40..a7143de 100644 --- a/src/AvaloniaExampleProject/Models/DesignData.cs +++ b/src/AvaloniaExampleProject/Models/DesignData.cs @@ -14,4 +14,5 @@ public static IServiceProvider ServiceProvider public static WelcomeViewModel WelcomeViewModel { get; } = ServiceProvider.GetRequiredService(); public static SettingsViewModel SettingsViewModel { get; } = ServiceProvider.GetRequiredService(); + public static MainViewModel MainViewModel { get; } = ServiceProvider.GetRequiredService(); } diff --git a/src/AvaloniaExampleProject/ViewModels/SettingsViewModel.cs b/src/AvaloniaExampleProject/ViewModels/SettingsViewModel.cs index 771d96f..5e753bd 100644 --- a/src/AvaloniaExampleProject/ViewModels/SettingsViewModel.cs +++ b/src/AvaloniaExampleProject/ViewModels/SettingsViewModel.cs @@ -17,16 +17,19 @@ public sealed partial class SettingsViewModel( Resources i18N, IConfigurationService configurationService, IDialogService dialogService, + IAppInformationService appInformationService, ILogger logger ) : ViewModelBase { private readonly IConfigurationService _configurationService = configurationService; private readonly IDialogService _dialogService = dialogService; + private readonly IAppInformationService _appInformationService = appInformationService; private readonly ILogger _logger = logger; public IThemeService ThemeService { get; } = themeService; public Resources I18N { get; } = i18N; - public IObservable AppVersion => I18N.Observe(x => x.FormatSettings_About_Version(App.Version)); + public IObservable AppVersion => + I18N.Observe(x => x.FormatSettings_About_Version(_appInformationService.Version)); [ObservableProperty] public partial string SelectedLanguage { get; set; } = configurationService.Config.UserPreferences.SelectedLanguage; diff --git a/src/AvaloniaExampleProject/Views/MainView.axaml b/src/AvaloniaExampleProject/Views/MainView.axaml index 39fcba9..92e978a 100644 --- a/src/AvaloniaExampleProject/Views/MainView.axaml +++ b/src/AvaloniaExampleProject/Views/MainView.axaml @@ -4,19 +4,27 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:AvaloniaExampleProject.ViewModels" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:models="clr-namespace:AvaloniaExampleProject.Models" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="AvaloniaExampleProject.Views.MainView" - x:DataType="vm:MainViewModel"> + x:DataType="vm:MainViewModel" + Design.DataContext="{x:Static models:DesignData.MainViewModel}"> 0,0,0,0 0,0,0,0 + + 8,8,0,0 + + + @@ -26,10 +34,7 @@ Content="{CompiledBinding I18N.Settings_Title}" /> - - - - + diff --git a/src/AvaloniaExampleProject/Views/SettingsView.axaml b/src/AvaloniaExampleProject/Views/SettingsView.axaml index de6fe92..24dccf1 100644 --- a/src/AvaloniaExampleProject/Views/SettingsView.axaml +++ b/src/AvaloniaExampleProject/Views/SettingsView.axaml @@ -53,7 +53,8 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render.verified.png b/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render.verified.png new file mode 100644 index 0000000..7488c3d Binary files /dev/null and b/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render.verified.png differ diff --git a/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render.verified.txt b/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render.verified.txt index 42ccfb0..495623a 100644 --- a/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render.verified.txt +++ b/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render.verified.txt @@ -39,11 +39,13 @@ }, { Type: SettingsExpander, - Header: Avalonia Example Project + Header: Avalonia Example Project, + Name: AboutSettingsExpander } ] } }, + FontFamily: Inter, DataContext: { ThemeService: { AvailableThemes: [ diff --git a/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render_AboutExpanded.verified.png b/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render_AboutExpanded.verified.png new file mode 100644 index 0000000..48aa61c Binary files /dev/null and b/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render_AboutExpanded.verified.png differ diff --git a/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render_AboutExpanded.verified.txt b/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render_AboutExpanded.verified.txt new file mode 100644 index 0000000..495623a --- /dev/null +++ b/test/AvaloniaExampleProject.Tests/Snapshots/SettingsViewModelTests/SettingsViewModelTests.Render_AboutExpanded.verified.txt @@ -0,0 +1,64 @@ +{ + Type: SettingsView, + Content: { + Type: ScrollViewer, + VerticalScrollBarVisibility: Auto, + Content: { + Type: StackPanel, + Spacing: 8.0, + MaxWidth: 1024.0, + Margin: 0,0,0,48, + Children: [ + { + Type: TextBlock, + Text: Personalization, + Theme: { + Type: ControlTheme + } + }, + { + Type: StackPanel, + Spacing: 4.0, + Children: [ + { + Type: SettingsExpander, + Header: Language + }, + { + Type: SettingsExpander, + Header: Theme + } + ] + }, + { + Type: TextBlock, + Text: About, + Theme: { + Type: ControlTheme + } + }, + { + Type: SettingsExpander, + Header: Avalonia Example Project, + Name: AboutSettingsExpander + } + ] + } + }, + FontFamily: Inter, + DataContext: { + ThemeService: { + AvailableThemes: [ + Default, + Dark, + Light + ], + RequestedThemeVariant: {} + }, + I18N: {Scrubbed}, + AppVersion: {}, + SelectedLanguage: en-EN, + SelectedTheme: Default, + ShowLicensesDialogCommand: SettingsViewModel.ShowLicensesDialogAsync(CancellationToken cancellationToken) + } +} \ No newline at end of file diff --git a/test/AvaloniaExampleProject.Tests/Snapshots/WelcomeViewModelTests/WelcomeViewModelTests.Render.verified.png b/test/AvaloniaExampleProject.Tests/Snapshots/WelcomeViewModelTests/WelcomeViewModelTests.Render.verified.png new file mode 100644 index 0000000..8b5713c Binary files /dev/null and b/test/AvaloniaExampleProject.Tests/Snapshots/WelcomeViewModelTests/WelcomeViewModelTests.Render.verified.png differ diff --git a/test/AvaloniaExampleProject.Tests/Snapshots/WelcomeViewModelTests/WelcomeViewModelTests.Render.verified.txt b/test/AvaloniaExampleProject.Tests/Snapshots/WelcomeViewModelTests/WelcomeViewModelTests.Render.verified.txt index 1bf0ccb..6303292 100644 --- a/test/AvaloniaExampleProject.Tests/Snapshots/WelcomeViewModelTests/WelcomeViewModelTests.Render.verified.txt +++ b/test/AvaloniaExampleProject.Tests/Snapshots/WelcomeViewModelTests/WelcomeViewModelTests.Render.verified.txt @@ -15,6 +15,7 @@ } ] }, + FontFamily: Inter, DataContext: { I18N: {Scrubbed}, ShowInputDialogCommand: WelcomeViewModel.ShowInputDialog(CancellationToken cancellationToken) diff --git a/test/AvaloniaExampleProject.Tests/TestAppBuilder.cs b/test/AvaloniaExampleProject.Tests/TestAppBuilder.cs index 13af150..9c50290 100644 --- a/test/AvaloniaExampleProject.Tests/TestAppBuilder.cs +++ b/test/AvaloniaExampleProject.Tests/TestAppBuilder.cs @@ -1,7 +1,9 @@ -using System.Reflection; using System.Runtime.CompilerServices; using Avalonia; +using Avalonia.Fonts.Inter; using Avalonia.Headless; +using Avalonia.Media; +using AvaloniaExampleProject.Business; using AvaloniaExampleProject.Models; using AvaloniaExampleProject.Tests; using Darp.Utils.Configuration; @@ -19,17 +21,8 @@ public class TestAppBuilder [ModuleInitializer] public static void Init() { + VerifyImageMagick.RegisterComparers(0.1); VerifierSettings.InitializePlugins(); - RemovePngFileConverters(); - } - - private static void RemovePngFileConverters() - { - object? list = typeof(VerifierSettings) - .GetField("typedConverters", BindingFlags.NonPublic | BindingFlags.Static) - ?.GetValue(null); - var clazz = typeof(VerifierSettings).Assembly.GetType("TypeConverter")!; - typeof(List<>).MakeGenericType(clazz).GetMethod("Clear")!.Invoke(list, null); } private static readonly MainConfig MainConfig = new() @@ -57,21 +50,28 @@ public static IConfigurationService SubstituteForMainConfigService() public static AppBuilder BuildAvaloniaApp() { + var appInformationService = Substitute.For(); + appInformationService.Version.Returns("1.2.3-aabbccdd"); IServiceProvider provider = new ServiceCollection() .AddLogging(builder => builder.AddXUnit()) .AddSingleton(SubstituteForMainConfigService()) .AddAppServices() + .AddSingleton(appInformationService) .BuildServiceProvider(); Services = provider; return AppBuilder .Configure(() => new App(provider)) + .UseSkia() .UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false }) + .WithInterFont() .AfterSetup(builder => { if (builder.Instance is null) throw new Exception("Instance is null"); + var themes = builder.Instance.Styles.OfType(); + builder.Instance.Styles.RemoveAll(themes); builder.Instance.Styles.Add( - new FluentAvaloniaTheme { PreferSystemTheme = true, PreferUserAccentColor = true } + new FluentAvaloniaTheme { PreferSystemTheme = false, PreferUserAccentColor = false } ); }); } diff --git a/test/AvaloniaExampleProject.Tests/VerifyHelpers.cs b/test/AvaloniaExampleProject.Tests/VerifyHelpers.cs index 36d2c1a..e6dbc61 100644 --- a/test/AvaloniaExampleProject.Tests/VerifyHelpers.cs +++ b/test/AvaloniaExampleProject.Tests/VerifyHelpers.cs @@ -1,13 +1,20 @@ using System.Runtime.CompilerServices; -using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Media; using AvaloniaExampleProject.Assets; +using Shouldly; namespace AvaloniaExampleProject.Tests; public static class VerifyHelpers { - public static SettingsTask VerifyControl(Control control, [CallerFilePath] string? callerFilePath = null) + public static SettingsTask VerifyControl(TemplatedControl control, [CallerFilePath] string? callerFilePath = null) { + var fontFamily = new FontFamily("avares://Avalonia.Fonts.Inter/Assets/Inter-Regular.ttf#Inter"); + control.FontFamily = fontFamily; + control.Resources.Add("ContentControlThemeFontFamily", fontFamily); + control.FontFamily.Name.ShouldBe("Inter"); + control.FontFamily.FamilyTypefaces.Count.ShouldBeGreaterThan(0); string directory = Path.GetFileNameWithoutExtension(callerFilePath) ?? throw new ArgumentNullException(nameof(callerFilePath)); return Verify(control).ScrubMembersWithType().UseDirectory(Path.Join("Snapshots", directory)); diff --git a/test/AvaloniaExampleProject.Tests/ViewModels/SettingsViewModelTests.cs b/test/AvaloniaExampleProject.Tests/ViewModels/SettingsViewModelTests.cs index 30c8dd6..7b39a0e 100644 --- a/test/AvaloniaExampleProject.Tests/ViewModels/SettingsViewModelTests.cs +++ b/test/AvaloniaExampleProject.Tests/ViewModels/SettingsViewModelTests.cs @@ -12,8 +12,18 @@ public class SettingsViewModelTests [AvaloniaFact] public Task Render() { - var viewModel = TestAppBuilder.Services.GetRequiredService(); - var control = new SettingsView { ViewModel = viewModel }; + var control = new SettingsView { ViewModel = TestAppBuilder.Services.GetRequiredService() }; return VerifyControl(control).ScrubMembersWithType(); } + + [AvaloniaFact] + public Task Render_AboutExpanded() + { + var control = new SettingsView + { + ViewModel = TestAppBuilder.Services.GetRequiredService(), + AboutSettingsExpander = { IsExpanded = true }, + }; + return VerifyControl(control); + } } diff --git a/test/AvaloniaExampleProject.Tests/ViewModels/WelcomeViewModelTests.cs b/test/AvaloniaExampleProject.Tests/ViewModels/WelcomeViewModelTests.cs index 1e1f9e0..42ccdd6 100644 --- a/test/AvaloniaExampleProject.Tests/ViewModels/WelcomeViewModelTests.cs +++ b/test/AvaloniaExampleProject.Tests/ViewModels/WelcomeViewModelTests.cs @@ -1,5 +1,4 @@ using Avalonia.Headless.XUnit; -using AvaloniaExampleProject.Assets; using AvaloniaExampleProject.ViewModels; using AvaloniaExampleProject.Views; using Microsoft.Extensions.DependencyInjection; @@ -14,6 +13,6 @@ public Task Render() { var viewModel = TestAppBuilder.Services.GetRequiredService(); var control = new WelcomeView { ViewModel = viewModel }; - return VerifyControl(control).ScrubMembersWithType(); + return VerifyControl(control); } }