mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-19 23:30:44 +08:00
1090 lines
36 KiB
C#
1090 lines
36 KiB
C#
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<OverlayDialogPresenter>().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<DialogButton>()
|
|
.Any(button => button.StandardButtonType == DialogStandardButton.Ok));
|
|
|
|
sessionTask.IsCompleted.ShouldBeFalse();
|
|
window.GetVisualDescendants()
|
|
.OfType<DialogButton>()
|
|
.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<UiThreadDialogContent, object>(
|
|
null,
|
|
new DialogOptions
|
|
{
|
|
StandardButtons = DialogStandardButton.Ok,
|
|
IsMotionEnabled = false
|
|
},
|
|
window,
|
|
TestContext.Current.CancellationToken));
|
|
PumpUntil(() => sessionTask.IsCompleted ||
|
|
window.GetVisualDescendants()
|
|
.OfType<DialogButton>()
|
|
.Any(button => button.StandardButtonType == DialogStandardButton.Ok));
|
|
|
|
sessionTask.IsCompleted.ShouldBeFalse();
|
|
window.GetVisualDescendants()
|
|
.OfType<DialogButton>()
|
|
.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<string>();
|
|
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<OverlayDialogPresenter>().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<OverlayDialogPresenter>().ShouldHaveSingleItem();
|
|
|
|
dialog.IsOpen = false;
|
|
PumpUntil(() => !window.GetVisualDescendants().OfType<OverlayDialogPresenter>().Any());
|
|
|
|
dialog.IsOpen.ShouldBeFalse();
|
|
openedCount.ShouldBe(0);
|
|
closedCount.ShouldBe(1);
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.GetVisualDescendants().OfType<OverlayDialogPresenter>().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<OverlayDialogPresenter>()
|
|
.ShouldHaveSingleItem();
|
|
firstPresenter.IsMotionEnabled = true;
|
|
firstPresenter.MotionDuration = TimeSpan.FromMilliseconds(80);
|
|
|
|
dialog.IsOpen = false;
|
|
Dispatcher.UIThread.RunJobs();
|
|
firstPresenter.Parent.ShouldBeOfType<DialogOverlayLayer>();
|
|
|
|
dialog.IsOpen = true;
|
|
PumpUntil(() => openedCount == 2);
|
|
|
|
dialog.IsOpen.ShouldBeTrue();
|
|
closedCount.ShouldBe(1);
|
|
window.GetVisualDescendants()
|
|
.OfType<OverlayDialogPresenter>()
|
|
.ShouldHaveSingleItem()
|
|
.ShouldNotBeSameAs(firstPresenter);
|
|
|
|
dialog.IsOpen = false;
|
|
PumpUntil(() => !window.GetVisualDescendants().OfType<OverlayDialogPresenter>().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<InvalidOperationException>(() =>
|
|
WaitWithDispatcherPump(dialog.OpenAsync(TestContext.Current.CancellationToken)));
|
|
|
|
var firstPresenter = window.GetVisualDescendants()
|
|
.OfType<OverlayDialogPresenter>()
|
|
.ShouldHaveSingleItem();
|
|
firstPresenter.IsMotionEnabled = true;
|
|
firstPresenter.MotionDuration = TimeSpan.FromMilliseconds(80);
|
|
|
|
dialog.IsOpen = false;
|
|
Dispatcher.UIThread.RunJobs();
|
|
Should.Throw<InvalidOperationException>(() =>
|
|
WaitWithDispatcherPump(dialog.OpenAsync(TestContext.Current.CancellationToken)));
|
|
PumpUntil(() => !window.GetVisualDescendants().OfType<OverlayDialogPresenter>().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<DialogSurface>().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<InvalidOperationException>(() =>
|
|
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<OverlayDialogPresenter>().Any());
|
|
|
|
root.Children.Remove(dialog);
|
|
PumpUntil(() => !window.GetVisualDescendants().OfType<OverlayDialogPresenter>().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<bool>(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<OverlayDialogPresenter>().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<OverlayDialogPresenter>()
|
|
.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<OverlayDialogPresenter>()
|
|
.Single();
|
|
var firstSurface = firstPresenter.Surface;
|
|
window.FocusManager.GetFocusedElement().ShouldBeSameAs(firstSurface);
|
|
|
|
var secondTask = second.OpenAsync(TestContext.Current.CancellationToken);
|
|
PumpUntil(() => secondOpened);
|
|
var secondSurface = window.GetVisualDescendants()
|
|
.OfType<OverlayDialogPresenter>()
|
|
.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<bool> 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";
|
|
}
|