From 0fa4ac552515e58c74f88be5058e8b10faba40ca Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 19 May 2026 05:54:48 -0400 Subject: [PATCH] refactor(central-ui): contextual errors, parallel recipient load, delete-path test for Notification Lists --- .../Notifications/NotificationLists.razor | 12 ++++-- .../Pages/NotificationListsPageTests.cs | 40 +++++++++++++++++-- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationLists.razor b/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationLists.razor index 371e156..d97d3aa 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationLists.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationLists.razor @@ -104,14 +104,18 @@ { _lists = (await NotificationRepository.GetAllNotificationListsAsync()).ToList(); _recipients.Clear(); - foreach (var list in _lists) + var recipientTasks = _lists.ToDictionary( + list => list.Id, + list => NotificationRepository.GetRecipientsByListIdAsync(list.Id)); + await Task.WhenAll(recipientTasks.Values); + foreach (var (id, task) in recipientTasks) { - _recipients[list.Id] = await NotificationRepository.GetRecipientsByListIdAsync(list.Id); + _recipients[id] = task.Result; } } catch (Exception ex) { - _errorMessage = ex.Message; + _errorMessage = $"Failed to load notification lists: {ex.Message}"; } _loading = false; } @@ -131,7 +135,7 @@ } catch (Exception ex) { - _toast.ShowError(ex.Message); + _toast.ShowError($"Failed to delete notification list: {ex.Message}"); } } } diff --git a/tests/ScadaLink.CentralUI.Tests/Pages/NotificationListsPageTests.cs b/tests/ScadaLink.CentralUI.Tests/Pages/NotificationListsPageTests.cs index c998130..89ac995 100644 --- a/tests/ScadaLink.CentralUI.Tests/Pages/NotificationListsPageTests.cs +++ b/tests/ScadaLink.CentralUI.Tests/Pages/NotificationListsPageTests.cs @@ -44,8 +44,11 @@ public class NotificationListsPageTests : BunitContext var cut = Render(); - Assert.Contains("Ops On-Call", cut.Markup); - Assert.Contains("jane@example.com", cut.Markup); + cut.WaitForAssertion(() => + { + Assert.Contains("Ops On-Call", cut.Markup); + Assert.Contains("jane@example.com", cut.Markup); + }); } [Fact] @@ -60,7 +63,38 @@ public class NotificationListsPageTests : BunitContext var cut = Render(); - Assert.Contains("No notification lists", cut.Markup); + cut.WaitForAssertion(() => + Assert.Contains("No notification lists", cut.Markup)); + } + + [Fact] + public void DeleteList_ConfirmsThenDeletesAndReloads() + { + var repo = Substitute.For(); + repo.GetAllNotificationListsAsync() + .Returns(Task.FromResult>( + new List { new("Ops On-Call") { Id = 1 } })); + repo.GetRecipientsByListIdAsync(1) + .Returns(Task.FromResult>( + new List())); + Services.AddSingleton(repo); + WireAuthAndDialog(); + + var cut = Render(); + + cut.WaitForState(() => cut.Markup.Contains("Ops On-Call")); + + var deleteButton = cut.FindAll("tbody tr button") + .First(b => b.TextContent.Contains("Delete")); + deleteButton.Click(); + + cut.WaitForAssertion(() => + { + repo.Received().DeleteNotificationListAsync(1); + repo.Received().SaveChangesAsync(); + // Reload re-invokes the list query (once on init, once after delete). + repo.Received(2).GetAllNotificationListsAsync(); + }); } /// A dialog service that auto-confirms, so action paths run end-to-end.