using Microsoft.Playwright;
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
using Xunit;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment;
///
/// E2E coverage for the Deployment Status page's real-time, push-based update
/// model (Tier 2). The page (/deployment/deployments) is NOT polled: in
/// OnInitializedAsync it subscribes to
/// IDeploymentStatusNotifier.StatusChanged — a process singleton the
/// DeploymentManager raises on every deployment-record status write — and reloads
/// the table on each notification, which Blazor Server pushes to the browser over
/// its SignalR circuit. So with the page already open, a CLI instance deploy
/// makes the instance's row surface with no manual Refresh: the row simply
/// appearing IS the proof the push path works.
///
///
/// Each fact mints a fresh ephemeral instance on the real site-a (via
/// ) and deletes it in a
/// finally. The minted uniqueName is unique, so its row can never
/// pre-exist — the row's appearance is attributable solely to the deploy under test.
///
///
///
/// Why Fact B's negative is deterministic: the Pause button toggles
/// _autoRefresh, and OnDeploymentStatusChanged early-returns on
/// !_autoRefresh, so a paused page deterministically ignores the
/// StatusChanged notification. The pause click round-trips over the SignalR
/// circuit (and we wait for the button to flip to "Resume" before deploying), so
/// _autoRefresh is committed false server-side BEFORE the deploy fires
/// — the suppression is not a race. Refresh calls LoadDataAsync directly,
/// bypassing the pause guard, so the row surfaces on the explicit reload.
///
///
[Collection("Playwright")]
public class DeploymentsRealtimeTests : IClassFixture
{
private readonly PlaywrightFixture _pw;
private readonly DeploymentFixture _cluster;
public DeploymentsRealtimeTests(PlaywrightFixture pw, DeploymentFixture cluster)
{
_pw = pw;
_cluster = cluster;
}
[SkippableFact]
public async Task DeployingInstance_PushesRowWithoutManualRefresh()
{
Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);
var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
try
{
// Open the page FIRST so the StatusChanged subscription is live before we deploy.
var page = await _pw.NewAuthenticatedPageAsync();
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/deployments");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await Assertions.Expect(page.Locator("h4:has-text('Deployment Status')")).ToBeVisibleAsync();
// Deploy over the CLI — this writes deployment records and raises StatusChanged,
// which the page reloads on (push). No Refresh click here: the row appearing IS
// the proof the SignalR push path works.
await CliRunner.DeployInstanceAsync(instanceId);
var row = page.Locator("table tbody tr", new() { HasText = uniqueName });
await Assertions.Expect(row).ToBeVisibleAsync(new() { Timeout = 25_000 });
}
finally
{
await CliRunner.DeleteInstanceAsync(instanceId);
}
}
[SkippableFact]
public async Task PausedUpdates_SuppressPush_RefreshRestoresRow()
{
Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);
var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
try
{
var page = await _pw.NewAuthenticatedPageAsync();
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/deployments");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// Confirm the page loaded before interacting (symmetric with Fact A) — a failed
// load then surfaces as a clear heading-miss rather than a confusing Pause timeout.
await Assertions.Expect(page.Locator("h4:has-text('Deployment Status')")).ToBeVisibleAsync();
// Pause — the click round-trips over the circuit, so _autoRefresh is committed
// false before the deploy below. The button flipping to "Resume" proves it.
await page.Locator("button[aria-label='Pause auto-refresh']").ClickAsync();
await Assertions.Expect(page.Locator("button[aria-label='Resume auto-refresh']"))
.ToBeVisibleAsync(new() { Timeout = 5_000 });
await CliRunner.DeployInstanceAsync(instanceId);
// Paused: StatusChanged is ignored, so the row is NOT auto-added. The push chain
// (deploy write → StatusChanged → InvokeAsync re-render → SignalR diff) completes
// well under a second on a healthy cluster, so a 2s settle is ample to let any
// erroneous push manifest before we assert absence. This is the one deliberate
// fixed wait — a negative real-time assertion has no DOM event to await on the
// "stayed absent" path. The Refresh-restores assertion below is the independent
// positive guard if a loaded cluster ever makes this settle empirically tight.
var row = page.Locator("table tbody tr", new() { HasText = uniqueName });
await page.WaitForTimeoutAsync(2_000);
await Assertions.Expect(row).ToHaveCountAsync(0);
// Refresh bypasses the pause (LoadDataAsync) -> the row surfaces.
await page.Locator("button[aria-label='Refresh deployments']").ClickAsync();
await Assertions.Expect(row).ToBeVisibleAsync(new() { Timeout = 15_000 });
}
finally
{
await CliRunner.DeleteInstanceAsync(instanceId);
}
}
}