From a2bfc79e3a8ca31b4cfbd7c9b921b8f0b23430af Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 8 Sep 2026 01:32:29 -0400 Subject: [PATCH] =?UTF-8?q?fix(central-ui):=20Flow=20Debugger=20=E2=80=94?= =?UTF-8?q?=20remount=20ParameterValueForm=20per=20script=20so=20stale=20f?= =?UTF-8?q?orm=20state=20cannot=20block=20Run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29 --- .../Pages/Deployment/FlowDebugger.razor | 7 ++- .../Pages/Deployment/FlowDebugger.razor.cs | 5 ++ .../Deployment/FlowDebuggerPageTests.cs | 60 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor index 1809db49..67469b3c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor @@ -235,7 +235,12 @@ else {
- diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs index ccacf1a1..b61cf3d0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs @@ -330,6 +330,11 @@ public partial class FlowDebugger : ComponentBase, IDisposable ResetResult(); _predicted = null; _state = FlowState.Idle; + // Inputs are per-entry-point, as in OnScriptChangedAsync: never carry a script's value or + // parameter tree (or its validity) across a switch of entry kind. + _scriptParams = new(); + _scriptParamsValid = false; + _valueChangeValue = string.Empty; await PersistAsync(StorageEntry, entry.ToString()); if (_entry == EntryKind.Method && _selectedMethodId > 0) await OnMethodChangedAsync(); diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/FlowDebuggerPageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/FlowDebuggerPageTests.cs index 56569c73..ccc22fea 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/FlowDebuggerPageTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/FlowDebuggerPageTests.cs @@ -691,4 +691,64 @@ public class FlowDebuggerPageTests : BunitContext cut.WaitForAssertion(() => Assert.Equal("Manual", cut.Find("select[data-test='flow-script-select']").GetAttribute("value"))); _ = _flow.Received().PredictScriptAsync(7, "Manual", Arg.Any()); } + + /// + /// Two call-kind scripts declaring BYTE-IDENTICAL parameter definitions — the case that makes + /// ParameterValueForm skip its reparse (and its ValidChanged) and keeps the previous + /// script's raw text and parse errors. The page must remount the form per script instead. + /// + private static readonly FlowScriptOption Manual2 = new("Manual2", "Call", null, + """{"type":"object","properties":{"n":{"type":"integer"}},"required":["n"]}""", 20); + + private void WireInstanceWithTwinScripts() + { + _templates.GetInstancesBySiteIdAsync(1, Arg.Any()) + .Returns(Task.FromResult>([new Instance("Sim01") { Id = 7, SiteId = 1, TemplateId = 10 }])); + _flow.GetInstanceScriptsAsync(7, Arg.Any()) + .Returns(Task.FromResult>([Manual, Manual2, Tick])); + _flow.PredictScriptAsync(7, Arg.Any(), Arg.Any()).Returns(ThreeNodePrediction()); + } + + private IRenderedComponent ChooseScriptWithAnInvalidParameter() + { + var cut = Render(); + cut.Find("button[data-test='flow-entry-script']").Click(); + cut.Find("select[data-test='flow-site-select']").Change("1"); + cut.Find("select[data-test='flow-instance-select']").Change("7"); + cut.Find("select[data-test='flow-script-select']").Change("Manual"); + cut.Find("#param-n").Input("abc"); // not an integer: the form parks a parse error + Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")); + return cut; + } + + [Fact] + public void ScriptEntry_SwitchingBetweenCallKindScripts_ResetsTheFormAndValidity() + { + RegisterServices(); + WireInstanceWithTwinScripts(); + var cut = ChooseScriptWithAnInvalidParameter(); + + cut.Find("select[data-test='flow-script-select']").Change("Manual2"); + + // A fresh form: no echo of the previous script's text, and no stale parse error keeping + // Run disabled invisibly — Run is disabled only because the required field is unset. + Assert.Equal(string.Empty, cut.Find("#param-n").GetAttribute("value")); + Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")); + + cut.Find("#param-n").Input("5"); + Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")); + } + + [Fact] + public void ScriptEntry_SwitchingFromAnInvalidFormToANoParameterScript_EnablesRun() + { + RegisterServices(); + WireInstanceWithTwinScripts(); + var cut = ChooseScriptWithAnInvalidParameter(); + + cut.Find("select[data-test='flow-script-select']").Change("Tick"); + + Assert.Contains("takes no parameters", cut.Find("[data-test='flow-inputs']").TextContent); + Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")); + } }