From 839770d503457e0131d61b3514d1a3fcfaa9167c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 6 Jun 2026 13:31:18 -0400 Subject: [PATCH] test(e2e): ParkedMessages filter-control gating + conditional bulk action-bar guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ParkedMessagesActionTests with three facts: Query disabled until site selected, Clear disabled until filter set (then re-disabled after clear), and a tolerant bulk Retry/Discard bar reveal when parked rows happen to be present. Distinct from the existing render-without-hang test — gating facts are deterministic; row-dependent fact early-returns gracefully in unseedable environments. --- .../Monitoring/ParkedMessagesActionTests.cs | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Monitoring/ParkedMessagesActionTests.cs diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Monitoring/ParkedMessagesActionTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Monitoring/ParkedMessagesActionTests.cs new file mode 100644 index 00000000..e47cda84 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Monitoring/ParkedMessagesActionTests.cs @@ -0,0 +1,124 @@ +using Microsoft.Playwright; +using Xunit; +using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Monitoring; + +/// +/// End-to-end action and filter-control gating tests for the central Parked Messages page +/// (/monitoring/parked-messages). +/// +/// +/// Why this is distinct from : the existing render +/// guard verifies that the singleton-backed cross-cluster query resolves (renders the results +/// table or the empty-state card) rather than hanging. This class targets the DETERMINISTIC +/// filter-control gating behaviour that is verifiable regardless of whether any parked rows +/// exist in the live environment: +/// +/// The Query button is disabled until a site is selected. +/// The Clear button is disabled until at least one filter is active, +/// and is re-disabled after clicking Clear. +/// +/// A third, tolerant fact exercises the conditional bulk-action bar (Retry/Discard selected) +/// that appears when at least one row checkbox is checked. Parked store-and-forward rows live +/// in the SITE's local SQLite buffer — there is no central table to seed — so that fact +/// performs an early-return no-op when no rows happen to be present, which is both expected +/// and acceptable in this environment. +/// +/// +/// +/// Gated on via Skip.IfNot: when the cluster is +/// unreachable the facts report as Skipped (not Failed), matching the established suite idiom. +/// +/// +[Collection("Playwright")] +public class ParkedMessagesActionTests +{ + private readonly PlaywrightFixture _fixture; + + public ParkedMessagesActionTests(PlaywrightFixture fixture) + { + _fixture = fixture; + } + + /// + /// The Query button must be disabled on page load (no site selected) and become + /// enabled once a site is selected from the site dropdown. + /// + [SkippableFact] + public async Task QueryButton_DisabledUntilSiteSelected() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var page = await _fixture.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/monitoring/parked-messages"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + await Assertions.Expect(page.Locator("h4:has-text('Parked Messages')")).ToBeVisibleAsync(); + + var query = page.Locator("button.btn.btn-primary.btn-sm:has-text('Query')"); + await Assertions.Expect(query).ToBeDisabledAsync(); + + await page.Locator("#pm-filter-site").SelectOptionAsync("site-a"); + // Selecting a site enables Query (and kicks off its own search — tolerated). + await Assertions.Expect(query).ToBeEnabledAsync(new() { Timeout = 5_000 }); + } + + /// + /// The Clear button must be disabled on page load (no active filters) and become + /// enabled once any filter is changed. Clicking Clear must re-disable it. + /// + [SkippableFact] + public async Task ClearButton_DisabledUntilFilterSet_ThenResets() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var page = await _fixture.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/monitoring/parked-messages"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + var clear = page.Locator("button.btn.btn-outline-secondary.btn-sm:has-text('Clear')"); + await Assertions.Expect(clear).ToBeDisabledAsync(); + + // Setting any filter (Age) flips HasActiveFilters -> Clear enables. + await page.Locator("#pm-filter-age").SelectOptionAsync("LastHour"); + await Assertions.Expect(clear).ToBeEnabledAsync(new() { Timeout = 5_000 }); + + await clear.ClickAsync(); + await Assertions.Expect(clear).ToBeDisabledAsync(new() { Timeout = 5_000 }); + } + + /// + /// When at least one parked row is present, checking its row checkbox must reveal + /// the bulk action bar with Retry selected and Discard selected buttons. If no rows + /// are present (the common case in a clean test environment — parked rows are not + /// seedable from central), the test exits early as a tolerated no-op. + /// + [SkippableFact] + public async Task SelectingParkedRow_RevealsBulkRetryDiscardBar_WhenRowsPresent() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var page = await _fixture.NewAuthenticatedPageAsync(); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/monitoring/parked-messages"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await page.Locator("#pm-filter-site").SelectOptionAsync("site-a"); + await page.Locator("button.btn.btn-primary.btn-sm:has-text('Query')").ClickAsync(); + + // Wait for the query to resolve (table OR empty-state card). + var resolved = page.Locator("table.parked-table, div.card-body:has-text('No parked messages')"); + await Assertions.Expect(resolved.First).ToBeVisibleAsync(new() { Timeout = 20_000 }); + + // Parked S&F rows are not seedable, so rows may be absent in this environment. Only + // assert the action affordances when at least one row rendered. + var rows = page.Locator("tr.parked-row"); + if (await rows.CountAsync() == 0) + { + return; // No parked messages at site-a — bulk-bar affordance can't be exercised. + } + + await rows.First.Locator("input.form-check-input").CheckAsync(); + await Assertions.Expect(page.Locator("button:has-text('Retry selected')")).ToBeVisibleAsync(); + await Assertions.Expect(page.Locator("button:has-text('Discard selected')")).ToBeVisibleAsync(); + } +}