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")); + } }