using Bunit; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared; using ZB.MOM.WW.ScadaBridge.CentralUI.Services; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared; /// /// bUnit tests for the (M9-T24b). The dialog /// surfaces a target-site picker (excluding the connection's current site) and, on /// confirm, dispatches a MoveDataConnectionCommand through the management /// path via — the SAME guard-running seam /// the server uses. A guard error must be shown inline and the dialog must stay open; /// a success must close the dialog and raise the refresh signal. /// /// The service is substituted so the tests capture the dispatched (connectionId, /// targetSiteId) and simulate both a success and a guard-error response without a /// real ManagementActor in scope. /// public class MoveDataConnectionDialogTests : BunitContext { private readonly IDataConnectionMoveService _service = Substitute.For(); public MoveDataConnectionDialogTests() { Services.AddSingleton(_service); } private IRenderedComponent RenderDialog( int connectionId = 100, string connectionName = "PLC-1", bool visible = true, IEnumerable<(int Id, string Label)>? siteOptions = null, EventCallback? onMoved = null, EventCallback? visibleChanged = null) { siteOptions ??= new[] { (2, "Plant-B"), (3, "Plant-C") }; return Render(p => p .Add(d => d.IsVisible, visible) .Add(d => d.ConnectionId, connectionId) .Add(d => d.ConnectionName, connectionName) .Add(d => d.SiteOptions, siteOptions) .Add(d => d.OnMoved, onMoved ?? default) .Add(d => d.IsVisibleChanged, visibleChanged ?? default)); } [Fact] public void Visible_RendersTargetSitePicker_WithSuppliedOptions() { // (a) The dialog opens with a site picker whose options are exactly the // supplied target sites (the page excludes the connection's current site). var cut = RenderDialog(siteOptions: new[] { (2, "Plant-B"), (3, "Plant-C") }); var options = cut.FindAll("select option"); Assert.Contains(options, o => o.TextContent.Contains("Plant-B")); Assert.Contains(options, o => o.TextContent.Contains("Plant-C")); // Picker reflects the connection name in the header. Assert.Contains("PLC-1", cut.Markup); } [Fact] public void Hidden_RendersNothing() { var cut = RenderDialog(visible: false); Assert.Empty(cut.Markup.Trim()); } [Fact] public void Confirm_DispatchesMoveCommand_WithConnectionAndTargetSiteIds() { // (b) Confirming dispatches the move through the management-dispatch service // with the connection id + the selected target site id. _service.MoveAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(DataConnectionMoveResult.Ok())); var cut = RenderDialog(connectionId: 100, siteOptions: new[] { (2, "Plant-B"), (3, "Plant-C") }); // Pick Plant-C (id 3) and confirm. cut.Find("select").Change("3"); cut.FindAll("button").First(b => b.TextContent.Contains("Move")).Click(); _service.Received(1).MoveAsync(100, 3, Arg.Any()); } [Fact] public void GuardError_IsShownInline_AndDialogStaysOpen() { // (c) A guard-error response is rendered inline and the dialog does NOT close // (IsVisibleChanged is not raised with false; OnMoved is not raised). const string guardError = "Cannot move data connection 'PLC-1' (ID 100): it is referenced by 1 instance binding(s)."; _service.MoveAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(DataConnectionMoveResult.Fail(guardError))); var closed = false; var moved = false; var cut = RenderDialog( connectionId: 100, siteOptions: new[] { (2, "Plant-B") }, onMoved: EventCallback.Factory.Create(this, () => moved = true), visibleChanged: EventCallback.Factory.Create(this, v => { if (!v) closed = true; })); cut.Find("select").Change("2"); cut.FindAll("button").First(b => b.TextContent.Contains("Move")).Click(); Assert.Contains(guardError, cut.Markup); Assert.False(closed, "Dialog must stay open on a guard error."); Assert.False(moved, "OnMoved must not fire on a guard error."); } [Fact] public void Success_ClosesDialog_AndRaisesRefreshSignal() { _service.MoveAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(DataConnectionMoveResult.Ok())); var closed = false; var moved = false; var cut = RenderDialog( connectionId: 100, siteOptions: new[] { (2, "Plant-B") }, onMoved: EventCallback.Factory.Create(this, () => moved = true), visibleChanged: EventCallback.Factory.Create(this, v => { if (!v) closed = true; })); cut.Find("select").Change("2"); cut.FindAll("button").First(b => b.TextContent.Contains("Move")).Click(); Assert.True(closed, "Dialog must close on success."); Assert.True(moved, "OnMoved must fire on success to refresh the tree."); } }