Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/ToastNotificationTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

87 lines
3.6 KiB
C#

using Bunit;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
/// <summary>
/// Regression tests for toast disposal safety. <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
{
/// <summary>Verifies that show toast, when after disposal, is no op and schedules nothing.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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);
}
/// <summary>Verifies that auto dismiss, when after disposal, does not throw unobserved exception.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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);
}
/// <summary>Verifies that auto dismiss, when before disposal, still removes toast.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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));
}
}