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(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(() => 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(); 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(); 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(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(); 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(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(); 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(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(); var first = new TrackingOwnedService("first", log); var second = new TrackingOwnedService("second", log); var exception = Should.Throw(() => 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? configure = null, IReadOnlyList? 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 _log; internal TrackingOwnedService(string name, ICollection 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}"); } } } }