Renames all 11 projects (5 src + 6 tests), the .slnx solution file, all source-file namespaces, all axaml namespace references, and all v1 documentation references in CLAUDE.md and docs/*.md (excluding docs/v2/ which is already in OtOpcUa form). Also updates the TopShelf service registration name from "LmxOpcUa" to "OtOpcUa" per Phase 0 Task 0.6.
Preserves runtime identifiers per Phase 0 Out-of-Scope rules to avoid breaking v1/v2 client trust during coexistence: OPC UA `ApplicationUri` defaults (`urn:{GalaxyName}:LmxOpcUa`), server `EndpointPath` (`/LmxOpcUa`), `ServerName` default (feeds cert subject CN), `MxAccessConfiguration.ClientName` default (defensive — stays "LmxOpcUa" for MxAccess audit-trail consistency), client OPC UA identifiers (`ApplicationName = "LmxOpcUaClient"`, `ApplicationUri = "urn:localhost:LmxOpcUaClient"`, cert directory `%LocalAppData%\LmxOpcUaClient\pki\`), and the `LmxOpcUaServer` class name (class rename out of Phase 0 scope per Task 0.5 sed pattern; happens in Phase 1 alongside `LmxNodeManager → GenericDriverNodeManager` Core extraction). 23 LmxOpcUa references retained, all enumerated and justified in `docs/v2/implementation/exit-gate-phase-0.md`.
Build clean: 0 errors, 30 warnings (lower than baseline 167). Tests at strict improvement over baseline: 821 passing / 1 failing vs baseline 820 / 2 (one flaky pre-existing failure passed this run; the other still fails — both pre-existing and unrelated to the rename). `Client.UI.Tests`, `Historian.Aveva.Tests`, `Client.Shared.Tests`, `IntegrationTests` all match baseline exactly. Exit gate compliance results recorded in `docs/v2/implementation/exit-gate-phase-0.md` with all 7 checks PASS or DEFERRED-to-PR-review (#7 service install verification needs Windows service permissions on the reviewer's box).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
543 lines
19 KiB
C#
543 lines
19 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
|
using ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
|
using ZB.MOM.WW.OtOpcUa.Client.UI.Tests.Fakes;
|
|
using ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;
|
|
using BrowseResult = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.BrowseResult;
|
|
using ConnectionState = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.ConnectionState;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Client.UI.Tests;
|
|
|
|
/// <summary>
|
|
/// Verifies the main UI shell behavior for connection state, settings persistence, browsing, subscriptions, and history navigation.
|
|
/// </summary>
|
|
public class MainWindowViewModelTests
|
|
{
|
|
private readonly FakeOpcUaClientService _service;
|
|
private readonly FakeSettingsService _settingsService;
|
|
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 BrowseResult("ns=2;s=Root", "Root", "Object", true)
|
|
],
|
|
RedundancyResult = new RedundancyInfo("None", 200, ["urn:test"], "urn:test")
|
|
};
|
|
|
|
_settingsService = new FakeSettingsService();
|
|
var factory = new FakeOpcUaClientServiceFactory(_service);
|
|
var dispatcher = new SynchronousUiDispatcher();
|
|
_vm = new MainWindowViewModel(factory, dispatcher, _settingsService);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the shell starts disconnected with the default endpoint and status text.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DefaultState_IsDisconnected()
|
|
{
|
|
_vm.ConnectionState.ShouldBe(ConnectionState.Disconnected);
|
|
_vm.IsConnected.ShouldBeFalse();
|
|
_vm.EndpointUrl.ShouldBe("opc.tcp://localhost:4840");
|
|
_vm.StatusMessage.ShouldBe("Disconnected");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the connect command is available before a session is established.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConnectCommand_CanExecute_WhenDisconnected()
|
|
{
|
|
_vm.ConnectCommand.CanExecute(null).ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that disconnect is disabled until a server session is active.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DisconnectCommand_CannotExecute_WhenDisconnected()
|
|
{
|
|
_vm.DisconnectCommand.CanExecute(null).ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that a successful connect command updates the shell into the connected state.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectCommand_TransitionsToConnected()
|
|
{
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
_vm.ConnectionState.ShouldBe(ConnectionState.Connected);
|
|
_vm.IsConnected.ShouldBeTrue();
|
|
_service.ConnectCallCount.ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the initial browse tree is loaded after a successful connect.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectCommand_LoadsRootNodes()
|
|
{
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
_vm.BrowseTree.RootNodes.Count.ShouldBe(1);
|
|
_vm.BrowseTree.RootNodes[0].DisplayName.ShouldBe("Root");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that redundancy details are fetched and exposed after connecting.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the session label shows the connected server and session identity.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectCommand_SetsSessionLabel()
|
|
{
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
_vm.SessionLabel.ShouldContain("TestServer");
|
|
_vm.SessionLabel.ShouldContain("TestSession");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that disconnect returns the shell to the disconnected state.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that disconnect clears session-specific UI state such as browse data and redundancy details.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that connection-state events from the client update the shell status text and state.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectionStateChangedEvent_UpdatesState()
|
|
{
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
_service.RaiseConnectionStateChanged(
|
|
new ConnectionStateChangedEventArgs(ConnectionState.Connected, ConnectionState.Reconnecting,
|
|
"opc.tcp://localhost:4840"));
|
|
|
|
_vm.ConnectionState.ShouldBe(ConnectionState.Reconnecting);
|
|
_vm.StatusMessage.ShouldBe("Reconnecting...");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that selecting a tree node updates the dependent read/write and history panels.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that a successful connect propagates connected state into the child tabs.
|
|
/// </summary>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that disconnect propagates disconnected state into the child tabs.
|
|
/// </summary>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that failed connection attempts restore the disconnected shell state and surface the error text.
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that connection-state transitions raise property-changed notifications for UI binding updates.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task PropertyChanged_FiredForConnectionState()
|
|
{
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
var changed = new List<string>();
|
|
_vm.PropertyChanged += (_, e) => changed.Add(e.PropertyName!);
|
|
|
|
_service.RaiseConnectionStateChanged(
|
|
new ConnectionStateChangedEventArgs(ConnectionState.Connected, ConnectionState.Reconnecting,
|
|
"opc.tcp://localhost:4840"));
|
|
|
|
changed.ShouldContain(nameof(MainWindowViewModel.ConnectionState));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the shell initializes advanced connection settings with the expected defaults.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DefaultState_HasCorrectAdvancedSettings()
|
|
{
|
|
_vm.FailoverUrls.ShouldBeNull();
|
|
_vm.SessionTimeoutSeconds.ShouldBe(60);
|
|
_vm.AutoAcceptCertificates.ShouldBeTrue();
|
|
_vm.CertificateStorePath.ShouldContain("LmxOpcUaClient");
|
|
_vm.CertificateStorePath.ShouldContain("pki");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that failover endpoint text is parsed into connection settings on connect.
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that empty failover text is normalized to no configured failover endpoints.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectCommand_MapsEmptyFailoverUrlsToNull()
|
|
{
|
|
_vm.FailoverUrls = "";
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
_service.LastConnectionSettings.ShouldNotBeNull();
|
|
_service.LastConnectionSettings!.FailoverUrls.ShouldBeNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the configured session timeout is passed into the connection settings.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectCommand_MapsSessionTimeoutToSettings()
|
|
{
|
|
_vm.SessionTimeoutSeconds = 120;
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
_service.LastConnectionSettings.ShouldNotBeNull();
|
|
_service.LastConnectionSettings!.SessionTimeoutSeconds.ShouldBe(120);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the auto-accept certificate toggle is passed into the connection settings.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectCommand_MapsAutoAcceptCertificatesToSettings()
|
|
{
|
|
_vm.AutoAcceptCertificates = false;
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
_service.LastConnectionSettings.ShouldNotBeNull();
|
|
_service.LastConnectionSettings!.AutoAcceptCertificates.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that a custom certificate store path is passed into the connection settings.
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that subscribing selected nodes adds subscriptions and switches the shell to the subscriptions tab.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task SubscribeSelectedNodesCommand_SubscribesAndSwitchesToTab()
|
|
{
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
// Use a Variable node so recursive subscribe subscribes it directly
|
|
var varNode = new TreeNodeViewModel(
|
|
"ns=2;s=TestVar", "TestVar", "Variable", false, _service, new SynchronousUiDispatcher());
|
|
_vm.SelectedTreeNodes.Add(varNode);
|
|
|
|
await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null);
|
|
|
|
_vm.Subscriptions!.ActiveSubscriptions.Count.ShouldBe(1);
|
|
_vm.Subscriptions.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=TestVar");
|
|
_vm.SelectedTabIndex.ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that subscribing selected nodes is a no-op when nothing is selected.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task SubscribeSelectedNodesCommand_DoesNothing_WhenNoSelection()
|
|
{
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null);
|
|
|
|
_vm.Subscriptions.ActiveSubscriptions.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the history command targets the selected node and switches the shell to the history tab.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that history actions are enabled when a variable node is selected.
|
|
/// </summary>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that history actions stay disabled when an object node rather than a variable is selected.
|
|
/// </summary>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that history actions stay disabled when no server connection is active.
|
|
/// </summary>
|
|
[Fact]
|
|
public void UpdateHistoryEnabledForSelection_FalseWhenDisconnected()
|
|
{
|
|
_vm.UpdateHistoryEnabledForSelection();
|
|
|
|
_vm.IsHistoryEnabledForSelection.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that saved user settings are loaded during shell construction.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Constructor_LoadsSettingsFromService()
|
|
{
|
|
_settingsService.LoadCallCount.ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that persisted connection and security settings are applied to the shell on startup.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Constructor_AppliesSavedSettings()
|
|
{
|
|
var saved = new UserSettings
|
|
{
|
|
EndpointUrl = "opc.tcp://saved:5555",
|
|
Username = "savedUser",
|
|
Password = "savedPass",
|
|
SecurityMode = SecurityMode.Sign,
|
|
FailoverUrls = "opc.tcp://backup:5555",
|
|
SessionTimeoutSeconds = 120,
|
|
AutoAcceptCertificates = false,
|
|
CertificateStorePath = "/custom/path"
|
|
};
|
|
|
|
var settingsService = new FakeSettingsService { Settings = saved };
|
|
var service = new FakeOpcUaClientService
|
|
{
|
|
ConnectResult = _service.ConnectResult,
|
|
BrowseResults = _service.BrowseResults,
|
|
RedundancyResult = _service.RedundancyResult
|
|
};
|
|
var factory = new FakeOpcUaClientServiceFactory(service);
|
|
var vm = new MainWindowViewModel(factory, new SynchronousUiDispatcher(), settingsService);
|
|
|
|
vm.EndpointUrl.ShouldBe("opc.tcp://saved:5555");
|
|
vm.Username.ShouldBe("savedUser");
|
|
vm.Password.ShouldBe("savedPass");
|
|
vm.SelectedSecurityMode.ShouldBe(SecurityMode.Sign);
|
|
vm.FailoverUrls.ShouldBe("opc.tcp://backup:5555");
|
|
vm.SessionTimeoutSeconds.ShouldBe(120);
|
|
vm.AutoAcceptCertificates.ShouldBeFalse();
|
|
vm.CertificateStorePath.ShouldBe("/custom/path");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that successful connections persist the current connection settings.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectCommand_SavesSettingsOnSuccess()
|
|
{
|
|
_vm.EndpointUrl = "opc.tcp://myserver:4840";
|
|
_vm.Username = "admin";
|
|
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
_settingsService.SaveCallCount.ShouldBe(1);
|
|
_settingsService.LastSaved.ShouldNotBeNull();
|
|
_settingsService.LastSaved!.EndpointUrl.ShouldBe("opc.tcp://myserver:4840");
|
|
_settingsService.LastSaved.Username.ShouldBe("admin");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that failed connection attempts do not overwrite saved settings.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectCommand_DoesNotSaveOnFailure()
|
|
{
|
|
_service.ConnectException = new Exception("Connection refused");
|
|
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
_settingsService.SaveCallCount.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that active subscriptions are persisted when the shell disconnects.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectCommand_SavesSubscribedNodes()
|
|
{
|
|
await _vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
// Add a subscription
|
|
_vm.Subscriptions.IsConnected = true;
|
|
await _vm.Subscriptions.AddSubscriptionForNodeAsync("ns=2;s=TestSub");
|
|
|
|
// Disconnect saves settings including subscriptions
|
|
await _vm.DisconnectCommand.ExecuteAsync(null);
|
|
|
|
_settingsService.LastSaved.ShouldNotBeNull();
|
|
_settingsService.LastSaved!.SubscribedNodes.ShouldContain("ns=2;s=TestSub");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that saved subscriptions are restored after reconnecting the shell.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectCommand_RestoresSavedSubscriptions()
|
|
{
|
|
_settingsService.Settings.SubscribedNodes = ["ns=2;s=Restored1", "ns=2;s=Restored2"];
|
|
|
|
var service = new FakeOpcUaClientService
|
|
{
|
|
ConnectResult = _service.ConnectResult,
|
|
BrowseResults = _service.BrowseResults,
|
|
RedundancyResult = _service.RedundancyResult
|
|
};
|
|
var factory = new FakeOpcUaClientServiceFactory(service);
|
|
var vm = new MainWindowViewModel(factory, new SynchronousUiDispatcher(), _settingsService);
|
|
|
|
await vm.ConnectCommand.ExecuteAsync(null);
|
|
|
|
vm.Subscriptions.ActiveSubscriptions.Count.ShouldBe(2);
|
|
vm.Subscriptions.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=Restored1");
|
|
vm.Subscriptions.ActiveSubscriptions[1].NodeId.ShouldBe("ns=2;s=Restored2");
|
|
vm.SubscriptionCount.ShouldBe(2);
|
|
}
|
|
}
|