using System.Text.Json; using Microsoft.Playwright; using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster; namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment; /// /// E2E round-trip for the InstanceConfigure page's Connection Bindings panel. The /// provisions a zztest template whose single /// bindable Double attribute carries a DataSourceReference (so it appears /// in the bindings panel), a zztest data-connection on site-a, a zztest area, and a /// non-deployed instance. This fact drives the page's bulk-assign UI to bind every /// data-sourced attribute to the fixture connection, saves, and then verifies the bind /// actually persisted via a CLI instance get read-back — not just the toast. /// /// /// Selector note: the bulk select (data-test='binding-bulk-select') is bound to /// _bulkConnectionId (an int), and its option VALUES are connection ids while the /// option TEXT is "{name} ({protocol})". Selecting by VALUE = the connection id is /// the robust choice (it doesn't depend on the connection's protocol suffix in the label). /// The bulk row only renders when there is at least one data-sourced attribute AND at /// least one site connection — both guaranteed by the fixture — so it is always present /// here. /// /// [Collection("Playwright")] public sealed class InstanceConfigureTests : IClassFixture { private readonly PlaywrightFixture _fixture; private readonly InstanceConfigureFixture _cfg; public InstanceConfigureTests(PlaywrightFixture fixture, InstanceConfigureFixture cfg) { _fixture = fixture; _cfg = cfg; } [SkippableFact] public async Task BindAllAttributes_SavesAndPersists() { Skip.IfNot(_cfg.Available, ClusterAvailability.SkipReason); var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password"); await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/instances/{_cfg.InstanceId}/configure"); // This is a Blazor Server page: it renders a LoadingSpinner while OnInitializedAsync // loads the template attributes + site connections, then re-renders the bindings // panel (the bulk select renders only once both lists are non-empty). Settle the // initial load (NetworkIdle) and web-first wait for the bulk select before driving it, // so the interaction never races the post-load re-render. await page.WaitForLoadStateAsync(LoadState.NetworkIdle); var bulkSelect = page.Locator("[data-test='binding-bulk-select']"); await Assertions.Expect(bulkSelect).ToBeVisibleAsync(new() { Timeout = 15_000 }); // Bulk-assign every bindable attribute to the fixture connection, then Apply + Save. // Select by VALUE (the connection id) — most robust, since the select binds _bulkConnectionId. await bulkSelect.SelectOptionAsync(new SelectOptionValue { Value = _cfg.ConnectionId.ToString() }); await page.GetByRole(AriaRole.Button, new() { Name = "Apply" }).ClickAsync(); await page.GetByRole(AriaRole.Button, new() { Name = "Save Bindings" }).ClickAsync(); await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 }); // Verify persistence via CLI read-back (not just the toast). using var doc = await CliRunner.GetInstanceDocumentAsync(_cfg.InstanceId); var bindings = doc.RootElement.GetProperty("connectionBindings"); var bound = bindings.EnumerateArray().Any(b => b.GetProperty("attributeName").GetString() == _cfg.AttributeName && b.GetProperty("dataConnectionId").GetInt32() == _cfg.ConnectionId); Assert.True(bound, "Expected the Value attribute to be bound to the fixture connection after Save Bindings."); } /// /// Round-trips an attribute override through the Attribute Overrides card. The /// override input carries no data-test hook, so it is located structurally: the /// overrides card is the one whose Save button reads "Save Overrides"; inside it, the /// table row whose label cell holds the attribute name (_cfg.AttributeName = "Value") /// owns the type=text input.form-control-sm. Fills a sentinel value, saves, asserts /// exactly one toast, then verifies the override persisted via a CLI instance get /// read-back (not just the toast). /// [SkippableFact] public async Task SaveOverride_RoundTrips() { Skip.IfNot(_cfg.Available, ClusterAvailability.SkipReason); var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password"); await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/instances/{_cfg.InstanceId}/configure"); // Blazor Server page renders a LoadingSpinner first; web-first wait for the overrides // section's Save button before driving the input so we never race the post-load re-render. await page.WaitForLoadStateAsync(LoadState.NetworkIdle); var saveOverrides = page.GetByRole(AriaRole.Button, new() { Name = "Save Overrides" }); await Assertions.Expect(saveOverrides).ToBeVisibleAsync(new() { Timeout = 15_000 }); // Scope to the Attribute Overrides card (the one containing the "Save Overrides" button), // pick the row whose label cell text is the attribute name, then its text input. var overridesCard = page.Locator("div.card", new() { Has = saveOverrides }); var overrideInput = overridesCard .GetByRole(AriaRole.Row, new() { Name = _cfg.AttributeName }) .Locator("input.form-control-sm[type='text']"); await overrideInput.FillAsync("zztest-override-42"); await saveOverrides.ClickAsync(); await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 }); // Verify persistence via CLI read-back (not just the toast). using var doc = await CliRunner.GetInstanceDocumentAsync(_cfg.InstanceId); var overrides = doc.RootElement.GetProperty("attributeOverrides"); var saved = overrides.EnumerateArray().Any(o => o.GetProperty("attributeName").GetString() == _cfg.AttributeName && o.GetProperty("overrideValue").GetString() == "zztest-override-42"); Assert.True(saved, "Expected the Value attribute override to persist after Save Overrides."); } /// /// Reassigns the (initially area-less) fixture instance to the fixture area via the /// Area Assignment card. Drives the existing data-test='area-select' hook by /// VALUE (the area id, since the select binds the area id), clicks "Set Area", asserts one /// toast, and verifies the new areaId via a CLI instance get read-back. This /// mutates the shared fixture instance's area, but is independent of the other tests (each /// gets a fresh page and asserts only on its own effect). /// [SkippableFact] public async Task SetArea_RoundTrips() { Skip.IfNot(_cfg.Available, ClusterAvailability.SkipReason); var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password"); await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/instances/{_cfg.InstanceId}/configure"); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); var areaSelect = page.Locator("[data-test='area-select']"); await Assertions.Expect(areaSelect).ToBeVisibleAsync(new() { Timeout = 15_000 }); // Select by VALUE = the area id (the select binds _reassignAreaId). await areaSelect.SelectOptionAsync(new SelectOptionValue { Value = _cfg.AreaId.ToString() }); await page.GetByRole(AriaRole.Button, new() { Name = "Set Area" }).ClickAsync(); await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 }); // Verify persistence: areaId must equal the fixture area after Set Area (it may have been // null/absent before). using var doc = await CliRunner.GetInstanceDocumentAsync(_cfg.InstanceId); Assert.True(doc.RootElement.TryGetProperty("areaId", out var areaIdEl) && areaIdEl.ValueKind == JsonValueKind.Number, "Expected areaId to be a number after Set Area."); Assert.Equal(_cfg.AreaId, areaIdEl.GetInt32()); } /// /// Not-found edge: navigating to a configure URL for a non-existent instance id surfaces the /// page's error alert (data-test='instance-error-alert') carrying the /// $"Instance #{Id} not found." message built in InstanceConfigure.OnInitializedAsync. /// [SkippableFact] public async Task NotFoundInstance_ShowsErrorAlert() { Skip.IfNot(_cfg.Available, ClusterAvailability.SkipReason); var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password"); await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/instances/999999999/configure"); var errorAlert = page.Locator("[data-test='instance-error-alert']"); await Assertions.Expect(errorAlert).ToBeVisibleAsync(new() { Timeout = 10_000 }); await Assertions.Expect(errorAlert).ToContainTextAsync("not found"); } // TODO(wave-N): alarm-override UI coverage — needs a template-with-alarm fixture (template alarms are not CLI-provisionable today). }