From fecac45d055ae8df5731dbe0877948a5011a4528 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 6 Jun 2026 11:58:45 -0400 Subject: [PATCH] test(e2e): InstanceConfigure attribute-override + area reassignment + not-found edge --- .../Deployment/InstanceConfigureTests.cs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/InstanceConfigureTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/InstanceConfigureTests.cs index ab0a61f9..8dd8faea 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/InstanceConfigureTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/InstanceConfigureTests.cs @@ -68,4 +68,103 @@ public sealed class InstanceConfigureTests : IClassFixture + /// 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). }