Toasts: non-blocking notifications replace MessageBoxes + log-only failures (TASK 24)
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using Notification.Core;
|
||||
using Notification.Wpf.Controls;
|
||||
using Xunit;
|
||||
using ytLive.Services;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// TASK 24 integration: the real NotificationService against a real window-hosted
|
||||
/// ToastArea. Transient notifications expire on their own; errors stick until the
|
||||
/// area goes away. ONE integration test per the Good Dog Rule.
|
||||
/// </summary>
|
||||
[Collection("RealApp")]
|
||||
public class NotificationAreaIntegrationTests
|
||||
{
|
||||
private readonly RealAppHost _app;
|
||||
|
||||
public NotificationAreaIntegrationTests(RealAppHost app) => _app = app;
|
||||
|
||||
[Fact]
|
||||
public void Info_Auto_Dismisses_And_Error_Sticks()
|
||||
{
|
||||
Window? window = null;
|
||||
NotificationArea? area = null;
|
||||
NotificationService? service = null;
|
||||
_app.Run(() =>
|
||||
{
|
||||
area = new NotificationArea { Name = NotificationService.AreaName, Position = NotificationPosition.BottomRight, MaxItems = 4 };
|
||||
var grid = new Grid();
|
||||
grid.Children.Add(area);
|
||||
window = new Window { Content = grid, Width = 400, Height = 300, WindowStartupLocation = WindowStartupLocation.Manual, Left = -32000, Top = 0, ShowInTaskbar = false, ShowActivated = false };
|
||||
window.Show();
|
||||
service = new NotificationService();
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
service!.Info("Hello", "transient");
|
||||
Assert.True(WaitFor(() => ItemCount(area!) == 1), "info toast never appeared");
|
||||
|
||||
Assert.True(WaitFor(() => ItemCount(area!) == 0, TimeSpan.FromSeconds(8)),
|
||||
"info toast never auto-dismissed");
|
||||
|
||||
service!.Error("Broken", "sticky");
|
||||
Assert.True(WaitFor(() => ItemCount(area!) == 1), "error toast never appeared");
|
||||
|
||||
Thread.Sleep(1200);
|
||||
Assert.Equal(1, ItemCount(area!));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_app.Run(() => window!.Close());
|
||||
}
|
||||
}
|
||||
|
||||
private int ItemCount(NotificationArea area)
|
||||
{
|
||||
var count = 0;
|
||||
_app.Run(() => count = CountItems(area));
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int CountItems(NotificationArea area)
|
||||
{
|
||||
var count = 0;
|
||||
Walk(area, ref count);
|
||||
return count;
|
||||
|
||||
static void Walk(DependencyObject node, ref int count)
|
||||
{
|
||||
if (node is Panel panel && panel.Name == "PART_Items")
|
||||
{
|
||||
count += panel.Children.Count;
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(node); i++)
|
||||
Walk(VisualTreeHelper.GetChild(node, i), ref count);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool WaitFor(Func<bool> condition, TimeSpan? timeout = null)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(3));
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (condition()) return true;
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
return condition();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Xunit;
|
||||
using ytLive.Services;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// TASK 24: the pure severity → request mapping behind the toast service.
|
||||
/// Errors must never auto-expire (sticky until dismissed); the rest get short
|
||||
/// lifetimes; everything routes to the in-window ToastArea.
|
||||
/// </summary>
|
||||
public class NotificationServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(AppNotificationSeverity.Info, 4)]
|
||||
[InlineData(AppNotificationSeverity.Success, 4)]
|
||||
[InlineData(AppNotificationSeverity.Warning, 8)]
|
||||
public void Transient_Severities_Expire_With_Scheduled_Lifetimes(AppNotificationSeverity severity, double expectedSeconds)
|
||||
{
|
||||
var request = NotificationService.BuildRequest("Title", "Body", severity);
|
||||
|
||||
Assert.Equal("ToastArea", request.AreaName);
|
||||
Assert.NotNull(request.ExpirationTime);
|
||||
Assert.Equal(TimeSpan.FromSeconds(expectedSeconds), request.ExpirationTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Errors_Never_Expire_And_Route_To_The_Toast_Area()
|
||||
{
|
||||
var request = NotificationService.BuildRequest("Title", "Body", AppNotificationSeverity.Error);
|
||||
|
||||
Assert.Equal("ToastArea", request.AreaName);
|
||||
Assert.Equal(TimeSpan.MaxValue, request.ExpirationTime);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(AppNotificationSeverity.Info, "#3a3b52")]
|
||||
[InlineData(AppNotificationSeverity.Success, "#1d5c38")]
|
||||
[InlineData(AppNotificationSeverity.Warning, "#b8860b")]
|
||||
[InlineData(AppNotificationSeverity.Error, "#8f1f1f")]
|
||||
public void Severities_Map_To_Dark_Card_Tints(AppNotificationSeverity severity, string expectedHex)
|
||||
{
|
||||
var request = NotificationService.BuildRequest("Title", "Body", severity);
|
||||
|
||||
Assert.Equal(expectedHex.ToLowerInvariant()[^6..], request.BackgroundColor!.Value.ToHex().ToLowerInvariant()[^6..]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user