test(e2e): assert Health KPI tiles resolve (singleton-hang guard)
Guards against the Akka singleton Ask hang regression: asserts all nine KPI tiles on /monitoring/health resolve to numeric values and never show the em-dash degrade placeholder (—). Covers Notification Outbox, Audit, and Site Call tile groups. Selector disambiguation: Outbox tiles are div.card, Site Call tiles are button.card — prevents strict-mode collisions on the shared "Stuck" and "Parked" labels.
This commit is contained in:
+149
@@ -0,0 +1,149 @@
|
||||
using Microsoft.Playwright;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Monitoring;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end guard for the Health dashboard KPI tiles (<c>/monitoring/health</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// The Health dashboard fans out to three Akka cluster-singleton <c>Ask</c>s every
|
||||
/// 10 s (Notification Outbox, Site Call, Audit) to populate nine KPI tiles. A
|
||||
/// previously-fixed bug caused those <c>Ask</c>s to hang, leaving every tile showing
|
||||
/// the em-dash degrade placeholder (<c>—</c>) instead of a resolved numeric value.
|
||||
/// This test guards that regression: it asserts that every tile resolves to a value
|
||||
/// and never shows <c>—</c>.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The three tile groups and their selectors:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <b>Notification Outbox</b> — inlined in <c>Health.razor</c> with no
|
||||
/// <c>data-test</c> attribute. Each is a Bootstrap <c>div.card</c> whose
|
||||
/// <c>.card-body</c> contains a value <c>h3</c> and a <c>small.text-muted</c>
|
||||
/// label. Located by the card that contains the label text, then its <c>h3</c>.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <b>Audit</b> — rendered by <c>AuditKpiTiles.razor</c>; each tile button
|
||||
/// carries a <c>data-test</c> attribute: <c>audit-kpi-volume</c>,
|
||||
/// <c>audit-kpi-error-rate</c>, <c>audit-kpi-backlog</c>.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <b>Site Calls</b> — rendered by <c>SiteCallKpiTiles.razor</c>; each tile
|
||||
/// button carries a <c>data-test</c> attribute: <c>site-call-kpi-buffered</c>,
|
||||
/// <c>site-call-kpi-stuck</c>, <c>site-call-kpi-parked</c>.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Collection("Playwright")]
|
||||
public class HealthDashboardTests
|
||||
{
|
||||
private const string HealthUrl = "/monitoring/health";
|
||||
|
||||
/// <summary>
|
||||
/// The degrade placeholder rendered when a KPI loader faults — an em-dash
|
||||
/// (U+2014). A healthy tile shows a non-negative integer instead.
|
||||
/// </summary>
|
||||
private const string DegradePlaceholder = "—"; // —
|
||||
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
public HealthDashboardTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that all nine KPI tiles on the Health dashboard resolve to numeric
|
||||
/// values and do not show the em-dash degrade placeholder (<c>—</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// A generous 20 s per-tile timeout is intentional: the tiles are populated
|
||||
/// asynchronously after initial render as the three singleton <c>Ask</c>s
|
||||
/// complete. The Playwright web-first assertion retries within that window
|
||||
/// rather than using a fixed sleep, so a fast cluster will pass quickly.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task KpiTiles_ResolveToValues_NotDegradePlaceholder()
|
||||
{
|
||||
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
||||
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}{HealthUrl}");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// ── Notification Outbox tiles (no data-test; inlined in Health.razor as
|
||||
// plain div.card elements, not buttons — use that to disambiguate from
|
||||
// the SiteCallKpiTiles which render their cards as <button class="card">)
|
||||
// The Outbox section renders under the "Notification Outbox" h6 heading.
|
||||
// Scope by the exact small.text-muted label to pinpoint each tile's h3.
|
||||
|
||||
var queueDepthH3 = page.Locator("div.card", new() { HasText = "Queue Depth" }).Locator("h3");
|
||||
// "Stuck" and "Parked" labels appear in both the Outbox section (div.card)
|
||||
// and the Site Call tiles section (button.card). Discriminate on the element
|
||||
// type: the Outbox tiles are <div class="card">, the Site Call tiles are
|
||||
// <button class="card">. Using `div.card` (not `button.card`) ensures we
|
||||
// select the Notification Outbox cards only.
|
||||
var outboxStuckH3 = page.Locator("div.card", new() { HasText = "Stuck" }).Locator("h3");
|
||||
var outboxParkedH3 = page.Locator("div.card", new() { HasText = "Parked" }).Locator("h3");
|
||||
|
||||
await AssertTileResolvedAsync(queueDepthH3, "Outbox Queue Depth");
|
||||
await AssertTileResolvedAsync(outboxStuckH3, "Outbox Stuck");
|
||||
await AssertTileResolvedAsync(outboxParkedH3, "Outbox Parked");
|
||||
|
||||
// ── Audit KPI tiles (AuditKpiTiles.razor — data-test on the button) ──
|
||||
|
||||
var auditVolume = page.Locator("[data-test='audit-kpi-volume']").Locator("h3");
|
||||
var auditErrorRate = page.Locator("[data-test='audit-kpi-error-rate']").Locator("h3");
|
||||
var auditBacklog = page.Locator("[data-test='audit-kpi-backlog']").Locator("h3");
|
||||
|
||||
await AssertTileResolvedAsync(auditVolume, "Audit Volume");
|
||||
await AssertTileResolvedAsync(auditErrorRate, "Audit Error Rate");
|
||||
await AssertTileResolvedAsync(auditBacklog, "Audit Backlog");
|
||||
|
||||
// ── Site Call KPI tiles (SiteCallKpiTiles.razor — data-test on the button) ──
|
||||
|
||||
var siteCallBuffered = page.Locator("[data-test='site-call-kpi-buffered']").Locator("h3");
|
||||
var siteCallStuck = page.Locator("[data-test='site-call-kpi-stuck']").Locator("h3");
|
||||
var siteCallParked = page.Locator("[data-test='site-call-kpi-parked']").Locator("h3");
|
||||
|
||||
await AssertTileResolvedAsync(siteCallBuffered, "Site Call Buffered");
|
||||
await AssertTileResolvedAsync(siteCallStuck, "Site Call Stuck");
|
||||
await AssertTileResolvedAsync(siteCallParked, "Site Call Parked");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits up to 20 s for <paramref name="tileH3"/> to be non-empty and not
|
||||
/// equal to the em-dash degrade placeholder. Uses Playwright web-first
|
||||
/// assertions so the retry loop is inside the Playwright engine, not a
|
||||
/// C# busy-wait.
|
||||
/// </summary>
|
||||
private static async Task AssertTileResolvedAsync(ILocator tileH3, string tileName)
|
||||
{
|
||||
// The tile must be visible and contain text before we check its value.
|
||||
await Assertions.Expect(tileH3)
|
||||
.ToBeVisibleAsync(new() { Timeout = 20_000 });
|
||||
|
||||
// Primary guard: the value must NOT be the degrade placeholder.
|
||||
await Assertions.Expect(tileH3)
|
||||
.Not.ToHaveTextAsync(DegradePlaceholder, new() { Timeout = 20_000 });
|
||||
|
||||
// Secondary guard: the value must be non-empty — it should be a digit string.
|
||||
await Assertions.Expect(tileH3)
|
||||
.Not.ToBeEmptyAsync(new() { Timeout = 20_000 });
|
||||
|
||||
// Diagnostic: capture the resolved text for context in test output, but
|
||||
// don't fail on any particular number (the cluster state is environment-
|
||||
// dependent and any non-negative integer is valid).
|
||||
var resolvedText = await tileH3.TextContentAsync();
|
||||
Assert.True(
|
||||
resolvedText != null && resolvedText.Trim().Length > 0,
|
||||
$"KPI tile '{tileName}' resolved to null/empty content.");
|
||||
Assert.False(
|
||||
resolvedText!.Trim() == DegradePlaceholder,
|
||||
$"KPI tile '{tileName}' shows the degrade placeholder '—' — singleton Ask likely hung or faulted.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user