Associate ScadaBridge controls, guidance and editor errors
This commit is contained in:
@@ -297,10 +297,10 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
// The method pane is the shared ParameterValueForm (typed inputs by field id), and
|
||||
// Run is gated on its validity — MachineCode is required, so Run stays off until it is set.
|
||||
Assert.NotEmpty(cut.FindAll("[data-test='flow-method-params']"));
|
||||
Assert.NotNull(cut.Find("#param-MachineCode"));
|
||||
Assert.NotNull(cut.Find("#param-Qty"));
|
||||
Assert.NotNull(cut.Find("[data-param-path='MachineCode']"));
|
||||
Assert.NotNull(cut.Find("[data-param-path='Qty']"));
|
||||
Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
cut.Find("#param-MachineCode").Input("Z28061");
|
||||
cut.Find("[data-param-path='MachineCode']").Input("Z28061");
|
||||
cut.WaitForAssertion(() => Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")));
|
||||
}
|
||||
|
||||
@@ -316,8 +316,8 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
cut.Find("select[data-test='flow-method-select']").Change("4");
|
||||
FillRequiredMethodInput(cut);
|
||||
|
||||
cut.Find("#param-MachineCode").Input("Z28061");
|
||||
cut.Find("#param-Qty").Input("3");
|
||||
cut.Find("[data-param-path='MachineCode']").Input("Z28061");
|
||||
cut.Find("[data-param-path='Qty']").Input("3");
|
||||
cut.Find("button[data-test='flow-run']").Click();
|
||||
|
||||
cut.WaitForAssertion(() => Assert.Equal("Completed", cut.Find("[data-test='flow-state']").TextContent.Trim()));
|
||||
@@ -791,7 +791,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
|
||||
Assert.NotEmpty(cut.FindAll("[data-test='flow-script-params']"));
|
||||
Assert.Empty(cut.FindAll("input[data-test='flow-input-value']"));
|
||||
cut.Find("#param-n").Input("3");
|
||||
cut.Find("[data-param-path='n']").Input("3");
|
||||
cut.Find("button[data-test='flow-run']").Click();
|
||||
cut.WaitForAssertion(() => Assert.Equal("Completed", cut.Find("[data-test='flow-state']").TextContent.Trim()));
|
||||
_ = _flow.Received(1).RunScriptAsync(7, "Manual", Arg.Is<JsonElement?>(p => p != null && p.Value.GetProperty("n").GetInt64() == 3), null, Arg.Any<CancellationToken>());
|
||||
@@ -851,7 +851,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
cut.Find("select[data-test='flow-site-select']").Change("1");
|
||||
PickInstance(cut, 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
|
||||
cut.Find("[data-param-path='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;
|
||||
}
|
||||
@@ -868,10 +868,10 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
|
||||
// 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.Equal(string.Empty, cut.Find("[data-param-path='n']").GetAttribute("value"));
|
||||
Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
|
||||
cut.Find("#param-n").Input("5");
|
||||
cut.Find("[data-param-path='n']").Input("5");
|
||||
Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
}
|
||||
|
||||
@@ -1298,7 +1298,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
/// </summary>
|
||||
private static void FillRequiredMethodInput(IRenderedComponent<FlowDebuggerPage> cut)
|
||||
{
|
||||
cut.WaitForElement("#param-MachineCode").Input("Z28061");
|
||||
cut.WaitForElement("[data-param-path='MachineCode']").Input("Z28061");
|
||||
cut.WaitForAssertion(() => Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")));
|
||||
}
|
||||
|
||||
@@ -1314,16 +1314,16 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
.Returns(GraphOutcome(withSite: true, withChildId: true));
|
||||
var cut = Render<FlowDebuggerPage>();
|
||||
cut.Find("select[data-test='flow-method-select']").Change("5");
|
||||
cut.WaitForElement("#param-SAPID");
|
||||
cut.WaitForElement("[data-param-path='SAPID']");
|
||||
|
||||
// Required array unset → Run off. Add two work orders through the list editor.
|
||||
Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
cut.Find("#param-SAPID").Input("103559_A");
|
||||
cut.Find("[data-param-path='SAPID']").Input("103559_A");
|
||||
cut.Find("button.param-list-add").Click();
|
||||
cut.Find("button.param-list-add").Click();
|
||||
cut.Find("[id='param-WorkOrders[0].WorkOrderNumber']").Input("WO-1");
|
||||
cut.Find("[id='param-WorkOrders[0].PartNumber']").Input("P-1");
|
||||
cut.Find("[id='param-WorkOrders[1].WorkOrderNumber']").Input("WO-2");
|
||||
cut.Find("[data-param-path='WorkOrders[0].WorkOrderNumber']").Input("WO-1");
|
||||
cut.Find("[data-param-path='WorkOrders[0].PartNumber']").Input("P-1");
|
||||
cut.Find("[data-param-path='WorkOrders[1].WorkOrderNumber']").Input("WO-2");
|
||||
cut.WaitForAssertion(() => Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")));
|
||||
|
||||
cut.Find("button[data-test='flow-run']").Click();
|
||||
@@ -1346,14 +1346,14 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
RegisterServices();
|
||||
var cut = Render<FlowDebuggerPage>();
|
||||
cut.Find("select[data-test='flow-method-select']").Change("5");
|
||||
cut.WaitForElement("#param-SAPID");
|
||||
cut.Find("#param-SAPID").Input("103559_A");
|
||||
cut.WaitForElement("[data-param-path='SAPID']");
|
||||
cut.Find("[data-param-path='SAPID']").Input("103559_A");
|
||||
cut.Find("button.param-list-add").Click();
|
||||
// An item exists but its required WorkOrderNumber is unset: still invalid.
|
||||
cut.Find("[id='param-WorkOrders[0].PartNumber']").Input("P-1");
|
||||
cut.Find("[data-param-path='WorkOrders[0].PartNumber']").Input("P-1");
|
||||
|
||||
Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
cut.Find("[id='param-WorkOrders[0].WorkOrderNumber']").Input("WO-1");
|
||||
cut.Find("[data-param-path='WorkOrders[0].WorkOrderNumber']").Input("WO-1");
|
||||
cut.WaitForAssertion(() => Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")));
|
||||
}
|
||||
|
||||
@@ -1401,7 +1401,7 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
.Returns(GraphOutcome(withSite: true, withChildId: true));
|
||||
var cut = Render<FlowDebuggerPage>();
|
||||
cut.Find("select[data-test='flow-method-select']").Change("5");
|
||||
cut.WaitForElement("#param-SAPID");
|
||||
cut.WaitForElement("[data-param-path='SAPID']");
|
||||
|
||||
cut.Find("button[data-test='flow-input-mode-raw']").Click();
|
||||
var editor = cut.FindComponent<MonacoEditor>();
|
||||
@@ -1412,9 +1412,9 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
|
||||
// Back to the form: the parsed tree is what the item editors now show.
|
||||
cut.Find("button[data-test='flow-input-mode-form']").Click();
|
||||
cut.WaitForElement("[id='param-WorkOrders[0].WorkOrderNumber']");
|
||||
Assert.Equal("WO-1", cut.Find("[id='param-WorkOrders[0].WorkOrderNumber']").GetAttribute("value"));
|
||||
Assert.Equal("103559_A", cut.Find("#param-SAPID").GetAttribute("value"));
|
||||
cut.WaitForElement("[data-param-path='WorkOrders[0].WorkOrderNumber']");
|
||||
Assert.Equal("WO-1", cut.Find("[data-param-path='WorkOrders[0].WorkOrderNumber']").GetAttribute("value"));
|
||||
Assert.Equal("103559_A", cut.Find("[data-param-path='SAPID']").GetAttribute("value"));
|
||||
cut.WaitForAssertion(() => Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")));
|
||||
|
||||
cut.Find("button[data-test='flow-run']").Click();
|
||||
@@ -1436,9 +1436,9 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
|
||||
cut.Find("select[data-test='flow-method-select']").Change("5");
|
||||
|
||||
cut.WaitForElement("#param-SAPID");
|
||||
Assert.Empty(cut.FindAll("#param-MachineCode"));
|
||||
Assert.Equal("", cut.Find("#param-SAPID").GetAttribute("value"));
|
||||
cut.WaitForElement("[data-param-path='SAPID']");
|
||||
Assert.Empty(cut.FindAll("[data-param-path='MachineCode']"));
|
||||
Assert.Equal("", cut.Find("[data-param-path='SAPID']").GetAttribute("value"));
|
||||
Assert.Empty(cut.FindAll(".param-list-row"));
|
||||
Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
}
|
||||
@@ -1453,8 +1453,8 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
var cut = Render<FlowDebuggerPage>();
|
||||
cut.Find("select[data-test='flow-method-select']").Change("4");
|
||||
|
||||
cut.WaitForAssertion(() => Assert.Equal("Z9", cut.Find("#param-MachineCode").GetAttribute("value")));
|
||||
Assert.Equal("7", cut.Find("#param-Qty").GetAttribute("value"));
|
||||
cut.WaitForAssertion(() => Assert.Equal("Z9", cut.Find("[data-param-path='MachineCode']").GetAttribute("value")));
|
||||
Assert.Equal("7", cut.Find("[data-param-path='Qty']").GetAttribute("value"));
|
||||
cut.WaitForAssertion(() => Assert.False(cut.Find("button[data-test='flow-run']").HasAttribute("disabled")));
|
||||
}
|
||||
|
||||
@@ -1470,8 +1470,8 @@ public class FlowDebuggerPageTests : BunitContext
|
||||
var cut = Render<FlowDebuggerPage>();
|
||||
cut.Find("select[data-test='flow-method-select']").Change("4");
|
||||
|
||||
cut.WaitForElement("#param-MachineCode");
|
||||
Assert.Equal("", cut.Find("#param-MachineCode").GetAttribute("value"));
|
||||
cut.WaitForElement("[data-param-path='MachineCode']");
|
||||
Assert.Equal("", cut.Find("[data-param-path='MachineCode']").GetAttribute("value"));
|
||||
Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -34,6 +34,27 @@ public class ParameterValueFormMonacoSchemaTests : BunitContext
|
||||
Services.AddSingleton(_library);
|
||||
}
|
||||
|
||||
/// <summary>Raw JSON errors describe the named composite and disappear after correction.</summary>
|
||||
[Fact]
|
||||
public async Task EscapeHatch_ParseErrorIsLinkedToNamedGroupUntilCorrection()
|
||||
{
|
||||
const string schema = """{"type":"object","properties":{"payload":{"$ref":"lib:Missing"}}}""";
|
||||
var cut = Render<ParameterValueForm>(p => p.Add(c => c.ParameterDefinitions, schema));
|
||||
var editor = cut.FindComponent<MonacoEditor>();
|
||||
Assert.Equal("Parameter payload JSON", editor.Instance.AriaLabel);
|
||||
await cut.InvokeAsync(() => editor.Instance.ValueChanged.InvokeAsync("{bad"));
|
||||
var group = cut.Find(".param-json-editor");
|
||||
Assert.Equal("group", group.GetAttribute("role"));
|
||||
Assert.Equal("Parameter payload JSON", group.GetAttribute("aria-label"));
|
||||
Assert.Equal("true", group.GetAttribute("aria-invalid"));
|
||||
Assert.Contains("JSON parse error", cut.Find("#" + group.GetAttribute("aria-describedby")).TextContent);
|
||||
await cut.InvokeAsync(() => editor.Instance.ValueChanged.InvokeAsync("{}"));
|
||||
group = cut.Find(".param-json-editor");
|
||||
Assert.Equal("false", group.GetAttribute("aria-invalid"));
|
||||
Assert.Null(group.GetAttribute("aria-describedby"));
|
||||
Assert.Equal("Parameter payload JSON", cut.FindComponent<MonacoEditor>().Instance.AriaLabel);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the case: escape Hatch, renders Monaco Json Editor, not Plain Textarea.</summary>
|
||||
[Fact]
|
||||
public void EscapeHatch_RendersMonacoJsonEditor_NotPlainTextarea()
|
||||
|
||||
@@ -28,6 +28,23 @@ public class ParameterValueFormTests : BunitContext
|
||||
Services.AddSingleton(_library);
|
||||
}
|
||||
|
||||
/// <summary>Repeated forms have distinct IDs and parse errors stay linked until correction.</summary>
|
||||
[Fact]
|
||||
public void RepeatedForms_HaveUniqueIdsAndLinkedParseErrors()
|
||||
{
|
||||
const string schema = """{"type":"object","properties":{"n":{"type":"integer"}}}""";
|
||||
var first = Render<ParameterValueForm>(p => p.Add(c => c.ParameterDefinitions, schema));
|
||||
var second = Render<ParameterValueForm>(p => p.Add(c => c.ParameterDefinitions, schema));
|
||||
var input = first.Find("input");
|
||||
Assert.NotEqual(input.Id, second.Find("input").Id);
|
||||
input.Input("abc");
|
||||
Assert.Equal("true", input.GetAttribute("aria-invalid"));
|
||||
Assert.Contains("Not a valid integer", first.Find("#" + input.GetAttribute("aria-describedby")).TextContent);
|
||||
input.Input("5");
|
||||
Assert.Equal("false", input.GetAttribute("aria-invalid"));
|
||||
Assert.Null(input.GetAttribute("aria-describedby"));
|
||||
}
|
||||
|
||||
private const string ObjectParamSchema = """
|
||||
{
|
||||
"type": "object",
|
||||
@@ -300,7 +317,7 @@ public class ParameterValueFormTests : BunitContext
|
||||
.Add(x => x.ParameterDefinitions, """{"type":"object","properties":{"n":{"type":"integer"}}}""")
|
||||
.Add(x => x.ValidChanged, v => valid.Add(v)));
|
||||
|
||||
var input = cut.Find("#param-n");
|
||||
var input = cut.Find("input[type=number]");
|
||||
input.Input("abc");
|
||||
Assert.False(valid.Last());
|
||||
input.Input("5");
|
||||
|
||||
@@ -22,6 +22,21 @@ public class OpcUaEndpointEditorTests : BunitContext
|
||||
Services.AddSingleton<ICertManagementService>(Substitute.For<ICertManagementService>());
|
||||
}
|
||||
|
||||
/// <summary>Existing endpoint validation is linked to its control and clears on correction.</summary>
|
||||
[Fact]
|
||||
public void EndpointError_IsDescribedAndInvalidUntilValidationClears()
|
||||
{
|
||||
var errors = ValidationResult.FromErrors(new ValidationEntry { EntityName = "EndpointUrl", Message = "Endpoint is required" });
|
||||
var cut = Render<OpcUaEndpointEditor>(p => p.Add(c => c.Config, new OpcUaEndpointConfig()).Add(c => c.Errors, errors));
|
||||
var input = cut.Find("input[placeholder='opc.tcp://host:4840']");
|
||||
Assert.Equal("true", input.GetAttribute("aria-invalid"));
|
||||
var errorId = input.GetAttribute("aria-describedby");
|
||||
Assert.Equal("Endpoint is required", cut.Find("#" + errorId).TextContent);
|
||||
cut.Render(p => p.Add(c => c.Errors, (ValidationResult?)null));
|
||||
Assert.Equal("false", cut.Find("input[placeholder='opc.tcp://host:4840']").GetAttribute("aria-invalid"));
|
||||
Assert.Null(cut.Find("input[placeholder='opc.tcp://host:4840']").GetAttribute("aria-describedby"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the editor renders all four section labels.</summary>
|
||||
[Fact]
|
||||
public void Renders_All_Four_Section_Labels()
|
||||
|
||||
@@ -305,7 +305,12 @@ public class TransportExportPageTests : BunitContext
|
||||
var passphraseInput = cut.Find("#passphrase");
|
||||
passphraseInput.Input("hunter2hunter2");
|
||||
var confirmInput = cut.Find("#passphrase-confirm");
|
||||
confirmInput.Input("different");
|
||||
Assert.Equal("true", confirmInput.GetAttribute("aria-invalid"));
|
||||
Assert.Contains("do not match", cut.Find("#" + confirmInput.GetAttribute("aria-describedby")).TextContent);
|
||||
confirmInput.Input("hunter2hunter2");
|
||||
Assert.Equal("false", confirmInput.GetAttribute("aria-invalid"));
|
||||
Assert.Null(confirmInput.GetAttribute("aria-describedby"));
|
||||
|
||||
// Click "Export" — the only enabled button labeled "Export" at this step.
|
||||
await cut.FindAll("button").First(b => b.TextContent.Trim() == "Export").ClickAsync(new());
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
|
||||
|
||||
/// <summary>Control names remain associated as search and selection values change.</summary>
|
||||
public class ControlLabelTests : BunitContext
|
||||
{
|
||||
/// <summary>Visible DataTable search label targets its instance-specific input.</summary>
|
||||
[Fact]
|
||||
public void TableSearch_HasAssociatedLabelAfterTyping()
|
||||
{
|
||||
var cut = Render<DataTable<string>>(p => p.Add(c => c.Items, new[] { "one", "two" })
|
||||
.Add(c => c.ShowSearch, true).Add(c => c.HeaderContent, (RenderFragment)(b => b.AddContent(0, "Value")))
|
||||
.Add(c => c.RowContent, (RenderFragment<string>)(v => b => b.AddContent(0, v))));
|
||||
var input = cut.Find("input");
|
||||
Assert.False(string.IsNullOrEmpty(input.Id));
|
||||
Assert.Equal("Search", cut.Find("label[for='" + input.Id + "']").TextContent.Trim());
|
||||
input.Input("one");
|
||||
Assert.NotNull(cut.Find("label[for='" + cut.Find("input").Id + "']"));
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,17 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
|
||||
/// </summary>
|
||||
public class MonacoEditorLoggingTests : BunitContext
|
||||
{
|
||||
/// <summary>Editor creation supplies a real editor accessibility name.</summary>
|
||||
[Fact]
|
||||
public void CreateEditor_SuppliesAccessibleName()
|
||||
{
|
||||
Services.AddLogging();
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
Render<MonacoEditor>();
|
||||
var options = JSInterop.Invocations.Single(x => x.Identifier == "MonacoBlazor.createEditor").Arguments[2]!;
|
||||
Assert.Equal("Code editor", options.GetType().GetProperty("ariaLabel")?.GetValue(options));
|
||||
}
|
||||
|
||||
/// <summary>Captures log entries so the test can assert on them.</summary>
|
||||
private sealed class CapturingLoggerProvider : ILoggerProvider
|
||||
{
|
||||
|
||||
@@ -62,6 +62,23 @@ public class TreeViewMultiSelectTests : BunitContext
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Each checkbox names its visible node, with IDs unique across repeated trees.</summary>
|
||||
[Fact]
|
||||
public void CheckboxNames_ReferenceVisibleNodeAndRemainUniqueAcrossTrees()
|
||||
{
|
||||
var first = RenderCheckboxTree();
|
||||
var second = RenderCheckboxTree();
|
||||
var ids = new HashSet<string>();
|
||||
foreach (var tree in new[] { first, second })
|
||||
foreach (var box in tree.FindAll("input.tv-checkbox"))
|
||||
{
|
||||
var id = box.GetAttribute("aria-labelledby");
|
||||
Assert.False(string.IsNullOrWhiteSpace(id));
|
||||
Assert.True(ids.Add(id!));
|
||||
Assert.Contains(tree.Find("#" + id).TextContent.Trim(), new[] { "Folder1", "Folder2", "Leaf1a", "Leaf1b", "Leaf2a" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that checkbox mode renders a checkbox input for each node.</summary>
|
||||
[Fact]
|
||||
public void Checkbox_mode_renders_input_checkbox_per_node()
|
||||
|
||||
Reference in New Issue
Block a user