From 560adefb0c1d9c258b71b5807114a40ab3cb0815 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 13:45:56 -0400 Subject: [PATCH] fix(centralui): Topology inline area rename commits the typed name on Enter (Gitea #38) The rename input bound _renameBuffer with the default onchange trigger. A browser fires change only after the Enter keydown (or on blur, which cancels by design), so CommitRename always saw the original name and silently no-oped: a UI rename never persisted. Present since the page was written (pre-dates 513fa1aa); the Playwright test masked it by dispatching a synthetic change event before Enter. Bind on oninput instead. Tests: bUnit InlineRename_TypeThenEnter_PersistsNewName (fails on the old binding); the Playwright rename test now types + Enter like an operator. Also fix the Playwright OpenStableTopologyAsync race: it counted 'Load instances' loaders immediately after clicking Expand all, before the circuit re-render landed, and returned an unexpanded tree; it now waits for no collapsed branch first. --- .../Pages/Deployment/Topology.razor | 5 ++++ .../Deployment/TopologyAreaTests.cs | 15 ++++++----- .../TopologyPageTests.cs | 27 +++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/Topology.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/Topology.razor index ef8156b8..afbd198a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/Topology.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/Topology.razor @@ -618,6 +618,7 @@ aria-label="@($"Rename {node.Label}")" @ref="_renameInput" @bind="_renameBuffer" + @bind:event="oninput" @onkeydown="(e) => OnRenameKeyDown(e, node)" @onblur="() => CancelRename()" /> @if (!string.IsNullOrEmpty(_renameError)) @@ -755,6 +756,10 @@ _renameError = null; } + // The buffer binds on `oninput` (Gitea #38): with the default `onchange` binding the + // browser's change event fires only AFTER the Enter keydown — so CommitRename saw the + // unchanged original name and silently cancelled — or on blur, which cancels by design. + // Binding per keystroke means Enter always commits what the operator typed. private async Task OnRenameKeyDown(KeyboardEventArgs e, TopoNode node) { if (e.Key == "Escape") diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/TopologyAreaTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/TopologyAreaTests.cs index 05966071..de857d5e 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/TopologyAreaTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/TopologyAreaTests.cs @@ -139,13 +139,10 @@ public class TopologyAreaTests : IClassFixture var input = page.Locator($"input[aria-label='Rename {original}']"); await Assertions.Expect(input).ToBeVisibleAsync(); + // Type then Enter, exactly as an operator does. The input binds per keystroke + // (`oninput`, Gitea #38) — this test used to dispatch a synthetic `change` event + // first, which masked the bug that a real Enter never committed the typed name. await input.FillAsync(renamed); - // The input is @bind="_renameBuffer" with the default onchange trigger, which a - // raw Enter keystroke does NOT fire (onchange only fires on blur in the browser) — - // and blur CANCELS the rename. So dispatch a `change` event to flush the bound - // value to the server WITHOUT losing focus, then press Enter to run CommitRename - // against the now-updated _renameBuffer. - await input.DispatchEventAsync("change"); await input.PressAsync("Enter"); // commit WITHOUT blurring (blur would cancel) await Assertions.Expect(page.Locator(".toast")).ToHaveCountAsync(1, new() { Timeout = 15_000 }); @@ -321,9 +318,15 @@ public class TopologyAreaTests : IClassFixture if (await live.IsCheckedAsync()) await live.UncheckAsync(); // stop the 15s tree reload // Lazy topology loads a bounded first page per branch. These small authoring // fixtures need their sites/areas loaded before addressing labels or instances. + // "Expand all" re-renders over the SignalR circuit, so the click returns before the + // newly-expanded branches (and their "Load instances" loaders) exist. Wait until no + // collapsed branch remains before counting loaders — counting immediately raced the + // render and returned an unexpanded tree whenever the round-trip lost. + var collapsed = page.Locator("li[role='treeitem'][aria-expanded='false']"); for (var branch = 0; branch < 300; branch++) { await page.Locator("button[aria-label='Expand all areas']").ClickAsync(); + await Assertions.Expect(collapsed).ToHaveCountAsync(0); var load = page.GetByRole(AriaRole.Button, new() { Name = "Load instances", Exact = true }); if (await load.CountAsync() == 0) return page; var pages = page.GetByText("Page 1", new() { Exact = true }); diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TopologyPageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TopologyPageTests.cs index 7d6e8e91..4444227a 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TopologyPageTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TopologyPageTests.cs @@ -449,6 +449,33 @@ public class TopologyPageTests : BunitContext Assert.NotNull(cut.Find("input.form-control-sm.d-inline-block")); } + /// + /// Gitea #38: typing a new name and pressing Enter must persist the rename. The buffer + /// used to bind on onchange, which a browser fires only after the Enter keydown + /// (or on blur, which cancels) — so the commit always saw the original name and no-oped. + /// + /// A task that represents the asynchronous operation. + [Fact] + public async Task InlineRename_TypeThenEnter_PersistsNewName() + { + var area = new Area("Line-1") { Id = 10, SiteId = 1 }; + var areasBySite = new Dictionary> { [1] = new List { area } }; + SeedRepos(sites: new[] { new Site("Plant-A", "plant-a") { Id = 1 } }, areasBySite: areasBySite); + _repo.GetAreaByIdAsync(10, Arg.Any()).Returns(area); + + var cut = RenderLoadedTopology(); + FindToggleForLabel(cut, "Plant-A")!.Click(); + cut.FindAll("span.tv-label").First(s => s.TextContent == "Line-1").DoubleClick(); + + var input = cut.Find("input[aria-label='Rename Line-1']"); + // Keystroke-level binding: no change event (that only arrives after Enter, or on blur). + input.Input("Line-1-renamed"); + await cut.Find("input[aria-label='Rename Line-1']").KeyDownAsync(new Microsoft.AspNetCore.Components.Web.KeyboardEventArgs { Key = "Enter" }); + + await _repo.Received(1).UpdateAreaAsync( + Arg.Is(a => a.Id == 10 && a.Name == "Line-1-renamed"), Arg.Any()); + } + /// Verifies that instance rows do not support double-click rename. [Fact] public void InstanceRows_DoNotHaveDoubleClickRename()