feat(ui): drill-in from Notifications to Audit Log (#23 M7)

This commit is contained in:
Joseph Doherty
2026-05-20 20:20:54 -04:00
parent 450f8bca28
commit 1c20e81d77
2 changed files with 64 additions and 4 deletions

View File

@@ -163,6 +163,14 @@
<td><TimestampDisplay Value="@n.CreatedAt" Format="yyyy-MM-dd HH:mm" /></td>
<td><TimestampDisplay Value="@n.DeliveredAt" Format="yyyy-MM-dd HH:mm" NullText="—" /></td>
<td class="text-end">
@* Bundle D (#23 M7-T10) drill-in: NotificationId is the audit
CorrelationId, so the link deep-links into the central Audit
Log pre-filtered to this notification's lifecycle events. *@
<a class="btn btn-outline-secondary btn-sm me-1"
href="/audit/log?correlationId=@n.NotificationId"
data-test="audit-link-@n.NotificationId">
View audit history
</a>
@if (n.Status == "Parked")
{
<button class="btn btn-outline-success btn-sm me-1"
@@ -174,10 +182,6 @@
Discard
</button>
}
else
{
<span class="text-muted small">—</span>
}
</td>
</tr>
}

View File

@@ -177,6 +177,62 @@ public class NotificationReportPageTests : BunitContext
Assert.Contains("outbox query backend unavailable", cut.Markup));
}
// ─────────────────────────────────────────────────────────────────────────
// Bundle D drill-in (#23 M7-T10) — every notification row carries a
// "View audit history" link to /audit/log?correlationId={NotificationId}.
// ─────────────────────────────────────────────────────────────────────────
[Fact]
public void NotificationRow_ViewAuditHistory_Link_HasCorrectHref()
{
var cut = Render<NotificationReportPage>();
cut.WaitForAssertion(() =>
{
// Both rows (Parked + Delivered) must surface the link — the drill-in
// is row-scope, not status-scope. We pin the parked row's href to the
// canonical correlationId-deep-link shape.
var parkedRow = cut.FindAll("tbody tr")
.First(r => r.TextContent.Contains("Pump fault at Plant-A"));
var link = parkedRow.QuerySelector("a[data-test^=\"audit-link-\"]");
Assert.NotNull(link);
Assert.Equal(
"/audit/log?correlationId=notif-aaaaaaaa-1111",
link!.GetAttribute("href"));
Assert.Contains("View audit history", link.TextContent);
var deliveredRow = cut.FindAll("tbody tr")
.First(r => r.TextContent.Contains("Daily summary"));
var deliveredLink = deliveredRow.QuerySelector("a[data-test^=\"audit-link-\"]");
Assert.NotNull(deliveredLink);
Assert.Equal(
"/audit/log?correlationId=notif-bbbbbbbb-2222",
deliveredLink!.GetAttribute("href"));
});
}
[Fact]
public void Click_NavigatesTo_AuditLog_WithCorrelationId()
{
// The drill-in is a plain <a href> — browser-native navigation, not a
// Blazor onclick handler — so this test verifies the rendered anchor's
// attributes are exactly what a browser would follow: href, role, and
// human-visible text. (Triggering bUnit's .Click() on a bare anchor
// raises MissingEventHandlerException because there is no onclick
// handler to invoke; the navigation contract lives in the <a> markup.)
var cut = Render<NotificationReportPage>();
cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A"));
var parkedRow = cut.FindAll("tbody tr")
.First(r => r.TextContent.Contains("Pump fault at Plant-A"));
var link = parkedRow.QuerySelector("a[data-test^=\"audit-link-\"]")!;
Assert.Equal("a", link.TagName, ignoreCase: true);
Assert.Equal("/audit/log?correlationId=notif-aaaaaaaa-1111", link.GetAttribute("href"));
Assert.Contains("View audit history", link.TextContent);
}
protected override void Dispose(bool disposing)
{
if (disposing)