diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/InstanceConfigureTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/InstanceConfigureTests.cs new file mode 100644 index 00000000..ab0a61f9 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/InstanceConfigureTests.cs @@ -0,0 +1,71 @@ +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."); + } +}