test(centralui): e2e execution-tree node detail modal + docs
This commit is contained in:
@@ -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,
|
hosts the Audit Log page (filter bar, results grid, drilldown drawer,
|
||||||
server-side CSV export). Drill-in links appear on Notifications, Site Calls,
|
server-side CSV export). Drill-in links appear on Notifications, Site Calls,
|
||||||
External Systems, Inbound API key, Sites, and Instances detail pages.
|
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
|
- **[Health Monitoring (#11)](Component-HealthMonitoring.md)** — three new
|
||||||
tiles (Volume, Error rate, Backlog) plus new health metrics:
|
tiles (Volume, Error rate, Backlog) plus new health metrics:
|
||||||
`SiteAuditBacklog`, `SiteAuditWriteFailures`, `SiteAuditTelemetryStalled`,
|
`SiteAuditBacklog`, `SiteAuditWriteFailures`, `SiteAuditTelemetryStalled`,
|
||||||
|
|||||||
@@ -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]
|
[Fact]
|
||||||
public async Task NotificationsPage_RendersAuditDrillInLinkPattern()
|
public async Task NotificationsPage_RendersAuditDrillInLinkPattern()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user