diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditConfigurationTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditConfigurationTests.cs index 8c1f5fb6..5900f2b4 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditConfigurationTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/AuditConfigurationTests.cs @@ -138,4 +138,130 @@ public class AuditConfigurationTests await ConfigAuditDataSeeder.DeleteByMarkerAsync(marker); } } + + [SkippableFact] + public async Task LargeState_OpensAndClosesModal() + { + Skip.IfNot(await ConfigAuditDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason); + + var marker = NewMarker(); + try + { + // 1 bulk row, no bundle rows. The seeder gives bulk row index 0 a + // > 1024-char AfterStateJson, so its State cell renders the large-state + // "View in modal" button (small-state rows show an inline View/Hide + // toggle instead, which we are not exercising here). + await ConfigAuditDataSeeder.SeedAsync(marker, Guid.NewGuid(), bulkCount: 1, bundleRows: 0); + + var page = await _fixture.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{ConfigAuditUrl}"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Filter by the unique marker and Search so only this run's single + // large-state row populates the grid. + await page.Locator("#audit-filter-entity-type").FillAsync(marker); + await page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')").ClickAsync(); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Open the modal from the seeded large-state row. The marker is unique, + // so a page-wide "View in modal" locator resolves to exactly this row. + await page.Locator("button.btn-outline-info.btn-sm:has-text('View in modal')").ClickAsync(); + + // The modal renders with a title like "Audit entry {id} — {EntityType} state". + await Assertions.Expect(page.Locator("div.modal.show .modal-title")).ToBeVisibleAsync(); + await Assertions.Expect(page.Locator("div.modal.show .modal-title")).ToContainTextAsync("state"); + + // Closing via the footer Close button removes the modal from the DOM. + await page.Locator(".modal.show button.btn-outline-secondary.btn-sm:has-text('Close')").ClickAsync(); + await Assertions.Expect(page.Locator("div.modal.show")).ToHaveCountAsync(0, new() { Timeout = 10_000 }); + } + finally + { + await ConfigAuditDataSeeder.DeleteByMarkerAsync(marker); + } + } + + [SkippableFact] + public async Task CopyEntityId_ShowsToast() + { + Skip.IfNot(await ConfigAuditDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason); + + var marker = NewMarker(); + try + { + // 1 bulk row (non-empty EntityId) so the row renders the 📋 copy button. + await ConfigAuditDataSeeder.SeedAsync(marker, Guid.NewGuid(), bulkCount: 1, bundleRows: 0); + + var page = await _fixture.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{ConfigAuditUrl}"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await page.Locator("#audit-filter-entity-type").FillAsync(marker); + await page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')").ClickAsync(); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Click the copy-entity-ID button on the (only) seeded row. + await page.Locator("button[aria-label^='Copy entity ID']").First.ClickAsync(); + + // Assert a toast appears, TOLERANTLY of which one. The cluster is served + // over a non-secure, non-localhost http origin (http://scadabridge-traefik), + // so navigator.clipboard is unavailable: CopyAsync's writeText throws and + // the handler shows the ERROR toast "Copy failed." instead of the success + // toast "Copied to clipboard.". Either path produces exactly one .toast, so + // we assert the count only and deliberately do NOT assert the message text. + await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 }); + } + finally + { + await ConfigAuditDataSeeder.DeleteByMarkerAsync(marker); + } + } + + [SkippableFact] + public async Task BundleImportId_DrillIn_ChipFiltersAndClears() + { + Skip.IfNot(await ConfigAuditDataSeeder.IsAvailableAsync(), DbUnavailableSkipReason); + + var marker = NewMarker(); + var bundleId = Guid.NewGuid(); + try + { + // 1 bulk row (EntityName "{marker}-0", BundleImportId NULL) + 2 bundle + // rows (EntityName "{marker}-bundle-0/1", BundleImportId = bundleId), all + // EntityType = marker so the finally cleanup removes every row. + await ConfigAuditDataSeeder.SeedAsync(marker, bundleId, bulkCount: 1, bundleRows: 2); + + var page = await _fixture.NewAuthenticatedPageAsync(); + // Arriving WITH a ?bundleImportId= query param auto-loads the grid + // pre-filtered (OnParametersSetAsync fires FetchPage because the param + // differs from the initial null) — no Search click needed. + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/audit/configuration?bundleImportId={bundleId}"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // The filter chip is shown for the active bundle import. + await Assertions.Expect(page.Locator("span.badge.bg-primary:has-text('Filtered by Bundle Import:')")).ToBeVisibleAsync(); + + // Exactly the 2 bundle rows show (EntityName "{marker}-bundle-0/1"). The + // bulk row (EntityName "{marker}-0", BundleImportId NULL) is filtered out. + // The "{marker}-0" token is NOT a substring of "{marker}-bundle-0", so the + // two locators distinguish the bundle rows from the bulk row unambiguously. + await Assertions.Expect(page.Locator("tr").Filter(new() { HasText = $"{marker}-bundle-" })) + .ToHaveCountAsync(2, new() { Timeout = 10_000 }); + await Assertions.Expect(page.Locator("tr").Filter(new() { HasText = $"{marker}-0" })) + .ToHaveCountAsync(0); + + // Clearing the filter removes the chip and navigates to the bare route; + // because BundleImportId goes from the guid back to null, the page + // re-fetches and reloads the grid with all entries (chip gone, rows ≥ 1). + await page.Locator("button[aria-label='Clear Bundle Import filter']").ClickAsync(); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await Assertions.Expect(page.Locator("span.badge.bg-primary:has-text('Filtered by Bundle Import:')")).ToHaveCountAsync(0); + await Assertions.Expect(page.Locator("tbody tr").First).ToBeVisibleAsync(); + } + finally + { + await ConfigAuditDataSeeder.DeleteByMarkerAsync(marker); + } + } }