81 lines
3.1 KiB
C#
81 lines
3.1 KiB
C#
using Bunit;
|
|
using ScadaLink.CentralUI.Components.Shared;
|
|
|
|
namespace ScadaLink.CentralUI.Tests.Shared;
|
|
|
|
/// <summary>
|
|
/// Regression tests for CentralUI-010. <c>ToastNotification.AddToast</c>
|
|
/// scheduled <c>Task.Delay(dismissMs).ContinueWith(...)</c> with the result
|
|
/// discarded; the continuation called <c>InvokeAsync(StateHasChanged)</c>. When
|
|
/// the host page is disposed before the delay elapses, the continuation ran
|
|
/// against a disposed component and threw <c>ObjectDisposedException</c> on a
|
|
/// thread-pool thread with no catch (an unobserved task exception). The fix
|
|
/// holds a <c>CancellationTokenSource</c> cancelled in <c>Dispose()</c>.
|
|
/// </summary>
|
|
public class ToastNotificationTests : BunitContext
|
|
{
|
|
[Fact]
|
|
public async Task ShowToast_AfterDisposal_IsNoOp_AndSchedulesNothing()
|
|
{
|
|
// Regression: the pre-fix AddToast always added the toast and scheduled
|
|
// a Task.Delay continuation, even after Dispose() — the continuation
|
|
// then ran InvokeAsync(StateHasChanged) against the disposed component.
|
|
// The fix short-circuits AddToast once the disposal token is cancelled.
|
|
var cut = Render<ToastNotification>();
|
|
|
|
await cut.InvokeAsync(() => cut.Instance.Dispose());
|
|
await cut.InvokeAsync(() => cut.Instance.ShowError("after dispose", autoDismissMs: 20));
|
|
|
|
Assert.Equal(0, cut.Instance.ToastCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoDismiss_AfterDisposal_DoesNotThrowUnobservedException()
|
|
{
|
|
var unobserved = new List<Exception>();
|
|
void Handler(object? s, UnobservedTaskExceptionEventArgs e)
|
|
{
|
|
unobserved.Add(e.Exception);
|
|
e.SetObserved();
|
|
}
|
|
TaskScheduler.UnobservedTaskException += Handler;
|
|
try
|
|
{
|
|
var cut = Render<ToastNotification>();
|
|
// Auto-dismiss after a very short delay so the continuation is
|
|
// guaranteed to fire well after we dispose the component.
|
|
await cut.InvokeAsync(() => cut.Instance.ShowSuccess("hello", autoDismissMs: 20));
|
|
|
|
// Dispose the component while the auto-dismiss is still pending.
|
|
await cut.InvokeAsync(() => cut.Instance.Dispose());
|
|
|
|
// Give the (now-cancelled) auto-dismiss well past its delay.
|
|
await Task.Delay(250);
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
GC.Collect();
|
|
}
|
|
finally
|
|
{
|
|
TaskScheduler.UnobservedTaskException -= Handler;
|
|
}
|
|
|
|
Assert.Empty(unobserved);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoDismiss_BeforeDisposal_StillRemovesToast()
|
|
{
|
|
var cut = Render<ToastNotification>();
|
|
await cut.InvokeAsync(() => cut.Instance.ShowInfo("transient", autoDismissMs: 20));
|
|
|
|
// The toast is visible immediately.
|
|
Assert.Contains("transient", cut.Markup);
|
|
|
|
// After the dismiss delay it is removed (auto-dismiss still works).
|
|
cut.WaitForAssertion(
|
|
() => Assert.DoesNotContain("transient", cut.Markup),
|
|
timeout: TimeSpan.FromSeconds(2));
|
|
}
|
|
}
|