Add tree context menu, missing connection settings, and fix lazy-load browse
Right-click on browse tree nodes to Subscribe (multi-select) or View History (Variable nodes only), with automatic tab switching. Add missing UI controls for failover URLs, session timeout, auto-accept certificates, and certificate store path. Fix tree expansion by adding two-way IsExpanded binding on TreeViewItem. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
public int HistoryReadAggregateCallCount { get; private set; }
|
||||
public int GetRedundancyInfoCallCount { get; private set; }
|
||||
|
||||
public ConnectionSettings? LastConnectionSettings { get; private set; }
|
||||
public NodeId? LastReadNodeId { get; private set; }
|
||||
public NodeId? LastWriteNodeId { get; private set; }
|
||||
public object? LastWriteValue { get; private set; }
|
||||
@@ -60,6 +61,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
public Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default)
|
||||
{
|
||||
ConnectCallCount++;
|
||||
LastConnectionSettings = settings;
|
||||
if (ConnectException != null) throw ConnectException;
|
||||
IsConnected = true;
|
||||
CurrentConnectionInfo = ConnectResult;
|
||||
|
||||
@@ -187,4 +187,142 @@ public class MainWindowViewModelTests
|
||||
changed.ShouldContain(nameof(MainWindowViewModel.ConnectionState));
|
||||
changed.ShouldContain(nameof(MainWindowViewModel.IsConnected));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultState_HasCorrectAdvancedSettings()
|
||||
{
|
||||
_vm.FailoverUrls.ShouldBeNull();
|
||||
_vm.SessionTimeoutSeconds.ShouldBe(60);
|
||||
_vm.AutoAcceptCertificates.ShouldBeTrue();
|
||||
_vm.CertificateStorePath.ShouldContain("LmxOpcUaClient");
|
||||
_vm.CertificateStorePath.ShouldContain("pki");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsFailoverUrlsToSettings()
|
||||
{
|
||||
_vm.FailoverUrls = "opc.tcp://backup1:4840, opc.tcp://backup2:4840";
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_service.LastConnectionSettings.ShouldNotBeNull();
|
||||
_service.LastConnectionSettings!.FailoverUrls.ShouldNotBeNull();
|
||||
_service.LastConnectionSettings.FailoverUrls!.Length.ShouldBe(2);
|
||||
_service.LastConnectionSettings.FailoverUrls[0].ShouldBe("opc.tcp://backup1:4840");
|
||||
_service.LastConnectionSettings.FailoverUrls[1].ShouldBe("opc.tcp://backup2:4840");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsEmptyFailoverUrlsToNull()
|
||||
{
|
||||
_vm.FailoverUrls = "";
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_service.LastConnectionSettings.ShouldNotBeNull();
|
||||
_service.LastConnectionSettings!.FailoverUrls.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsSessionTimeoutToSettings()
|
||||
{
|
||||
_vm.SessionTimeoutSeconds = 120;
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_service.LastConnectionSettings.ShouldNotBeNull();
|
||||
_service.LastConnectionSettings!.SessionTimeoutSeconds.ShouldBe(120);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsAutoAcceptCertificatesToSettings()
|
||||
{
|
||||
_vm.AutoAcceptCertificates = false;
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_service.LastConnectionSettings.ShouldNotBeNull();
|
||||
_service.LastConnectionSettings!.AutoAcceptCertificates.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsCertificateStorePathToSettings()
|
||||
{
|
||||
_vm.CertificateStorePath = "/custom/pki/path";
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_service.LastConnectionSettings.ShouldNotBeNull();
|
||||
_service.LastConnectionSettings!.CertificateStorePath.ShouldBe("/custom/pki/path");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubscribeSelectedNodesCommand_SubscribesAndSwitchesToTab()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
var node1 = _vm.BrowseTree.RootNodes[0];
|
||||
_vm.SelectedTreeNodes.Add(node1);
|
||||
|
||||
await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.Subscriptions.ActiveSubscriptions.Count.ShouldBe(1);
|
||||
_vm.Subscriptions.ActiveSubscriptions[0].NodeId.ShouldBe(node1.NodeId);
|
||||
_vm.SelectedTabIndex.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubscribeSelectedNodesCommand_DoesNothing_WhenNoSelection()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.Subscriptions.ActiveSubscriptions.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ViewHistoryForSelectedNodeCommand_SetsNodeAndSwitchesToTab()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
var node = _vm.BrowseTree.RootNodes[0];
|
||||
_vm.SelectedTreeNodes.Add(node);
|
||||
|
||||
_vm.ViewHistoryForSelectedNodeCommand.Execute(null);
|
||||
|
||||
_vm.History.SelectedNodeId.ShouldBe(node.NodeId);
|
||||
_vm.SelectedTabIndex.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateHistoryEnabledForSelection_TrueForVariableNode()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
var variableNode = new TreeNodeViewModel(
|
||||
"ns=2;s=Var1", "Var1", "Variable", false, _service, new SynchronousUiDispatcher());
|
||||
_vm.SelectedTreeNodes.Add(variableNode);
|
||||
|
||||
_vm.UpdateHistoryEnabledForSelection();
|
||||
|
||||
_vm.IsHistoryEnabledForSelection.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateHistoryEnabledForSelection_FalseForObjectNode()
|
||||
{
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
var objectNode = new TreeNodeViewModel(
|
||||
"ns=2;s=Obj1", "Obj1", "Object", true, _service, new SynchronousUiDispatcher());
|
||||
_vm.SelectedTreeNodes.Add(objectNode);
|
||||
|
||||
_vm.UpdateHistoryEnabledForSelection();
|
||||
|
||||
_vm.IsHistoryEnabledForSelection.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateHistoryEnabledForSelection_FalseWhenDisconnected()
|
||||
{
|
||||
_vm.UpdateHistoryEnabledForSelection();
|
||||
|
||||
_vm.IsHistoryEnabledForSelection.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,4 +132,40 @@ public class SubscriptionsViewModelTests
|
||||
{
|
||||
_vm.NewInterval.ShouldBe(1000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddSubscriptionForNodeAsync_AddsSubscription()
|
||||
{
|
||||
_vm.IsConnected = true;
|
||||
|
||||
await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode");
|
||||
|
||||
_vm.ActiveSubscriptions.Count.ShouldBe(1);
|
||||
_vm.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=TestNode");
|
||||
_vm.SubscriptionCount.ShouldBe(1);
|
||||
_service.SubscribeCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddSubscriptionForNodeAsync_SkipsDuplicate()
|
||||
{
|
||||
_vm.IsConnected = true;
|
||||
|
||||
await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode");
|
||||
await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode");
|
||||
|
||||
_vm.ActiveSubscriptions.Count.ShouldBe(1);
|
||||
_service.SubscribeCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddSubscriptionForNodeAsync_DoesNothing_WhenDisconnected()
|
||||
{
|
||||
_vm.IsConnected = false;
|
||||
|
||||
await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode");
|
||||
|
||||
_vm.ActiveSubscriptions.ShouldBeEmpty();
|
||||
_service.SubscribeCallCount.ShouldBe(0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user