test(centralui): e2e execution-tree node detail modal + docs

This commit is contained in:
Joseph Doherty
2026-05-22 01:54:12 -04:00
parent 3f1ad08f42
commit 35cef4ad1b
2 changed files with 107 additions and 0 deletions

View File

@@ -428,6 +428,9 @@ global value in v1; per-channel overrides are deferred to v1.x.
hosts the Audit Log page (filter bar, results grid, drilldown drawer,
server-side CSV export). Drill-in links appear on Notifications, Site Calls,
External Systems, Inbound API key, Sites, and Instances detail pages.
Double-clicking a node on the execution-tree page opens a detail modal
listing that execution's audit rows, with click-through to each row's full
detail view.
- **[Health Monitoring (#11)](Component-HealthMonitoring.md)** — three new
tiles (Volume, Error rate, Backlog) plus new health metrics:
`SiteAuditBacklog`, `SiteAuditWriteFailures`, `SiteAuditTelemetryStalled`,

View File

@@ -516,6 +516,110 @@ public class AuditLogPageTests
}
}
[Fact]
public async Task DoubleClickTreeNode_OpensExecutionRowModal()
{
// Execution-Tree Node Detail Modal feature, Task 5: double-clicking a
// node on the /audit/execution-tree page opens ExecutionDetailModal —
// a modal listing that execution's audit rows, with click-through to
// each row's full <AuditEventDetail> view. We seed ONE execution with
// TWO audit rows (so the modal opens to the list view, not straight to
// a single-row detail), open the tree, double-click the node, walk
// list → row → detail, then close the modal.
if (!await AuditDataSeeder.IsAvailableAsync())
{
throw new InvalidOperationException("MSSQL unavailable; see FilterNarrowing test for setup instructions.");
}
var runId = Guid.NewGuid().ToString("N");
var targetPrefix = $"playwright-test/exec-node-modal/{runId}/";
var executionId = Guid.NewGuid();
var inboundEventId = Guid.NewGuid();
var outboundEventId = Guid.NewGuid();
var now = DateTime.UtcNow;
try
{
// Two rows sharing the same ExecutionId — an inbound request and an
// outbound call it made. The shared ExecutionId makes the tree node
// multi-row, so the modal lands on the list view.
await AuditDataSeeder.InsertAuditEventAsync(
eventId: inboundEventId,
occurredAtUtc: now,
channel: "ApiInbound",
kind: "InboundRequest",
status: "Delivered",
target: targetPrefix + "inbound",
executionId: executionId,
httpStatus: 200,
durationMs: 9);
await AuditDataSeeder.InsertAuditEventAsync(
eventId: outboundEventId,
occurredAtUtc: now,
channel: "ApiOutbound",
kind: "ApiCall",
status: "Delivered",
target: targetPrefix + "outbound",
executionId: executionId,
httpStatus: 200,
durationMs: 21);
var page = await _fixture.NewAuthenticatedPageAsync();
// Open the execution tree directly for the seeded execution.
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/audit/execution-tree?executionId={executionId}");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// The seeded execution renders as a tree node.
var nodeBody = page.Locator($"[data-test='tree-node-{executionId}'] .execution-tree-body");
await Assertions.Expect(nodeBody).ToBeVisibleAsync();
// Double-clicking the node body raises ExecutionTree's @ondblclick,
// which is a Blazor Server (InteractiveServer) handler — it only
// fires once the SignalR circuit is live. NetworkIdle can settle
// before the circuit connects, so a single early DblClick can be
// dropped. Retry the double-click until the modal appears.
var modal = page.Locator("[data-test='execution-detail-modal']");
for (var attempt = 0; attempt < 10 && await modal.CountAsync() == 0; attempt++)
{
await nodeBody.DblClickAsync();
try
{
await modal.WaitForAsync(new() { State = WaitForSelectorState.Visible, Timeout = 1000 });
}
catch (TimeoutException)
{
// Circuit not connected yet — loop and re-issue the dblclick.
}
}
await Assertions.Expect(modal).ToBeVisibleAsync();
// The modal opens on the list view — one button per audit row.
var inboundRow = page.Locator($"[data-test='execution-detail-row-{inboundEventId}']");
var outboundRow = page.Locator($"[data-test='execution-detail-row-{outboundEventId}']");
await Assertions.Expect(inboundRow).ToBeVisibleAsync();
await Assertions.Expect(outboundRow).ToBeVisibleAsync();
// Clicking a row switches the modal to that row's full detail —
// the shared <AuditEventDetail> field block renders.
await outboundRow.ClickAsync();
await Assertions.Expect(page.Locator("[data-test='drawer-fields']")).ToBeVisibleAsync();
// Closing the modal tears it down. The close click round-trips
// over the SignalR circuit before the @if(IsOpen) block re-renders
// away, so use the auto-retrying assertion rather than a bare
// CountAsync.
await page.Locator("[data-test='execution-detail-close']").ClickAsync();
await Assertions.Expect(modal).ToHaveCountAsync(0);
}
finally
{
await AuditDataSeeder.DeleteByTargetPrefixAsync(targetPrefix);
}
}
[Fact]
public async Task NotificationsPage_RendersAuditDrillInLinkPattern()
{