111 lines
5.3 KiB
C#
111 lines
5.3 KiB
C#
using Microsoft.Playwright;
|
|
using Xunit;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Monitoring;
|
|
|
|
/// <summary>
|
|
/// End-to-end render + control-gating tests for the Site Event Logs page
|
|
/// (<c>/monitoring/event-logs</c>).
|
|
///
|
|
/// <para>
|
|
/// <b>Why this is a render + controls + tolerant-query suite (no row assertions):</b>
|
|
/// Site event logs live in the SITE's local SQLite store and are fetched on demand via
|
|
/// an Akka <c>Ask</c> round-trip to a live site — there is no central table, and no CLI
|
|
/// or DB seed path. A clean site may legitimately have zero logged events, so these
|
|
/// facts must never assert on specific event rows. Instead they verify the two things
|
|
/// that are deterministic regardless of site state:
|
|
/// <list type="bullet">
|
|
/// <item><description>The Search button is gated on site selection (disabled until a
|
|
/// site is chosen, then enabled).</description></item>
|
|
/// <item><description>Clicking Search resolves the query — the results table renders
|
|
/// either a data row or the empty-state row.</description></item>
|
|
/// </list>
|
|
/// The row-expand affordance is exercised only when real data rows happen to exist.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Gated on <see cref="ClusterAvailability"/> via <c>Skip.IfNot</c>: when the cluster is
|
|
/// unreachable the facts report as Skipped (not Failed), matching the suite idiom.
|
|
/// </para>
|
|
/// </summary>
|
|
[Collection("Playwright")]
|
|
public class EventLogsTests
|
|
{
|
|
private const string EventLogsUrl = "/monitoring/event-logs";
|
|
|
|
private readonly PlaywrightFixture _fixture;
|
|
|
|
public EventLogsTests(PlaywrightFixture fixture)
|
|
{
|
|
_fixture = fixture;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Search button must be disabled on page load (no site selected) and become
|
|
/// enabled once a site is selected from the site dropdown. Deterministic regardless
|
|
/// of whether the site has any logged events.
|
|
/// </summary>
|
|
[SkippableFact]
|
|
public async Task EventLogs_SearchGatedOnSiteSelection()
|
|
{
|
|
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
|
|
|
var page = await _fixture.NewAuthenticatedPageAsync();
|
|
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{EventLogsUrl}");
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
|
|
await Assertions.Expect(page.Locator("h4:has-text('Site Event Logs')")).ToBeVisibleAsync();
|
|
|
|
// The site selector is populated from the cluster's known sites; the live cluster
|
|
// exposes site-a. Each option's value is the site's SiteIdentifier.
|
|
await Assertions.Expect(page.Locator("#filter-site option[value='site-a']")).ToHaveCountAsync(1);
|
|
|
|
// Search is gated: disabled until a site is selected.
|
|
var search = page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')");
|
|
await Assertions.Expect(search).ToBeDisabledAsync();
|
|
|
|
await page.Locator("#filter-site").SelectOptionAsync(new SelectOptionValue { Value = "site-a" });
|
|
await Assertions.Expect(search).ToBeEnabledAsync(new() { Timeout = 5_000 });
|
|
}
|
|
|
|
/// <summary>
|
|
/// After selecting a site and clicking Search, the query must resolve: the results
|
|
/// table renders either a data row or the empty-state row (both match
|
|
/// <c>table tbody tr</c>). The row-expand affordance is exercised only when real data
|
|
/// rows exist — a site may have zero logged events, and event logs are not seedable
|
|
/// from central, so the empty-state render is itself the assertion in that case.
|
|
/// </summary>
|
|
[SkippableFact]
|
|
public async Task EventLogs_Search_RendersTableOrEmptyState()
|
|
{
|
|
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
|
|
|
var page = await _fixture.NewAuthenticatedPageAsync();
|
|
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{EventLogsUrl}");
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
|
|
await page.Locator("#filter-site").SelectOptionAsync(new SelectOptionValue { Value = "site-a" });
|
|
await page.Locator("button.btn.btn-primary.btn-sm:has-text('Search')").ClickAsync();
|
|
|
|
// The query resolved once the results table renders. The empty-state is itself a
|
|
// <tr><td colspan=7>No events found.</td></tr>, so a data row OR the empty-state
|
|
// row both surface under table tbody tr. Generous timeout: the fetch is an Akka
|
|
// Ask round-trip to a live site.
|
|
var settled = page.Locator("table tbody tr, td:has-text('No events found.')");
|
|
await Assertions.Expect(settled.First).ToBeVisibleAsync(new() { Timeout = 15_000 });
|
|
|
|
// Exercise the row-expand ONLY IF real data rows exist. A clean site may have zero
|
|
// logged events, and event logs are not seedable from central — so when no expand
|
|
// toggle is present, the empty-state render above is the assertion (tolerated).
|
|
var expandBtns = page.Locator("button[aria-label='View full message']");
|
|
if (await expandBtns.CountAsync() > 0)
|
|
{
|
|
await expandBtns.First.ClickAsync();
|
|
await Assertions.Expect(page.Locator("button[aria-label='Hide full message']").First)
|
|
.ToBeVisibleAsync(new() { Timeout = 5_000 });
|
|
}
|
|
// else: no events logged on this site — the empty-state render is the assertion.
|
|
}
|
|
}
|