diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditLogPageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditLogPageTests.cs index cbbf833a..ab4ff50b 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditLogPageTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditLogPageTests.cs @@ -112,6 +112,89 @@ public class AuditLogPageTests } } + [SkippableFact] + public async Task FilterCombination_ChannelPlusTarget_NarrowsToMatch() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + // Two rows differing in channel AND target. Applying the channel filter + // (ApiOutbound) AND a contains-match target filter together should narrow + // the grid to the single matching row — proving the filters AND rather + // than OR. The match row is ApiOutbound + target ".../match"; the other + // row is DbOutbound + target ".../other" and is excluded on both axes. + var runId = Guid.NewGuid().ToString("N"); + var targetPrefix = $"playwright-test/wave4-audit/{runId}/"; + var matchId = Guid.NewGuid(); + var otherId = Guid.NewGuid(); + var now = DateTime.UtcNow; + + try + { + // Row 1 — the match: ApiOutbound channel, target ends in "match". + await AuditDataSeeder.InsertAuditEventAsync( + eventId: matchId, + occurredAtUtc: now, + channel: "ApiOutbound", + kind: "ApiCall", + status: "Delivered", + target: targetPrefix + "match", + httpStatus: 200, + durationMs: 42); + + // Row 2 — the non-match channel: DbOutbound, target ends in "other". + await AuditDataSeeder.InsertAuditEventAsync( + eventId: otherId, + occurredAtUtc: now, + channel: "DbOutbound", + kind: "DbWrite", + status: "Delivered", + target: targetPrefix + "other", + durationMs: 17); + + var page = await _fixture.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/audit/log"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Apply channel AND target together. + await page.Locator("[data-test='filter-channel-select']").SelectOptionAsync("ApiOutbound"); + await page.FillAsync("#audit-target", targetPrefix + "match"); + await page.ClickAsync("[data-test='filter-apply']"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // The match row is visible; the other row is absent — the two filters + // AND, so a row must satisfy both to survive. + await Assertions.Expect(page.Locator($"[data-test='grid-row-{matchId}']")).ToBeVisibleAsync(); + await Assertions.Expect(page.Locator($"[data-test='grid-row-{otherId}']")).ToHaveCountAsync(0); + } + finally + { + await AuditDataSeeder.DeleteByTargetPrefixAsync(targetPrefix); + } + } + + [SkippableFact] + public async Task EmptyResults_AfterApply_ShowsEmptyState() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + // A fresh random executionId GUID matches nothing — the ExecutionId + // filter is an exact match, so a never-seeded GUID is guaranteed-empty by + // construction. After Apply the grid renders zero rows and the empty-state + // message. No seeding, no teardown. + var page = await _fixture.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/audit/log"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await page.FillAsync("#audit-execution-id", Guid.NewGuid().ToString()); + await page.ClickAsync("[data-test='filter-apply']"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Zero grid rows AND the empty-state literal inside the grid container. + await Assertions.Expect(page.Locator("[data-test^='grid-row-']")).ToHaveCountAsync(0); + await Assertions.Expect(page.Locator("[data-test='audit-results-grid']")) + .ToContainTextAsync("No audit events match the current filter."); + } + [SkippableFact] public async Task DrilldownDrawer_JsonPrettyPrintsRequestBody() {