更新框架
This commit is contained in:
1 parent
ef50fa0620
commit
84b28f9672
72 files changed
+2222
-282
No files matched your search
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Source/Core/MAX.Core/MAX.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,54 @@
|
||||
using Autofac;
|
||||
using MAX.Core.Infrastructure;
|
||||
using MAX.Notification;
|
||||
|
||||
var builder = new ContainerBuilder();
|
||||
new MAX.Core.Startup().Register(builder);
|
||||
builder.RegisterType<ScopeState>().InstancePerLifetimeScope();
|
||||
using var container = builder.Build();
|
||||
using (var scope = container.BeginLifetimeScope())
|
||||
{
|
||||
var publisher = scope.Resolve<IPublisher>();
|
||||
var manager = scope.Resolve<MediatorManager>();
|
||||
Check(ReferenceEquals(publisher, manager), "发布接口和管理器应为同一实例");
|
||||
var state = scope.Resolve<ScopeState>();
|
||||
await publisher.Publish(new Changed());
|
||||
Check(state.Calls == 1, "程序集扫描和作用域内处理器解析");
|
||||
var dynamicHandler = new DynamicHandler<Changed>();
|
||||
manager.RegisterNotification(dynamicHandler);
|
||||
await publisher.Publish(new Changed());
|
||||
Check(state.Calls == 2 && dynamicHandler.Calls == 1, "容器和动态处理器均收到通知");
|
||||
using var otherScope = container.BeginLifetimeScope();
|
||||
var otherPublisher = otherScope.Resolve<IPublisher>();
|
||||
Check(!ReferenceEquals(publisher, otherPublisher), "不同作用域拥有不同发布器");
|
||||
await otherPublisher.Publish(new Changed());
|
||||
Check(otherScope.Resolve<ScopeState>().Calls == 1 && state.Calls == 2,
|
||||
"处理器使用所属作用域的依赖");
|
||||
Check(dynamicHandler.Calls == 1, "动态注册不泄漏到其他作用域");
|
||||
}
|
||||
Console.WriteLine("MAX.Core notification integration tests passed.");
|
||||
|
||||
static void Check(bool condition, string message)
|
||||
{
|
||||
if (!condition) throw new InvalidOperationException(message);
|
||||
}
|
||||
public sealed record Changed : INotification;
|
||||
public sealed class ScopeState { public int Calls { get; set; } }
|
||||
public sealed class ScopedHandler(ScopeState state) : INotificationHandler<Changed>
|
||||
{
|
||||
public Task Handle(Changed notification, CancellationToken cancellationToken)
|
||||
{
|
||||
state.Calls++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
// 保持开放泛型,避免被程序集扫描为封闭处理器;由测试显式注册实例。
|
||||
public class DynamicHandler<T> : INotificationHandler<T> where T : INotification
|
||||
{
|
||||
public int Calls { get; private set; }
|
||||
public Task Handle(T notification, CancellationToken cancellationToken)
|
||||
{
|
||||
Calls++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="PolySharp" />
|
||||
<ProjectReference Include="../../Source/Core/MAX.Notification/MAX.Notification.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,86 @@
|
||||
using MAX.Notification.NotificationPublishers;
|
||||
using MAX.Notification;
|
||||
|
||||
var calls = new List<int>();
|
||||
var services = new TestServices();
|
||||
services.Handlers = [new First((_, _) => { calls.Add(1); return Task.CompletedTask; }),
|
||||
new First((_, _) => { calls.Add(99); return Task.CompletedTask; }),
|
||||
new Second((_, _) => { calls.Add(2); return Task.CompletedTask; })];
|
||||
var publisher = new NotificationPublisher(services);
|
||||
INotification message = new Changed();
|
||||
await publisher.Publish(message);
|
||||
Check(calls.SequenceEqual([1, 2]), "运行时类型分发、顺序和类型去重");
|
||||
calls.Clear();
|
||||
await publisher.Publish((object)message);
|
||||
Check(calls.SequenceEqual([1, 2]) && services.Resolutions == 2, "对象分发和每次重新解析");
|
||||
services.Handlers = [];
|
||||
await publisher.Publish(message);
|
||||
services.Handlers = null;
|
||||
await publisher.Publish(message);
|
||||
await Throws<ArgumentNullException>(() => publisher.Publish((object)null!));
|
||||
await Throws<ArgumentException>(() => publisher.Publish(new object()));
|
||||
await Throws<ArgumentNullException>(() => Task.FromResult(new NotificationPublisher(services, null!)));
|
||||
using var cancelled = new CancellationTokenSource();
|
||||
cancelled.Cancel();
|
||||
// 原版只传递取消令牌;没有处理器时即使已取消也正常完成。
|
||||
await publisher.Publish(message, cancelled.Token);
|
||||
|
||||
services.Handlers = [new First((_, _) => throw new InvalidOperationException("expected")),
|
||||
new Second((_, _) => { calls.Add(3); return Task.CompletedTask; })];
|
||||
calls.Clear();
|
||||
await Throws<InvalidOperationException>(() => publisher.Publish(message));
|
||||
Check(calls.Count == 0, "顺序模式失败后停止");
|
||||
await Throws<InvalidOperationException>(() => new NotificationPublisher(services,
|
||||
new TaskWhenAllPublisher()).Publish(message));
|
||||
Check(calls.Count == 0, "原版并发模式同步异常阻止后续枚举");
|
||||
|
||||
var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var started = 0;
|
||||
Task Handle(Changed _, CancellationToken token)
|
||||
{
|
||||
Check(token == cancelled.Token || !token.IsCancellationRequested, "取消令牌传递");
|
||||
if (++started == 2) gate.SetResult();
|
||||
return gate.Task;
|
||||
}
|
||||
services.Handlers = [new First(Handle), new Second(Handle)];
|
||||
await new NotificationPublisher(services, new TaskWhenAllPublisher())
|
||||
.Publish(message).WaitAsync(TimeSpan.FromSeconds(5));
|
||||
Check(started == 2, "并发启动和等待全部完成");
|
||||
|
||||
using var during = new CancellationTokenSource();
|
||||
services.Handlers = [new First((_, token) => { Check(token == during.Token, "原始令牌"); during.Cancel(); return Task.CompletedTask; }),
|
||||
new Second((_, token) => { token.ThrowIfCancellationRequested(); return Task.CompletedTask; })];
|
||||
await Throws<OperationCanceledException>(() => publisher.Publish(message, during.Token));
|
||||
Console.WriteLine("All notification smoke tests passed.");
|
||||
|
||||
static void Check(bool condition, string name)
|
||||
{
|
||||
if (!condition) throw new Exception($"检查失败:{name}");
|
||||
}
|
||||
static async Task Throws<T>(Func<Task> action) where T : Exception
|
||||
{
|
||||
try { await action(); }
|
||||
catch (T) { return; }
|
||||
throw new Exception($"预期异常:{typeof(T).Name}");
|
||||
}
|
||||
sealed record Changed : INotification;
|
||||
sealed class First(Func<Changed, CancellationToken, Task> action) : INotificationHandler<Changed>
|
||||
{
|
||||
public Task Handle(Changed notification, CancellationToken cancellationToken) => action(notification, cancellationToken);
|
||||
}
|
||||
sealed class Second(Func<Changed, CancellationToken, Task> action) : INotificationHandler<Changed>
|
||||
{
|
||||
public Task Handle(Changed notification, CancellationToken cancellationToken) => action(notification, cancellationToken);
|
||||
}
|
||||
sealed class TestServices : IServiceProvider
|
||||
{
|
||||
public INotificationHandler<Changed>[]? Handlers { get; set; }
|
||||
public int Resolutions { get; private set; }
|
||||
public object? GetService(Type serviceType)
|
||||
{
|
||||
if (serviceType != typeof(IEnumerable<INotificationHandler<Changed>>))
|
||||
throw new Exception($"错误的解析类型:{serviceType}");
|
||||
Resolutions++;
|
||||
return Handlers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Store.Management.Api;
|
||||
using Store.Management.Api.Authentication;
|
||||
using Store.Management.Api.Transport;
|
||||
using Store.Management.Api.Users;
|
||||
|
||||
static void Check(bool condition, string message)
|
||||
{
|
||||
if (!condition) throw new Exception(message);
|
||||
}
|
||||
static async Task Error(Func<Task> action, ApiErrorKind kind)
|
||||
{
|
||||
try { await action(); }
|
||||
catch (ApiException ex) { Check(ex.Kind == kind, $"预期 {kind},实际 {ex.Kind}"); return; }
|
||||
throw new Exception($"未抛出 {kind}");
|
||||
}
|
||||
var handler = new StubHandler();
|
||||
using var http = new HttpClient(handler);
|
||||
using var api = new StoreApiClient(new ApiClientOptions { BaseUrl = new Uri("https://test.invalid/root/") }, http);
|
||||
await Error(async () => await api.Auth.MeAsync(), ApiErrorKind.Unauthorized);
|
||||
Check(handler.Count == 0, "未登录不应发送请求");
|
||||
handler.Json = """{"code":200,"data":{"token":"session-one","tokenType":"Bearer","expiresInMinutes":100}}""";
|
||||
await api.Auth.LoginAsync(new LoginRequest { UserName = "admin", Password = "secret" });
|
||||
Check(handler.Path == "/root/v1/Auth/Login" && handler.Authorization is null, "登录路由或匿名认证错误");
|
||||
handler.Json = """{"code":200,"data":{"total":1,"items":[{"id":3,"userName":"alice"}]}}""";
|
||||
var page = await api.Users.SearchAsync(new UserSearchRequest { Page = 2, PageSize = 10 });
|
||||
Check(page.Total == 1 && page.Items[0].UserName == "alice", "分页解析失败");
|
||||
Check(handler.Authorization == "Bearer session-one" && handler.Body!.Contains("\"page\":2"), "Token 或分页请求体错误");
|
||||
var count = handler.Count;
|
||||
try { await api.Users.SearchAsync(new UserSearchRequest { Page = 0 }); throw new Exception("未校验分页"); }
|
||||
catch (ValidationException) { Check(handler.Count == count, "无效参数不应发送"); }
|
||||
handler.Json = """{"code":2001,"msg":"业务失败","traceId":"trace-1","data":null}""";
|
||||
await Error(async () => await api.Auth.MeAsync(), ApiErrorKind.Business);
|
||||
Check(api.Session.IsAuthenticated, "业务失败不应退出会话");
|
||||
handler.Json = "not-json";
|
||||
await Error(async () => await api.Auth.MeAsync(), ApiErrorKind.InvalidResponse);
|
||||
handler.Status = HttpStatusCode.Forbidden;
|
||||
handler.Json = """{"code":403,"msg":"无权限","data":null}""";
|
||||
await Error(async () => await api.Auth.MeAsync(), ApiErrorKind.Http);
|
||||
Check(api.Session.IsAuthenticated, "403 不应清除会话");
|
||||
handler.Status = HttpStatusCode.OK;
|
||||
handler.Json = """{"code":401,"msg":"已失效","data":null}""";
|
||||
await Error(async () => await api.Auth.MeAsync(), ApiErrorKind.Unauthorized);
|
||||
Check(!api.Session.IsAuthenticated, "业务401应清理会话");
|
||||
handler.Json = """{"code":200,"data":{"token":"session-two"}}""";
|
||||
await api.Auth.LoginAsync(new LoginRequest { UserName = "admin", Password = "secret" });
|
||||
handler.Status = HttpStatusCode.Unauthorized;
|
||||
await Error(async () => await api.Auth.MeAsync(), ApiErrorKind.Unauthorized);
|
||||
Check(!api.Session.IsAuthenticated, "HTTP401应清理会话");
|
||||
handler.Status = HttpStatusCode.OK;
|
||||
handler.Json = """{"code":200,"data":{"token":"session-three"}}""";
|
||||
await api.Auth.LoginAsync(new LoginRequest { UserName = "admin", Password = "secret" });
|
||||
handler.Json = """{"code":200,"data":null}""";
|
||||
await api.Auth.LogoutAsync();
|
||||
Check(!api.Session.IsAuthenticated && handler.Path.EndsWith("Auth/Logout"), "退出失败");
|
||||
using var cancelled = new CancellationTokenSource();
|
||||
cancelled.Cancel();
|
||||
try { await api.Auth.LoginAsync(new LoginRequest(), cancelled.Token); throw new Exception("取消未生效"); }
|
||||
catch (OperationCanceledException) { }
|
||||
Console.WriteLine("PASS: 登录、路由、Bearer、分页、校验、业务错误、格式错误、403、HTTP/业务401、退出、取消。");
|
||||
|
||||
sealed class StubHandler : HttpMessageHandler
|
||||
{
|
||||
public string Json { get; set; } = "";
|
||||
public HttpStatusCode Status { get; set; } = HttpStatusCode.OK;
|
||||
public int Count { get; private set; }
|
||||
public string Path { get; private set; } = "";
|
||||
public string? Authorization { get; private set; }
|
||||
public string? Body { get; private set; }
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
Count++;
|
||||
Path = request.RequestUri!.AbsolutePath;
|
||||
Authorization = request.Headers.Authorization?.ToString();
|
||||
Body = request.Content is null ? null : await request.Content.ReadAsStringAsync(cancellationToken);
|
||||
return new HttpResponseMessage(Status) { Content = new StringContent(Json, Encoding.UTF8, "application/json") };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings></PropertyGroup>
|
||||
<ItemGroup><ProjectReference Include="..\..\Source\Store.Management.Api\Store.Management.Api.csproj" /></ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using Store.Management;
|
||||
using Store.Management.Views;
|
||||
using Store.Management.ViewModels;
|
||||
using Store.Management.Framework.Controls;
|
||||
class Program
|
||||
{
|
||||
[STAThread]
|
||||
static int Main()
|
||||
{
|
||||
var app = new App();
|
||||
app.InitializeComponent();
|
||||
int exitCode = 0;
|
||||
app.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var page = (FrameworkTestPage)((MainWindow)app.MainWindow).Content;
|
||||
var vm = (FrameworkTestPageViewModel)page.DataContext;
|
||||
vm.TestIocCommand.Execute();
|
||||
Check(vm.IocResult.StartsWith("通过"), vm.IocResult);
|
||||
vm.ProductName = " ";
|
||||
await vm.PublishNotificationCommand.Execute();
|
||||
Check(vm.NotificationResult == "请输入商品名称。", "通知输入校验");
|
||||
vm.ProductName = "通知测试商品";
|
||||
await vm.PublishNotificationCommand.Execute();
|
||||
Check(vm.NotificationResult.StartsWith("通知处理完成")
|
||||
&& vm.NotificationResult.Contains("缓存模块")
|
||||
&& vm.NotificationResult.Contains("审计模块")
|
||||
&& vm.NotificationResult.Contains(vm.ProductName), vm.NotificationResult);
|
||||
vm.ShowModelessCommand.Execute();
|
||||
var dialog = app.Windows.OfType<DialogWindow>().Single();
|
||||
var dialogVm = (FrameworkTestDialogViewModel)dialog.DataContext;
|
||||
Check(dialogVm.Message == vm.DialogInput, "Dialog input");
|
||||
dialogVm.Message = " ";
|
||||
dialogVm.ConfirmCommand.Execute(null);
|
||||
Check(dialog.IsVisible && dialogVm.ErrorMsg.Length > 0, "Empty validation");
|
||||
dialogVm.Message = "返回测试";
|
||||
dialogVm.ConfirmCommand.Execute(null);
|
||||
Check(vm.DialogResult == "确认:返回测试", vm.DialogResult);
|
||||
vm.ShowModelessCommand.Execute();
|
||||
app.Windows.OfType<DialogWindow>().Single().Close();
|
||||
Check(vm.DialogResult.Contains("None"), vm.DialogResult);
|
||||
_ = app.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() =>
|
||||
((FrameworkTestDialogViewModel)app.Windows.OfType<DialogWindow>().Single().DataContext).CancelCommand.Execute(null)));
|
||||
vm.ShowModalCommand.Execute();
|
||||
Check(vm.DialogResult.Contains("Cancel"), vm.DialogResult);
|
||||
vm.DialogInput = " ";
|
||||
vm.ShowModalCommand.Execute();
|
||||
Check(!app.Windows.OfType<DialogWindow>().Any(), "Empty input blocks dialog");
|
||||
Console.WriteLine("PASS: startup, view model wiring, IoC, modeless/modal dialogs, parameters, validation, confirm/cancel/window-close.");
|
||||
}
|
||||
catch (Exception ex) { Console.Error.WriteLine(ex); exitCode = 1; }
|
||||
finally { app.Shutdown(); }
|
||||
}));
|
||||
app.Run();
|
||||
return exitCode;
|
||||
}
|
||||
static void Check(bool success, string message) { if (!success) throw new Exception(message); }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Source/Store.Management/Store.Management.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using Newtonsoft.Json;
|
||||
using Store.Management.Framework.Controls;
|
||||
using Store.Management.Framework.Converters;
|
||||
using Store.Management.Framework.Services;
|
||||
using Store.Management.Framework.Utilities;
|
||||
using Store.Management.Framework.Configs;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
{
|
||||
int count = 0;
|
||||
var cleanup = new DisposeAction(() => Interlocked.Increment(ref count));
|
||||
Parallel.For(0, 100, _ => cleanup.Dispose());
|
||||
Check(count == 1, "Concurrent cleanup");
|
||||
VerifyEvents().GetAwaiter().GetResult();
|
||||
string? crashPath = ApplicationLogService.WriteFatal(new InvalidOperationException("smoke-crash-marker"), "SmokeTest");
|
||||
Check(crashPath != null && File.ReadAllText(crashPath).Contains("smoke-crash-marker"), "Crash report without logger initialization");
|
||||
File.Delete(crashPath!);
|
||||
|
||||
Check(Path.IsPathFullyQualified(FolderConfig.ConfigFolder), "Absolute config directory");
|
||||
Check(FolderConfig.AppSettingsFilePath == Path.Combine(FolderConfig.ConfigFolder, "AppSettings.json"), "Settings location");
|
||||
Check(new DefaultPathSettings().FilePath == Path.Combine(FolderConfig.ConfigFolder, "SettingsModel.json"), "Default model location");
|
||||
foreach (string invalid in new[] { "../escape.json", "D:\\escape.json", "..", "config.json." })
|
||||
{
|
||||
try { FolderConfig.GetSettingsFilePath(invalid); throw new Exception("Expected invalid filename error"); }
|
||||
catch (ArgumentException) { }
|
||||
}
|
||||
|
||||
string directory = Path.Combine(Path.GetTempPath(), "StoreFramework-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(directory);
|
||||
try
|
||||
{
|
||||
string file = Path.Combine(directory, "settings.JSON");
|
||||
var settings = new TestSettings(file);
|
||||
settings.LoadSettings();
|
||||
Check(settings.Model.Value == 7, "Missing settings defaults");
|
||||
settings.Model.Value = 42;
|
||||
settings.SaveSettings();
|
||||
var reloaded = new TestSettings(file);
|
||||
reloaded.LoadSettingsAsync().GetAwaiter().GetResult();
|
||||
Check(reloaded.Model.Value == 42, "Settings roundtrip");
|
||||
Task.WhenAll(Enumerable.Range(0, 10).Select(_ => settings.SaveSettingsAsync())).GetAwaiter().GetResult();
|
||||
Check(Directory.GetFiles(directory, "*.tmp").Length == 0, "Temporary file cleanup");
|
||||
File.WriteAllText(file, "{broken");
|
||||
try { reloaded.LoadSettings(); throw new Exception("Expected JSON error"); }
|
||||
catch (JsonException) { }
|
||||
Check(reloaded.Model.Value == 42, "Corrupt settings preserve model");
|
||||
File.WriteAllText(file, "null");
|
||||
try { reloaded.LoadSettings(); throw new Exception("Expected null error"); }
|
||||
catch (JsonException) { }
|
||||
string[] args = [file];
|
||||
AppArgsService.Initialize(args);
|
||||
args[0] = "changed";
|
||||
Check(AppArgsService.GetArgs()[0] == file, "Arguments snapshot");
|
||||
Check(AppArgsService.GetFilePaths("json").Single() == file, "Extension case");
|
||||
Check(AppArgsService.GetFilePaths(".JSON").Single() == file, "Dotted extension");
|
||||
try { AppArgsService.GetFilePaths("."); throw new Exception("Expected invalid extension error"); }
|
||||
catch (ArgumentException) { }
|
||||
}
|
||||
finally { Directory.Delete(directory, recursive: true); }
|
||||
|
||||
var grid = new AutoColumnsUniformGrid { CellMinWidth = 200, MinColumns = 2, MaxColumns = 4 };
|
||||
grid.Measure(new Size(1000, 500));
|
||||
Check(grid.Columns == 4, "Maximum columns");
|
||||
grid.Measure(new Size(100, 500));
|
||||
Check(grid.Columns == 2, "Minimum columns");
|
||||
grid.MaxColumns = 1;
|
||||
grid.Measure(new Size(1000, 500));
|
||||
Check(grid.Columns == 2, "Conflicting bounds");
|
||||
try { grid.CellMinWidth = double.NaN; throw new Exception("Expected invalid width error"); }
|
||||
catch (ArgumentException) { }
|
||||
var converter = new StringNullOrEmpty2VisibilityConverter();
|
||||
Check(Equals(converter.Convert(null!, typeof(Visibility), null!, CultureInfo.InvariantCulture), Visibility.Visible), "Null placeholder");
|
||||
Check(Equals(converter.Convert("value", typeof(Visibility), null!, CultureInfo.InvariantCulture), Visibility.Collapsed), "Nonempty placeholder");
|
||||
Console.WriteLine("PASS: cleanup, async events, settings, arguments, WPF layout and converter boundaries.");
|
||||
}
|
||||
|
||||
private static async Task VerifyEvents()
|
||||
{
|
||||
var events = new AsyncEvent<EventArgs>();
|
||||
var calls = new List<int>();
|
||||
Func<object?, EventArgs, Task> second = (_, _) => { calls.Add(2); return Task.CompletedTask; };
|
||||
events.Subscribe(async (_, _) => { await Task.Yield(); calls.Add(1); events.Unsubscribe(second); });
|
||||
events.Subscribe(second);
|
||||
await events.InvokeAsync(null, EventArgs.Empty);
|
||||
Check(calls.SequenceEqual([1, 2]), "Event snapshot and order");
|
||||
calls.Clear();
|
||||
await events.InvokeAsync(null, EventArgs.Empty);
|
||||
Check(calls.SequenceEqual([1]), "Event unsubscribe");
|
||||
}
|
||||
|
||||
private static void Check(bool result, string message)
|
||||
{
|
||||
if (!result) throw new Exception(message);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SettingsModel { public int Value { get; set; } = 7; }
|
||||
internal sealed class TestSettings(string path) : JsonSettingsServiceBase<SettingsModel>
|
||||
{
|
||||
public override string FilePath => path;
|
||||
public override SettingsModel GetDefaultModel() => new();
|
||||
}
|
||||
|
||||
internal sealed class DefaultPathSettings : JsonSettingsServiceBase<SettingsModel>
|
||||
{
|
||||
public override SettingsModel GetDefaultModel() => new();
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Source/Store.Management.Framework/Store.Management.Framework.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in new issue
Block a user