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(); } }