mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 14:37:19 +08:00
303 lines
10 KiB
C#
303 lines
10 KiB
C#
using AtomUI.Localization;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Media;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace AtomUI.Core.Tests.Localization;
|
|
|
|
public class ApplicationScopeTests
|
|
{
|
|
[Fact]
|
|
public void Scope_Mounts_One_Stable_Provider_And_Exposes_Exact_Service_Instances()
|
|
{
|
|
HeadlessTestApp.Run(() =>
|
|
{
|
|
var application = Application.Current!;
|
|
using var scope = CreateScope(application);
|
|
|
|
scope.InitializeApplication();
|
|
|
|
application.Resources.MergedDictionaries
|
|
.Count(provider => ReferenceEquals(provider, scope.ResourceProvider))
|
|
.ShouldBe(1);
|
|
scope.ResourceProvider.Owner.ShouldBeSameAs(application);
|
|
ApplicationScopeRegistry.Get(application).ShouldBeSameAs(scope);
|
|
scope.LanguageManager.ShouldBeSameAs(scope.LocalizationHost.LanguageManager);
|
|
scope.Localizer.ShouldBeSameAs(scope.LocalizationHost.Localizer);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Scope_Projects_FlowDirection_Through_The_Same_Language_Provider()
|
|
{
|
|
HeadlessTestApp.Run(() =>
|
|
{
|
|
var application = Application.Current!;
|
|
using var scope = CreateScope(
|
|
application,
|
|
builder => builder.UseLanguages(
|
|
LanguageTags.ArSA,
|
|
[LanguageTags.ArSA, LanguageTags.EnUS]));
|
|
scope.InitializeApplication();
|
|
var provider = scope.ResourceProvider;
|
|
var window = new Window
|
|
{
|
|
Content = new Border()
|
|
};
|
|
window.Show();
|
|
|
|
window.FlowDirection.ShouldBe(FlowDirection.RightToLeft);
|
|
scope.LanguageManager.ChangeLanguage(LanguageTags.EnUS);
|
|
|
|
scope.ResourceProvider.ShouldBeSameAs(provider);
|
|
window.FlowDirection.ShouldBe(FlowDirection.LeftToRight);
|
|
window.Close();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Scope_Rejects_Duplicate_Initialization_For_The_Same_Application()
|
|
{
|
|
HeadlessTestApp.Run(() =>
|
|
{
|
|
var application = Application.Current!;
|
|
using var first = CreateScope(application);
|
|
using var second = CreateScope(application);
|
|
first.InitializeApplication();
|
|
|
|
var exception = Should.Throw<InvalidOperationException>(second.InitializeApplication);
|
|
|
|
exception.Message.ShouldContain("already initialized");
|
|
application.Resources.MergedDictionaries
|
|
.Count(provider => ReferenceEquals(provider, second.ResourceProvider))
|
|
.ShouldBe(0);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispose_Unmounts_Application_Resources_Styles_And_Services()
|
|
{
|
|
HeadlessTestApp.Run(() =>
|
|
{
|
|
var application = Application.Current!;
|
|
var scope = CreateScope(application);
|
|
scope.InitializeApplication();
|
|
var provider = scope.ResourceProvider;
|
|
var flowDirectionStyle = scope.FlowDirectionStyle;
|
|
|
|
scope.Dispose();
|
|
|
|
application.Resources.MergedDictionaries.Contains(provider).ShouldBeFalse();
|
|
application.Styles.Contains(flowDirectionStyle).ShouldBeFalse();
|
|
ApplicationScopeRegistry.Get(application).ShouldBeNull();
|
|
Should.Throw<InvalidOperationException>(() =>
|
|
scope.LanguageManager.ChangeLanguage(LanguageTags.EnUS));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Owned_Services_Attach_In_Registration_Order_And_Cleanup_In_Reverse_Order()
|
|
{
|
|
HeadlessTestApp.Run(() =>
|
|
{
|
|
var application = Application.Current!;
|
|
var log = new List<string>();
|
|
var first = new TrackingOwnedService("first", log);
|
|
var second = new TrackingOwnedService("second", log);
|
|
var scope = CreateScope(application, ownedServices: [first, second]);
|
|
|
|
scope.InitializeApplication();
|
|
scope.Dispose();
|
|
|
|
log.ShouldBe([
|
|
"attach:first",
|
|
"attach:second",
|
|
"detach:second",
|
|
"detach:first",
|
|
"dispose:second",
|
|
"dispose:first"
|
|
]);
|
|
ApplicationScopeRegistry.Get(application).ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Owned_Service_Attach_Failure_Rolls_Back_Only_Successfully_Attached_Services()
|
|
{
|
|
HeadlessTestApp.Run(() =>
|
|
{
|
|
var application = Application.Current!;
|
|
var log = new List<string>();
|
|
var first = new TrackingOwnedService("first", log);
|
|
var second = new TrackingOwnedService("second", log) { ThrowOnAttach = true };
|
|
var third = new TrackingOwnedService("third", log);
|
|
var scope = CreateScope(application, ownedServices: [first, second, third]);
|
|
|
|
Should.Throw<InvalidOperationException>(scope.InitializeApplication)
|
|
.Message.ShouldContain("attach:second");
|
|
|
|
log.ShouldBe([
|
|
"attach:first",
|
|
"attach:second",
|
|
"detach:first"
|
|
]);
|
|
ApplicationScopeRegistry.Get(application).ShouldBeNull();
|
|
application.Resources.MergedDictionaries.Contains(scope.ResourceProvider).ShouldBeFalse();
|
|
application.Styles.Contains(scope.FlowDirectionStyle).ShouldBeFalse();
|
|
|
|
scope.Dispose();
|
|
log.ShouldBe([
|
|
"attach:first",
|
|
"attach:second",
|
|
"detach:first",
|
|
"dispose:third",
|
|
"dispose:second",
|
|
"dispose:first"
|
|
]);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispose_Continues_After_Detach_And_Dispose_Failures()
|
|
{
|
|
HeadlessTestApp.Run(() =>
|
|
{
|
|
var application = Application.Current!;
|
|
var log = new List<string>();
|
|
var first = new TrackingOwnedService("first", log) { ThrowOnDetach = true };
|
|
var second = new TrackingOwnedService("second", log) { ThrowOnDispose = true };
|
|
var scope = CreateScope(application, ownedServices: [first, second]);
|
|
scope.InitializeApplication();
|
|
|
|
var exception = Should.Throw<AggregateException>(scope.Dispose);
|
|
|
|
exception.InnerExceptions.Count.ShouldBe(2);
|
|
log.ShouldBe([
|
|
"attach:first",
|
|
"attach:second",
|
|
"detach:second",
|
|
"detach:first",
|
|
"dispose:second",
|
|
"dispose:first"
|
|
]);
|
|
ApplicationScopeRegistry.Get(application).ShouldBeNull();
|
|
application.Resources.MergedDictionaries.Contains(scope.ResourceProvider).ShouldBeFalse();
|
|
application.Styles.Contains(scope.FlowDirectionStyle).ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Initialize_Aggregates_Attach_And_Rollback_Failures()
|
|
{
|
|
HeadlessTestApp.Run(() =>
|
|
{
|
|
var application = Application.Current!;
|
|
var log = new List<string>();
|
|
var first = new TrackingOwnedService("first", log) { ThrowOnDetach = true };
|
|
var second = new TrackingOwnedService("second", log) { ThrowOnAttach = true };
|
|
var scope = CreateScope(application, ownedServices: [first, second]);
|
|
|
|
var exception = Should.Throw<AggregateException>(scope.InitializeApplication);
|
|
|
|
exception.InnerExceptions.Count.ShouldBe(2);
|
|
log.ShouldBe([
|
|
"attach:first",
|
|
"attach:second",
|
|
"detach:first"
|
|
]);
|
|
ApplicationScopeRegistry.Get(application).ShouldBeNull();
|
|
scope.Dispose();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Owned_Service_Factory_Failure_Disposes_Previously_Created_Services_In_Reverse_Order()
|
|
{
|
|
HeadlessTestApp.Run(() =>
|
|
{
|
|
var application = Application.Current!;
|
|
var log = new List<string>();
|
|
var first = new TrackingOwnedService("first", log);
|
|
var second = new TrackingOwnedService("second", log);
|
|
|
|
var exception = Should.Throw<InvalidOperationException>(() => application.UseAtomUI(builder =>
|
|
{
|
|
var atomBuilder = (AtomUIBuilder)builder;
|
|
atomBuilder.AddOwnedService("first", _ => first);
|
|
atomBuilder.AddOwnedService("second", _ => second);
|
|
atomBuilder.AddOwnedService(
|
|
"failure",
|
|
_ => throw new InvalidOperationException("factory:failure"));
|
|
}));
|
|
|
|
exception.Message.ShouldBe("factory:failure");
|
|
log.ShouldBe([
|
|
"dispose:second",
|
|
"dispose:first"
|
|
]);
|
|
ApplicationScopeRegistry.Get(application).ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
private static ApplicationScope CreateScope(
|
|
Application application,
|
|
Action<IAtomUIBuilder>? configure = null,
|
|
IReadOnlyList<IAtomUIOwnedService>? ownedServices = null)
|
|
{
|
|
var builder = new AtomUIBuilder(application);
|
|
configure?.Invoke(builder);
|
|
return new ApplicationScope(
|
|
application,
|
|
builder.ThemeManagerBuilder.Build(),
|
|
builder.LocalizationBuilder.Build(),
|
|
ownedServices ?? []);
|
|
}
|
|
|
|
private sealed class TrackingOwnedService : IAtomUIOwnedService
|
|
{
|
|
private readonly string _name;
|
|
private readonly ICollection<string> _log;
|
|
|
|
internal TrackingOwnedService(string name, ICollection<string> log)
|
|
{
|
|
_name = name;
|
|
_log = log;
|
|
}
|
|
|
|
internal bool ThrowOnAttach { get; init; }
|
|
|
|
internal bool ThrowOnDetach { get; init; }
|
|
|
|
internal bool ThrowOnDispose { get; init; }
|
|
|
|
public void Attach(Application application)
|
|
{
|
|
_log.Add($"attach:{_name}");
|
|
if (ThrowOnAttach)
|
|
{
|
|
throw new InvalidOperationException($"attach:{_name}");
|
|
}
|
|
}
|
|
|
|
public void Detach(Application application)
|
|
{
|
|
_log.Add($"detach:{_name}");
|
|
if (ThrowOnDetach)
|
|
{
|
|
throw new InvalidOperationException($"detach:{_name}");
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_log.Add($"dispose:{_name}");
|
|
if (ThrowOnDispose)
|
|
{
|
|
throw new InvalidOperationException($"dispose:{_name}");
|
|
}
|
|
}
|
|
}
|
|
}
|