using Moq; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Threading; using Xunit; using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Services.AppState; using XplorePlane.ViewModels.Debug; namespace XplorePlane.Tests.ViewModels { /// /// 状态显示 ViewModel 单元测试 /// StateDisplayViewModel unit tests /// public class StateDisplayViewModelTests { private readonly Mock _mockAppStateService; private readonly Mock _mockLoggerService; private readonly Mock _mockModuleLogger; private readonly Dispatcher _dispatcher; public StateDisplayViewModelTests() { _mockAppStateService = new Mock(); _mockLoggerService = new Mock(); _mockModuleLogger = new Mock(); // Setup logger to return module logger _mockLoggerService .Setup(l => l.ForModule()) .Returns(_mockModuleLogger.Object); // Create a dispatcher for the current thread _dispatcher = Dispatcher.CurrentDispatcher; } [Fact] public void Constructor_InitializesStateTree_WithEightRootNodes() { // Arrange & Act var viewModel = new StateDisplayViewModel( _mockAppStateService.Object, _mockLoggerService.Object, _dispatcher); // Assert Assert.Equal(8, viewModel.StateTree.Count); Assert.Contains(viewModel.StateTree, n => n.Name == "MotionState"); Assert.Contains(viewModel.StateTree, n => n.Name == "RaySourceState"); Assert.Contains(viewModel.StateTree, n => n.Name == "DetectorState"); Assert.Contains(viewModel.StateTree, n => n.Name == "SystemState"); Assert.Contains(viewModel.StateTree, n => n.Name == "CameraState"); Assert.Contains(viewModel.StateTree, n => n.Name == "LinkedViewState"); Assert.Contains(viewModel.StateTree, n => n.Name == "RecipeExecutionState"); Assert.Contains(viewModel.StateTree, n => n.Name == "CalibrationMatrix"); } [Fact] public void Constructor_InitializesMotionStateFields_WithCorrectCount() { // Arrange & Act var viewModel = new StateDisplayViewModel( _mockAppStateService.Object, _mockLoggerService.Object, _dispatcher); // Assert var motionStateNode = viewModel.StateTree.First(n => n.Name == "MotionState"); Assert.Equal(18, motionStateNode.Children.Count); } [Fact] public void Initialize_SubscribesToAllStateChangeEvents() { // Arrange var viewModel = new StateDisplayViewModel( _mockAppStateService.Object, _mockLoggerService.Object, _dispatcher); // Act viewModel.Initialize(); // Assert _mockAppStateService.VerifyAdd(s => s.MotionStateChanged += It.IsAny>>(), Times.Once); _mockAppStateService.VerifyAdd(s => s.RaySourceStateChanged += It.IsAny>>(), Times.Once); _mockAppStateService.VerifyAdd(s => s.DetectorStateChanged += It.IsAny>>(), Times.Once); _mockAppStateService.VerifyAdd(s => s.SystemStateChanged += It.IsAny>>(), Times.Once); _mockAppStateService.VerifyAdd(s => s.CameraStateChanged += It.IsAny>>(), Times.Once); _mockAppStateService.VerifyAdd(s => s.LinkedViewStateChanged += It.IsAny>>(), Times.Once); _mockAppStateService.VerifyAdd(s => s.RecipeExecutionStateChanged += It.IsAny>>(), Times.Once); } [Fact] public void Dispose_UnsubscribesFromAllStateChangeEvents() { // Arrange var viewModel = new StateDisplayViewModel( _mockAppStateService.Object, _mockLoggerService.Object, _dispatcher); viewModel.Initialize(); // Act viewModel.Dispose(); // Assert _mockAppStateService.VerifyRemove(s => s.MotionStateChanged -= It.IsAny>>(), Times.Once); _mockAppStateService.VerifyRemove(s => s.RaySourceStateChanged -= It.IsAny>>(), Times.Once); _mockAppStateService.VerifyRemove(s => s.DetectorStateChanged -= It.IsAny>>(), Times.Once); _mockAppStateService.VerifyRemove(s => s.SystemStateChanged -= It.IsAny>>(), Times.Once); _mockAppStateService.VerifyRemove(s => s.CameraStateChanged -= It.IsAny>>(), Times.Once); _mockAppStateService.VerifyRemove(s => s.LinkedViewStateChanged -= It.IsAny>>(), Times.Once); _mockAppStateService.VerifyRemove(s => s.RecipeExecutionStateChanged -= It.IsAny>>(), Times.Once); } [Fact] public void StateChange_UpdatesNodeValue_AndSetsHighlight() { // Arrange var viewModel = new StateDisplayViewModel( _mockAppStateService.Object, _mockLoggerService.Object, _dispatcher); viewModel.Initialize(); var oldState = new RaySourceState(false, 100.0, 5.0); var newState = new RaySourceState(true, 160.0, 8.0); // Act _mockAppStateService.Raise( s => s.RaySourceStateChanged += null, _mockAppStateService.Object, new StateChangedEventArgs(oldState, newState)); // Process dispatcher queue DoEvents(); // Assert var raySourceNode = viewModel.StateTree.First(n => n.Name == "RaySourceState"); var isOnNode = raySourceNode.Children.First(n => n.Name == "IsOn"); var voltageNode = raySourceNode.Children.First(n => n.Name == "Voltage"); var powerNode = raySourceNode.Children.First(n => n.Name == "Power"); Assert.Equal("True", isOnNode.Value); Assert.True(isOnNode.IsHighlighted); Assert.Equal("Green", isOnNode.HighlightColor); // Boolean true -> Green Assert.Equal("160.00", voltageNode.Value); Assert.True(voltageNode.IsHighlighted); Assert.Equal("Green", voltageNode.HighlightColor); // Increase -> Green Assert.Equal("8.00", powerNode.Value); Assert.True(powerNode.IsHighlighted); Assert.Equal("Green", powerNode.HighlightColor); // Increase -> Green } [Fact] public void StateChange_BooleanFalse_SetsRedHighlight() { // Arrange var viewModel = new StateDisplayViewModel( _mockAppStateService.Object, _mockLoggerService.Object, _dispatcher); viewModel.Initialize(); var oldState = new RaySourceState(true, 160.0, 8.0); var newState = new RaySourceState(false, 160.0, 8.0); // Act _mockAppStateService.Raise( s => s.RaySourceStateChanged += null, _mockAppStateService.Object, new StateChangedEventArgs(oldState, newState)); // Process dispatcher queue DoEvents(); // Assert var raySourceNode = viewModel.StateTree.First(n => n.Name == "RaySourceState"); var isOnNode = raySourceNode.Children.First(n => n.Name == "IsOn"); Assert.Equal("False", isOnNode.Value); Assert.True(isOnNode.IsHighlighted); Assert.Equal("Red", isOnNode.HighlightColor); // Boolean false -> Red } [Fact] public void StateChange_NumericDecrease_SetsRedHighlight() { // Arrange var viewModel = new StateDisplayViewModel( _mockAppStateService.Object, _mockLoggerService.Object, _dispatcher); viewModel.Initialize(); var oldState = new RaySourceState(true, 160.0, 8.0); var newState = new RaySourceState(true, 120.0, 5.0); // Act _mockAppStateService.Raise( s => s.RaySourceStateChanged += null, _mockAppStateService.Object, new StateChangedEventArgs(oldState, newState)); // Process dispatcher queue DoEvents(); // Assert var raySourceNode = viewModel.StateTree.First(n => n.Name == "RaySourceState"); var voltageNode = raySourceNode.Children.First(n => n.Name == "Voltage"); var powerNode = raySourceNode.Children.First(n => n.Name == "Power"); Assert.Equal("120.00", voltageNode.Value); Assert.True(voltageNode.IsHighlighted); Assert.Equal("Red", voltageNode.HighlightColor); // Decrease -> Red Assert.Equal("5.00", powerNode.Value); Assert.True(powerNode.IsHighlighted); Assert.Equal("Red", powerNode.HighlightColor); // Decrease -> Red } [Fact] public async Task StateChange_HighlightClearsAfterTwoSeconds() { // Arrange var viewModel = new StateDisplayViewModel( _mockAppStateService.Object, _mockLoggerService.Object, _dispatcher); viewModel.Initialize(); var oldState = new RaySourceState(false, 100.0, 5.0); var newState = new RaySourceState(true, 160.0, 8.0); // Act _mockAppStateService.Raise( s => s.RaySourceStateChanged += null, _mockAppStateService.Object, new StateChangedEventArgs(oldState, newState)); // Process dispatcher queue DoEvents(); var raySourceNode = viewModel.StateTree.First(n => n.Name == "RaySourceState"); var isOnNode = raySourceNode.Children.First(n => n.Name == "IsOn"); // Verify highlight is set Assert.True(isOnNode.IsHighlighted); // Wait for 2.5 seconds (2 seconds delay + buffer) await Task.Delay(2500); DoEvents(); // Assert - highlight should be cleared Assert.False(isOnNode.IsHighlighted); } [Fact] public void StateChange_NoChange_DoesNotSetHighlight() { // Arrange var viewModel = new StateDisplayViewModel( _mockAppStateService.Object, _mockLoggerService.Object, _dispatcher); viewModel.Initialize(); var oldState = new RaySourceState(true, 160.0, 8.0); var newState = new RaySourceState(true, 160.0, 8.0); // Act _mockAppStateService.Raise( s => s.RaySourceStateChanged += null, _mockAppStateService.Object, new StateChangedEventArgs(oldState, newState)); // Process dispatcher queue DoEvents(); // Assert var raySourceNode = viewModel.StateTree.First(n => n.Name == "RaySourceState"); var isOnNode = raySourceNode.Children.First(n => n.Name == "IsOn"); var voltageNode = raySourceNode.Children.First(n => n.Name == "Voltage"); var powerNode = raySourceNode.Children.First(n => n.Name == "Power"); // Values should be updated but not highlighted Assert.Equal("True", isOnNode.Value); Assert.False(isOnNode.IsHighlighted); Assert.Equal("160.00", voltageNode.Value); Assert.False(voltageNode.IsHighlighted); Assert.Equal("8.00", powerNode.Value); Assert.False(powerNode.IsHighlighted); } /// /// 处理 Dispatcher 队列中的所有待处理消息 /// Process all pending messages in the Dispatcher queue /// private void DoEvents() { var frame = new DispatcherFrame(); _dispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback( delegate (object f) { ((DispatcherFrame)f).Continue = false; return null; }), frame); Dispatcher.PushFrame(frame); } } }