Add cross-platform OPC UA client stack: shared library, CLI tool, and Avalonia UI
Implements Client.Shared (IOpcUaClientService with connection lifecycle, failover, browse, read/write, subscriptions, alarms, history, redundancy), Client.CLI (8 CliFx commands mirroring tools/opcuacli-dotnet), and Client.UI (Avalonia desktop app with tree browser, read/write, subscriptions, alarms, and history tabs). All three target .NET 10 and are covered by 249 unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.UI.Services;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.UI.Tests.Fakes;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.UI.ViewModels;
|
||||
using BrowseResult = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult;
|
||||
using ConnectionState = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.ConnectionState;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.UI.Tests;
|
||||
|
||||
public class MainWindowViewModelTests
|
||||
{
|
||||
private readonly FakeOpcUaClientService _service;
|
||||
private readonly MainWindowViewModel _vm;
|
||||
|
||||
public MainWindowViewModelTests()
|
||||
{
|
||||
_service = new FakeOpcUaClientService
|
||||
{
|
||||
ConnectResult = new ConnectionInfo(
|
||||
"opc.tcp://localhost:4840",
|
||||
"TestServer",
|
||||
"None",
|
||||
"http://opcfoundation.org/UA/SecurityPolicy#None",
|
||||
"session-1",
|
||||
"TestSession"),
|
||||
BrowseResults = new[]
|
||||
{
|
||||
new BrowseResult("ns=2;s=Root", "Root", "Object", true)
|
||||
},
|
||||
RedundancyResult = new RedundancyInfo("None", 200, new[] { "urn:test" }, "urn:test")
|
||||
};
|
||||
|
||||
var factory = new FakeOpcUaClientServiceFactory(_service);
|
||||
var dispatcher = new SynchronousUiDispatcher();
|
||||
_vm = new MainWindowViewModel(factory, dispatcher);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultState_IsDisconnected()
|
||||
{
|
||||
_vm.ConnectionState.ShouldBe(ConnectionState.Disconnected);
|
||||
_vm.IsConnected.ShouldBeFalse();
|
||||
_vm.EndpointUrl.ShouldBe("opc.tcp://localhost:4840");
|
||||
_vm.StatusMessage.ShouldBe("Disconnected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectCommand_CanExecute_WhenDisconnected()
|
||||
{
|
||||
_vm.ConnectCommand.CanExecute(null).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisconnectCommand_CannotExecute_WhenDisconnected()
|
||||
{
|
||||
_vm.DisconnectCommand.CanExecute(null).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectCommand_TransitionsToConnected()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.ConnectionState.ShouldBe(ConnectionState.Connected);
|
||||
_vm.IsConnected.ShouldBeTrue();
|
||||
_service.ConnectCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectCommand_LoadsRootNodes()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.BrowseTree.RootNodes.Count.ShouldBe(1);
|
||||
_vm.BrowseTree.RootNodes[0].DisplayName.ShouldBe("Root");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectCommand_FetchesRedundancyInfo()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.RedundancyInfo.ShouldNotBeNull();
|
||||
_vm.RedundancyInfo!.Mode.ShouldBe("None");
|
||||
_vm.RedundancyInfo.ServiceLevel.ShouldBe((byte)200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectCommand_SetsSessionLabel()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.SessionLabel.ShouldContain("TestServer");
|
||||
_vm.SessionLabel.ShouldContain("TestSession");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisconnectCommand_TransitionsToDisconnected()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
await _vm.DisconnectCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.ConnectionState.ShouldBe(ConnectionState.Disconnected);
|
||||
_vm.IsConnected.ShouldBeFalse();
|
||||
_service.DisconnectCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Disconnect_ClearsStateAndChildren()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
await _vm.DisconnectCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.SessionLabel.ShouldBe(string.Empty);
|
||||
_vm.RedundancyInfo.ShouldBeNull();
|
||||
_vm.BrowseTree.RootNodes.ShouldBeEmpty();
|
||||
_vm.SubscriptionCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectionStateChangedEvent_UpdatesState()
|
||||
{
|
||||
_service.RaiseConnectionStateChanged(
|
||||
new ConnectionStateChangedEventArgs(ConnectionState.Disconnected, ConnectionState.Reconnecting, "opc.tcp://localhost:4840"));
|
||||
|
||||
_vm.ConnectionState.ShouldBe(ConnectionState.Reconnecting);
|
||||
_vm.StatusMessage.ShouldBe("Reconnecting...");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SelectedTreeNode_PropagatesToChildViewModels()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
var node = _vm.BrowseTree.RootNodes[0];
|
||||
_vm.SelectedTreeNode = node;
|
||||
|
||||
_vm.ReadWrite.SelectedNodeId.ShouldBe(node.NodeId);
|
||||
_vm.History.SelectedNodeId.ShouldBe(node.NodeId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectCommand_PropagatesIsConnectedToChildViewModels()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.ReadWrite.IsConnected.ShouldBeTrue();
|
||||
_vm.Subscriptions.IsConnected.ShouldBeTrue();
|
||||
_vm.Alarms.IsConnected.ShouldBeTrue();
|
||||
_vm.History.IsConnected.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisconnectCommand_PropagatesIsConnectedFalseToChildViewModels()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
await _vm.DisconnectCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.ReadWrite.IsConnected.ShouldBeFalse();
|
||||
_vm.Subscriptions.IsConnected.ShouldBeFalse();
|
||||
_vm.Alarms.IsConnected.ShouldBeFalse();
|
||||
_vm.History.IsConnected.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectFailure_RevertsToDisconnected()
|
||||
{
|
||||
_service.ConnectException = new Exception("Connection refused");
|
||||
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.ConnectionState.ShouldBe(ConnectionState.Disconnected);
|
||||
_vm.StatusMessage.ShouldContain("Connection refused");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PropertyChanged_FiredForConnectionState()
|
||||
{
|
||||
var changed = new List<string>();
|
||||
_vm.PropertyChanged += (_, e) => changed.Add(e.PropertyName!);
|
||||
|
||||
_service.RaiseConnectionStateChanged(
|
||||
new ConnectionStateChangedEventArgs(ConnectionState.Disconnected, ConnectionState.Connected, "opc.tcp://localhost:4840"));
|
||||
|
||||
changed.ShouldContain(nameof(MainWindowViewModel.ConnectionState));
|
||||
changed.ShouldContain(nameof(MainWindowViewModel.IsConnected));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user