Files
XplorePlane/XplorePlane.Tests/ViewModels/StateDisplayViewModelTests.cs
T
zhengxuan.zhang 0ccf9c529e 新增调试页面
2026-05-16 13:44:36 +08:00

321 lines
13 KiB
C#

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
{
/// <summary>
/// 状态显示 ViewModel 单元测试
/// StateDisplayViewModel unit tests
/// </summary>
public class StateDisplayViewModelTests
{
private readonly Mock<IAppStateService> _mockAppStateService;
private readonly Mock<ILoggerService> _mockLoggerService;
private readonly Mock<ILoggerService> _mockModuleLogger;
private readonly Dispatcher _dispatcher;
public StateDisplayViewModelTests()
{
_mockAppStateService = new Mock<IAppStateService>();
_mockLoggerService = new Mock<ILoggerService>();
_mockModuleLogger = new Mock<ILoggerService>();
// Setup logger to return module logger
_mockLoggerService
.Setup(l => l.ForModule<StateDisplayViewModel>())
.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<EventHandler<StateChangedEventArgs<MotionState>>>(), Times.Once);
_mockAppStateService.VerifyAdd(s => s.RaySourceStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<RaySourceState>>>(), Times.Once);
_mockAppStateService.VerifyAdd(s => s.DetectorStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<DetectorState>>>(), Times.Once);
_mockAppStateService.VerifyAdd(s => s.SystemStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<SystemState>>>(), Times.Once);
_mockAppStateService.VerifyAdd(s => s.CameraStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<CameraState>>>(), Times.Once);
_mockAppStateService.VerifyAdd(s => s.LinkedViewStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<LinkedViewState>>>(), Times.Once);
_mockAppStateService.VerifyAdd(s => s.RecipeExecutionStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<RecipeExecutionState>>>(), 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<EventHandler<StateChangedEventArgs<MotionState>>>(), Times.Once);
_mockAppStateService.VerifyRemove(s => s.RaySourceStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<RaySourceState>>>(), Times.Once);
_mockAppStateService.VerifyRemove(s => s.DetectorStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<DetectorState>>>(), Times.Once);
_mockAppStateService.VerifyRemove(s => s.SystemStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<SystemState>>>(), Times.Once);
_mockAppStateService.VerifyRemove(s => s.CameraStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<CameraState>>>(), Times.Once);
_mockAppStateService.VerifyRemove(s => s.LinkedViewStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<LinkedViewState>>>(), Times.Once);
_mockAppStateService.VerifyRemove(s => s.RecipeExecutionStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<RecipeExecutionState>>>(), 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<RaySourceState>(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<RaySourceState>(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<RaySourceState>(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<RaySourceState>(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<RaySourceState>(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);
}
/// <summary>
/// 处理 Dispatcher 队列中的所有待处理消息
/// Process all pending messages in the Dispatcher queue
/// </summary>
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);
}
}
}