using System.Runtime.CompilerServices; using AtomUI.Controls.Primitives; using Avalonia; using Avalonia.Controls; using Avalonia.Headless; using Avalonia.Threading; using Avalonia.VisualTree; using Shouldly; using Xunit; namespace AtomUI.Desktop.Controls.Tests.Dialog; [Collection(DialogLifecycleTestCollection.Name)] public class DialogLifecycleTests { static DialogLifecycleTests() { AvaloniaTestApp.EnsureInitialized(); } [Fact] public void Dialog_Default_Placement_Matches_DialogOptions() { var dialog = new AtomUI.Desktop.Controls.Dialog(); var options = new DialogOptions(); dialog.HorizontalStartupLocation.ShouldBe(options.HorizontalStartupLocation); dialog.VerticalStartupLocation.ShouldBe(options.VerticalStartupLocation); dialog.HorizontalStartupLocation.ShouldBe(DialogHorizontalAnchor.Center); dialog.VerticalStartupLocation.ShouldBe(DialogVerticalAnchor.Center); dialog.CalculatePlacementOffset(new Size(320, 180), new Size(640, 480)) .ShouldBe(new Point(160, 150)); } [Fact] public void DialogOptions_Defaults_Match_The_Static_Dialog_Defaults() { var options = new DialogOptions(); options.DefaultStandardButton.ShouldBe(DialogStandardButton.Ok); options.HorizontalStartupLocation.ShouldBe(DialogHorizontalAnchor.Center); options.VerticalStartupLocation.ShouldBe(DialogVerticalAnchor.Center); } [Fact] public void OpenAsync_Represents_The_Complete_Modeless_Session() { RunOnUIThread(OpenAsync_Represents_The_Complete_Modeless_Session_Core); } [Fact] public void OpenAsync_From_A_Background_Thread_Waits_For_The_Complete_Session() { RunOnUIThread(() => { var placementTarget = new Border { Width = 100, Height = 40 }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget } }; var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = root }; var dialog = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = placementTarget, IsMotionEnabled = false }; try { window.Show(); var sessionTask = Task.Run(() => dialog.OpenAsync(TestContext.Current.CancellationToken)); PumpUntil(() => dialog.IsOpen && window.GetVisualDescendants().OfType().Any()); sessionTask.IsCompleted.ShouldBeFalse(); dialog.Done(); WaitWithDispatcherPump(sessionTask); } finally { window.Close(); } }); } [Fact] public void Programmatic_Close_From_A_Background_Thread_Runs_On_The_UI_Dispatcher() { RunOnUIThread(() => { var placementTarget = new Border { Width = 100, Height = 40 }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget } }; var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = root }; var dialog = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = placementTarget, IsMotionEnabled = false }; var opened = false; bool? closingOnUiThread = null; dialog.Opened += (_, _) => opened = true; dialog.Closing += (_, _) => closingOnUiThread = Dispatcher.UIThread.CheckAccess(); try { window.Show(); var sessionTask = dialog.OpenAsync(TestContext.Current.CancellationToken); PumpUntil(() => opened); WaitWithDispatcherPump(Task.Run(dialog.Accept)); WaitWithDispatcherPump(sessionTask); closingOnUiThread.ShouldBe(true); dialog.Result.ShouldBe(DialogCode.Accepted); } finally { window.Close(); } }); } [Fact] public void Static_Dialog_From_A_Background_Thread_Uses_The_UI_Dispatcher() { RunOnUIThread(() => { var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = new ScopeAwareOverlayLayerPanel() }; try { window.Show(); var content = new TextBlock { Text = "Background" }; var sessionTask = Task.Run(() => AtomUI.Desktop.Controls.Dialog.ShowDialogAsync( content, options: new DialogOptions { StandardButtons = DialogStandardButton.Ok, IsMotionEnabled = false }, topLevel: window, cancellationToken: TestContext.Current.CancellationToken)); PumpUntil(() => sessionTask.IsCompleted || window.GetVisualDescendants() .OfType() .Any(button => button.StandardButtonType == DialogStandardButton.Ok)); sessionTask.IsCompleted.ShouldBeFalse(); window.GetVisualDescendants() .OfType() .Single(button => button.StandardButtonType == DialogStandardButton.Ok) .RaiseEvent(new Avalonia.Interactivity.RoutedEventArgs( Avalonia.Controls.Button.ClickEvent)); WaitWithDispatcherPump(sessionTask); sessionTask.Result.ShouldBe(DialogCode.Accepted); } finally { window.Close(); } }); } [Fact] public void Generic_Static_Dialog_From_A_Background_Thread_Constructs_The_View_On_The_UI_Dispatcher() { RunOnUIThread(() => { var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = new ScopeAwareOverlayLayerPanel() }; try { window.Show(); var sessionTask = Task.Run(() => AtomUI.Desktop.Controls.Dialog.ShowDialogAsync( null, new DialogOptions { StandardButtons = DialogStandardButton.Ok, IsMotionEnabled = false }, window, TestContext.Current.CancellationToken)); PumpUntil(() => sessionTask.IsCompleted || window.GetVisualDescendants() .OfType() .Any(button => button.StandardButtonType == DialogStandardButton.Ok)); sessionTask.IsCompleted.ShouldBeFalse(); window.GetVisualDescendants() .OfType() .Single(button => button.StandardButtonType == DialogStandardButton.Ok) .RaiseEvent(new Avalonia.Interactivity.RoutedEventArgs( Avalonia.Controls.Button.ClickEvent)); WaitWithDispatcherPump(sessionTask); sessionTask.Result.ShouldBe(DialogCode.Accepted); } finally { window.Close(); } }); } private static void OpenAsync_Represents_The_Complete_Modeless_Session_Core() { var placementTarget = new Border { Width = 100, Height = 40 }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget } }; var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = root }; var dialog = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = placementTarget, Content = "Dialog", IsModal = false, IsMotionEnabled = false, StandardButtons = DialogStandardButton.Ok, HostWidth = 320, HostHeight = 180 }; var events = new List(); dialog.Opened += (_, _) => events.Add("Opened"); dialog.Closing += (_, _) => events.Add("Closing"); dialog.Accepted += (_, _) => events.Add("Accepted"); dialog.Finished += (_, _) => events.Add("Finished"); dialog.Closed += (_, _) => events.Add("Closed"); try { window.Show(); var sessionTask = dialog.OpenAsync(TestContext.Current.CancellationToken); Dispatcher.UIThread.RunJobs(); dialog.IsOpen.ShouldBeTrue(); sessionTask.IsCompleted.ShouldBeFalse(); events.ShouldBe(new[] { "Opened" }); dialog.Accept(); WaitWithDispatcherPump(sessionTask); dialog.Result.ShouldBe(DialogCode.Accepted); dialog.IsOpen.ShouldBeFalse(); events.ShouldBe(new[] { "Opened", "Closing", "Accepted", "Finished", "Closed" }); } finally { window.Close(); } } [Fact] public void Placement_Target_Detach_Forces_The_Active_Session_To_Tear_Down() { RunOnUIThread(Placement_Target_Detach_Forces_The_Active_Session_To_Tear_Down_Core); } private static void Placement_Target_Detach_Forces_The_Active_Session_To_Tear_Down_Core() { var placementTarget = new Border { Width = 100, Height = 40 }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget } }; var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = root }; var beforeCloseCallCount = 0; var closingCallCount = 0; var dialog = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = placementTarget, Content = "Dialog", IsModal = false, IsMotionEnabled = false, BeforeCloseAsync = _ => { beforeCloseCallCount++; return ValueTask.FromResult(false); } }; dialog.Closing += (_, e) => { closingCallCount++; e.Cancel = true; }; try { window.Show(); var sessionTask = dialog.OpenAsync(TestContext.Current.CancellationToken); Dispatcher.UIThread.RunJobs(); root.Children.Remove(placementTarget); WaitWithDispatcherPump(sessionTask); closingCallCount.ShouldBe(1); beforeCloseCallCount.ShouldBe(0); dialog.IsOpen.ShouldBeFalse(); window.GetVisualDescendants().OfType().ShouldBeEmpty(); } finally { window.Close(); } } [Fact] public void IsOpen_False_During_Opening_Cannot_Reopen_A_Stale_Session() { RunOnUIThread(() => { var placementTarget = new Border { Width = 100, Height = 40 }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget } }; var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = root }; var dialog = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = placementTarget, IsMotionEnabled = true, HostWidth = 320, HostHeight = 180 }; var openedCount = 0; var closedCount = 0; dialog.Opened += (_, _) => openedCount++; dialog.Closed += (_, _) => closedCount++; try { window.Show(); dialog.IsOpen = true; Dispatcher.UIThread.RunJobs(); window.GetVisualDescendants().OfType().ShouldHaveSingleItem(); dialog.IsOpen = false; PumpUntil(() => !window.GetVisualDescendants().OfType().Any()); dialog.IsOpen.ShouldBeFalse(); openedCount.ShouldBe(0); closedCount.ShouldBe(1); Dispatcher.UIThread.RunJobs(); window.GetVisualDescendants().OfType().ShouldBeEmpty(); } finally { window.Close(); } }); } [Fact] public void IsOpen_True_During_Closing_Starts_A_New_Session_After_Teardown() { RunOnUIThread(() => { var placementTarget = new Border { Width = 100, Height = 40 }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget } }; var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = root }; var dialog = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = placementTarget, IsMotionEnabled = false, HostWidth = 320, HostHeight = 180 }; var openedCount = 0; var closedCount = 0; dialog.Opened += (_, _) => openedCount++; dialog.Closed += (_, _) => closedCount++; try { window.Show(); dialog.IsOpen = true; PumpUntil(() => openedCount == 1); var firstPresenter = window.GetVisualDescendants() .OfType() .ShouldHaveSingleItem(); firstPresenter.IsMotionEnabled = true; firstPresenter.MotionDuration = TimeSpan.FromMilliseconds(80); dialog.IsOpen = false; Dispatcher.UIThread.RunJobs(); firstPresenter.Parent.ShouldBeOfType(); dialog.IsOpen = true; PumpUntil(() => openedCount == 2); dialog.IsOpen.ShouldBeTrue(); closedCount.ShouldBe(1); window.GetVisualDescendants() .OfType() .ShouldHaveSingleItem() .ShouldNotBeSameAs(firstPresenter); dialog.IsOpen = false; PumpUntil(() => !window.GetVisualDescendants().OfType().Any()); } finally { window.Close(); } }); } [Fact] public void OpenAsync_Rejects_An_Existing_Active_Session() { RunOnUIThread(() => { var placementTarget = new Border { Width = 100, Height = 40 }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget } }; var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = root }; var dialog = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = placementTarget, IsMotionEnabled = false }; var openedCount = 0; dialog.Opened += (_, _) => openedCount++; try { window.Show(); dialog.IsOpen = true; PumpUntil(() => openedCount == 1); Should.Throw(() => WaitWithDispatcherPump(dialog.OpenAsync(TestContext.Current.CancellationToken))); var firstPresenter = window.GetVisualDescendants() .OfType() .ShouldHaveSingleItem(); firstPresenter.IsMotionEnabled = true; firstPresenter.MotionDuration = TimeSpan.FromMilliseconds(80); dialog.IsOpen = false; Dispatcher.UIThread.RunJobs(); Should.Throw(() => WaitWithDispatcherPump(dialog.OpenAsync(TestContext.Current.CancellationToken))); PumpUntil(() => !window.GetVisualDescendants().OfType().Any()); openedCount.ShouldBe(1); } finally { window.Close(); } }); } [Fact] public void IsOpen_True_Before_Attach_Defers_Opening_Until_A_Placement_Target_Is_Available() { RunOnUIThread(() => { var dialog = new AtomUI.Desktop.Controls.Dialog { IsOpen = true, IsMotionEnabled = false }; Dispatcher.UIThread.RunJobs(); dialog.IsOpen.ShouldBeTrue(); var placementTarget = new Border { Width = 80, Height = 32 }; var root = new ScopeAwareOverlayLayerPanel { Width = 480, Height = 360, Children = { placementTarget, dialog } }; var window = new AtomUI.Desktop.Controls.Window { Width = 480, Height = 360, Content = root }; try { window.Show(); PumpUntil(() => window.GetVisualDescendants().OfType().Any()); dialog.IsOpen.ShouldBeTrue(); dialog.Done(); PumpUntil(() => !dialog.IsOpen); } finally { window.Close(); } }); } [Fact] public void OpenAsync_Without_An_Attached_Placement_Target_Fails_Explicitly() { RunOnUIThread(() => { var dialog = new AtomUI.Desktop.Controls.Dialog { IsMotionEnabled = false }; var exception = Should.Throw(() => WaitWithDispatcherPump(dialog.OpenAsync(TestContext.Current.CancellationToken))); exception.Message.ShouldContain("placement target"); dialog.IsOpen.ShouldBeFalse(); }); } [Fact] public void Detaching_The_Dialog_Forces_The_Active_Session_To_Tear_Down() { RunOnUIThread(() => { var placementTarget = new Border { Width = 80, Height = 32 }; var dialog = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = placementTarget, IsMotionEnabled = false }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget, dialog } }; var window = new AtomUI.Desktop.Controls.Window { Width = 480, Height = 360, Content = root }; var closedCount = 0; dialog.Closed += (_, _) => closedCount++; try { window.Show(); dialog.IsOpen = true; PumpUntil(() => window.GetVisualDescendants().OfType().Any()); root.Children.Remove(dialog); PumpUntil(() => !window.GetVisualDescendants().OfType().Any()); dialog.IsOpen.ShouldBeFalse(); closedCount.ShouldBe(1); } finally { window.Close(); } }); } [Fact] public void Declarative_Close_Policy_Failure_Is_Reported_Through_The_Dispatcher() { RunOnUIThread(() => { var expectedException = new InvalidOperationException("policy failed"); var placementTarget = new Border { Width = 80, Height = 32 }; var dialog = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = placementTarget, IsMotionEnabled = false, BeforeCloseAsync = _ => ValueTask.FromException(expectedException) }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget, dialog } }; var window = new AtomUI.Desktop.Controls.Window { Width = 480, Height = 360, Content = root }; Exception? reportedException = null; void HandleUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e) { reportedException = e.Exception; e.Handled = true; } Dispatcher.UIThread.UnhandledException += HandleUnhandledException; try { window.Show(); dialog.IsOpen = true; PumpUntil(() => window.GetVisualDescendants().OfType().Any()); dialog.Accept(); PumpUntil(() => reportedException is not null); reportedException.ShouldBeSameAs(expectedException); dialog.IsOpen.ShouldBeTrue(); dialog.BeforeCloseAsync = null; dialog.Done(); PumpUntil(() => !dialog.IsOpen); } finally { Dispatcher.UIThread.UnhandledException -= HandleUnhandledException; window.Close(); } }); } [Fact] public void Latest_Open_Intent_Is_Reconciled_After_A_Faulted_Teardown() { RunOnUIThread(() => { var expectedException = new InvalidOperationException("closed failed"); var placementTarget = new Border { Width = 80, Height = 32 }; var dialog = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = placementTarget, IsMotionEnabled = true }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget, dialog } }; var window = new AtomUI.Desktop.Controls.Window { Width = 480, Height = 360, Content = root }; var openedCount = 0; var isFirstSession = true; Exception? reportedException = null; dialog.Opened += (_, _) => openedCount++; dialog.Finished += (_, _) => { if (isFirstSession) { dialog.IsOpen = true; } }; dialog.Closed += (_, _) => { if (isFirstSession) { isFirstSession = false; throw expectedException; } }; void HandleUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e) { reportedException = e.Exception; e.Handled = true; } Dispatcher.UIThread.UnhandledException += HandleUnhandledException; try { window.Show(); dialog.IsOpen = true; PumpUntil(() => openedCount == 1); var firstPresenter = window.GetVisualDescendants() .OfType() .ShouldHaveSingleItem(); firstPresenter.MotionDuration = TimeSpan.FromMilliseconds(80); dialog.Accept(); PumpUntil(() => reportedException is not null && openedCount == 2); reportedException.ShouldBeSameAs(expectedException); dialog.IsOpen.ShouldBeTrue(); dialog.Done(); PumpUntil(() => !dialog.IsOpen); } finally { Dispatcher.UIThread.UnhandledException -= HandleUnhandledException; window.Close(); } }); } [Fact] public void Nested_Dialogs_Restore_The_Lower_Surface_Then_The_Original_Focus() { RunOnUIThread(() => { var trigger = new Border { Width = 100, Height = 40, Focusable = true }; var first = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = trigger, Content = "First", IsMotionEnabled = false, HostWidth = 320, HostHeight = 180 }; var second = new AtomUI.Desktop.Controls.Dialog { PlacementTarget = trigger, Content = "Second", IsMotionEnabled = false, HostWidth = 320, HostHeight = 180 }; var firstOpened = false; var secondOpened = false; first.Opened += (_, _) => firstOpened = true; second.Opened += (_, _) => secondOpened = true; var root = new ScopeAwareOverlayLayerPanel { Children = { trigger, first, second } }; var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = root }; try { window.Show(); trigger.Focus().ShouldBeTrue(); var firstTask = first.OpenAsync(TestContext.Current.CancellationToken); PumpUntil(() => firstOpened); var firstPresenter = window.GetVisualDescendants() .OfType() .Single(); var firstSurface = firstPresenter.Surface; window.FocusManager.GetFocusedElement().ShouldBeSameAs(firstSurface); var secondTask = second.OpenAsync(TestContext.Current.CancellationToken); PumpUntil(() => secondOpened); var secondSurface = window.GetVisualDescendants() .OfType() .Single(presenter => !ReferenceEquals(presenter, firstPresenter)) .Surface; window.FocusManager.GetFocusedElement().ShouldBeSameAs(secondSurface); second.Done(); WaitWithDispatcherPump(secondTask); window.FocusManager.GetFocusedElement().ShouldBeSameAs(firstSurface); first.Done(); WaitWithDispatcherPump(firstTask); window.FocusManager.GetFocusedElement().ShouldBeSameAs(trigger); } finally { window.Close(); } }); } [Fact] public void Closed_Overlay_Session_Releases_Presenter_Surface_And_Content() { ClosedSessionReferences? references = null; RunOnUIThread(() => references = CreateClosedSessionReferences()); CollectGarbage(); references.ShouldNotBeNull(); references.Session.IsAlive.ShouldBeFalse(); references.Presenter.IsAlive.ShouldBeFalse(); references.Surface.IsAlive.ShouldBeFalse(); references.Content.IsAlive.ShouldBeFalse(); } [Fact] public void Closed_Overlay_Session_Releases_Surface_When_A_Custom_Button_Is_Retained() { RetainedButtonSessionReferences? references = null; RunOnUIThread(() => references = CreateRetainedButtonSessionReferences()); CollectGarbage(); references.ShouldNotBeNull(); references.Button.ShouldNotBeNull(); references.Presenter.IsAlive.ShouldBeFalse(); references.Surface.IsAlive.ShouldBeFalse(); } [Fact] public void Closed_Window_Session_Releases_Presenter_Surface_And_Content() { ClosedSessionReferences? references = null; RunOnUIThread(() => references = CreateClosedWindowSessionReferences()); CollectGarbage(); references.ShouldNotBeNull(); references.Session.IsAlive.ShouldBeFalse(); references.Presenter.IsAlive.ShouldBeFalse(); references.Surface.IsAlive.ShouldBeFalse(); references.Content.IsAlive.ShouldBeFalse(); references.RetainedButton.ShouldNotBeNull(); } [MethodImpl(MethodImplOptions.NoInlining)] private static ClosedSessionReferences CreateClosedSessionReferences() { var placementTarget = new Border { Width = 100, Height = 40 }; var content = new Border { Width = 180, Height = 70 }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget } }; var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = root }; var dialog = new AtomUI.Desktop.Controls.Dialog { Content = content, IsMotionEnabled = false, HostWidth = 320, HostHeight = 180 }; var presenter = new OverlayDialogPresenter(dialog, placementTarget); var session = new DialogSession( dialog, presenter, CancellationToken.None, placementTarget, window); var references = new ClosedSessionReferences( new WeakReference(session), new WeakReference(presenter), new WeakReference(presenter.Surface), new WeakReference(content), null); window.Show(); var sessionTask = session.RunAsync(); PumpUntil(() => session.State == DialogSessionState.Open); WaitWithDispatcherPump(session.RequestCloseAsync( new DialogCloseRequest(null, DialogCloseReason.Programmatic, null)).AsTask()); WaitWithDispatcherPump(sessionTask); WaitWithDispatcherPump(session.DisposeAsync().AsTask()); AvaloniaHeadlessPlatform.ForceRenderTimerTick(1); Dispatcher.UIThread.RunJobs(); window.Close(); return references; } [MethodImpl(MethodImplOptions.NoInlining)] private static RetainedButtonSessionReferences CreateRetainedButtonSessionReferences() { var placementTarget = new Border { Width = 100, Height = 40 }; var customButton = new DialogButton { Content = "Retained", Role = DialogButtonRole.CustomRole }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget } }; var window = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = root }; var dialog = new AtomUI.Desktop.Controls.Dialog { IsMotionEnabled = false, HostWidth = 320, HostHeight = 180 }; dialog.CustomButtons.Add(customButton); var presenter = new OverlayDialogPresenter(dialog, placementTarget); var session = new DialogSession( dialog, presenter, CancellationToken.None, placementTarget, window); var references = new RetainedButtonSessionReferences( customButton, new WeakReference(presenter), new WeakReference(presenter.Surface)); window.Show(); var sessionTask = session.RunAsync(); PumpUntil(() => session.State == DialogSessionState.Open); WaitWithDispatcherPump(session.RequestCloseAsync( new DialogCloseRequest(null, DialogCloseReason.Programmatic, null)).AsTask()); WaitWithDispatcherPump(sessionTask); AvaloniaHeadlessPlatform.ForceRenderTimerTick(1); Dispatcher.UIThread.RunJobs(); window.Close(); return references; } [MethodImpl(MethodImplOptions.NoInlining)] private static ClosedSessionReferences CreateClosedWindowSessionReferences() { var placementTarget = new Border { Width = 100, Height = 40 }; var content = new Border { Width = 180, Height = 70 }; var root = new ScopeAwareOverlayLayerPanel { Children = { placementTarget } }; var owner = new AtomUI.Desktop.Controls.Window { Width = 640, Height = 480, Content = root }; var dialog = new AtomUI.Desktop.Controls.Dialog { Content = content, IsModal = false, IsMotionEnabled = false, HostWidth = 320, HostHeight = 180 }; var retainedButton = new DialogButton { Content = "Retained", Role = DialogButtonRole.CustomRole }; dialog.CustomButtons.Add(retainedButton); var presenter = new WindowDialogPresenter(dialog, owner); var session = new DialogSession( dialog, presenter, CancellationToken.None, placementTarget, owner); var references = new ClosedSessionReferences( new WeakReference(session), new WeakReference(presenter), new WeakReference(presenter.FocusScope), new WeakReference(content), retainedButton); owner.Show(); var sessionTask = session.RunAsync(); PumpUntil(() => session.State == DialogSessionState.Open); WaitWithDispatcherPump(session.RequestCloseAsync( new DialogCloseRequest(null, DialogCloseReason.Programmatic, null)).AsTask()); WaitWithDispatcherPump(sessionTask); WaitWithDispatcherPump(session.DisposeAsync().AsTask()); AvaloniaHeadlessPlatform.ForceRenderTimerTick(1); Dispatcher.UIThread.RunJobs(); owner.Close(); return references; } [MethodImpl(MethodImplOptions.NoInlining)] private static void CollectGarbage() { for (var i = 0; i < 3; i++) { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } } private sealed record ClosedSessionReferences( WeakReference Session, WeakReference Presenter, WeakReference Surface, WeakReference Content, DialogButton? RetainedButton); private sealed record RetainedButtonSessionReferences( DialogButton Button, WeakReference Presenter, WeakReference Surface); private sealed class UiThreadDialogContent : Border { public UiThreadDialogContent() { Dispatcher.UIThread.CheckAccess().ShouldBeTrue(); } } private static void WaitWithDispatcherPump(Task task) { var timeoutAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(5); while (!task.IsCompleted && DateTimeOffset.UtcNow < timeoutAt) { Dispatcher.UIThread.RunJobs(); Thread.Sleep(1); } task.IsCompleted.ShouldBeTrue(); task.GetAwaiter().GetResult(); } private static void PumpUntil(Func condition) { var timeoutAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(5); while (!condition() && DateTimeOffset.UtcNow < timeoutAt) { Dispatcher.UIThread.RunJobs(); Thread.Sleep(1); } condition().ShouldBeTrue(); } private static void RunOnUIThread(Action action) { Dispatcher.UIThread.Invoke(action); } } [CollectionDefinition(Name, DisableParallelization = true)] public sealed class DialogLifecycleTestCollection { public const string Name = "Dialog lifecycle tests"; }