using System; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Controls; using Avalonia.Threading; using System.Reactive.Disposables; using System.Threading; using Avalonia.Input.Platform; using Avalonia.Animation; using Avalonia.Media; namespace Avalonia.UnitTests { public class UnitTestApplication : Application { private readonly TestServices _services; public UnitTestApplication() : this(null) { } public UnitTestApplication(TestServices? services) { _services = services ?? new TestServices(); AvaloniaLocator.CurrentMutable.BindToSelf(this); RegisterServices(); } static UnitTestApplication() { AssetLoader.RegisterResUriParsers(); } public static new UnitTestApplication Current => (UnitTestApplication)Application.Current!; public TestServices Services => _services; public static IDisposable Start(TestServices? services = null) { var scope = AvaloniaLocator.EnterScope(); var oldContext = SynchronizationContext.Current; _ = new UnitTestApplication(services); Dispatcher.ResetBeforeUnitTests(); return Disposable.Create(() => { if (Dispatcher.UIThread.CheckAccess()) { Dispatcher.UIThread.RunJobs(); } (AvaloniaLocator.Current.GetService() as ToolTipService)?.Dispose(); (AvaloniaLocator.Current.GetService() as IDisposable)?.Dispose(); Dispatcher.ResetForUnitTests(); scope.Dispose(); Dispatcher.ResetBeforeUnitTests(); SynchronizationContext.SetSynchronizationContext(oldContext); }); } public override void RegisterServices() { // Arrange (as part of layouting) calls TaskScheduler.FromCurrentSynchronizationContext, which needs a non-null context. // If it's null, it will fail. So we need to ensure it's not null. // Historically, this line wasn't needed because xunit itself used to always install its own SynchronizationContext. // This was changed in xunit.v3. if (SynchronizationContext.Current is null) { SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); } AvaloniaLocator.CurrentMutable .Bind().ToConstant(Services.AssetLoader) .Bind().ToConstant(Services.GlobalClock) .BindToSelf(this) .Bind().ToConstant(Services.InputManager) .Bind().ToConstant(Services.InputManager == null ? null : new ToolTipService(Services.InputManager)) .Bind().ToConstant(Services.KeyboardDevice?.Invoke()) .Bind().ToConstant(Services.MouseDevice?.Invoke()) .Bind().ToFunc(Services.KeyboardNavigation ?? (() => null)) .Bind().ToConstant(Services.Platform) .Bind().ToConstant(Services.RenderInterface) .Bind().ToConstant(Services.FontManagerImpl) .Bind().ToConstant(Services.TextShaperImpl) .Bind().ToConstant(Services.DispatcherImpl) .Bind().ToConstant(Services.StandardCursorFactory) .Bind().ToConstant(Services.WindowingPlatform) .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(Services.AccessKeyHandler) ; // This is a hack to make tests work, we need to refactor the way font manager is registered // See https://github.com/AvaloniaUI/Avalonia/issues/10081 AvaloniaLocator.CurrentMutable.Bind().ToConstant((FontManager)null!); var theme = Services.Theme?.Invoke(); if (theme is Style styles) { Styles.AddRange(styles.Children); } else if (theme is not null) { Styles.Add(theme); } } } }