新增调试页面
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
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>
|
||||
/// EventLogViewModel 单元测试
|
||||
/// Unit tests for EventLogViewModel
|
||||
/// </summary>
|
||||
public class EventLogViewModelTests
|
||||
{
|
||||
private readonly Mock<IAppStateService> _mockAppStateService;
|
||||
private readonly Mock<ILoggerService> _mockLoggerService;
|
||||
private readonly Dispatcher _dispatcher;
|
||||
|
||||
public EventLogViewModelTests()
|
||||
{
|
||||
_mockAppStateService = new Mock<IAppStateService>();
|
||||
_mockLoggerService = new Mock<ILoggerService>();
|
||||
|
||||
// 创建 Dispatcher(需要在 STA 线程上)
|
||||
// Create Dispatcher (needs to be on STA thread)
|
||||
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||
|
||||
// 设置 logger mock 返回自身(链式调用)
|
||||
// Setup logger mock to return itself (method chaining)
|
||||
_mockLoggerService.Setup(l => l.ForModule<EventLogViewModel>())
|
||||
.Returns(_mockLoggerService.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InitializesFilterOptionsWithAllStatesEnabled()
|
||||
{
|
||||
// Arrange & Act
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(8, viewModel.FilterOptions.Count);
|
||||
Assert.True(viewModel.FilterOptions["MotionState"]);
|
||||
Assert.True(viewModel.FilterOptions["RaySourceState"]);
|
||||
Assert.True(viewModel.FilterOptions["DetectorState"]);
|
||||
Assert.True(viewModel.FilterOptions["SystemState"]);
|
||||
Assert.True(viewModel.FilterOptions["CameraState"]);
|
||||
Assert.True(viewModel.FilterOptions["LinkedViewState"]);
|
||||
Assert.True(viewModel.FilterOptions["RecipeExecutionState"]);
|
||||
Assert.True(viewModel.FilterOptions["CalibrationMatrix"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToggleFilterCommand_TogglesFilterState()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
var initialState = viewModel.FilterOptions["MotionState"];
|
||||
|
||||
// Act
|
||||
viewModel.ToggleFilterCommand.Execute("MotionState");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(!initialState, viewModel.FilterOptions["MotionState"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectAllFiltersCommand_EnablesAllFilters()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// 先禁用所有过滤器
|
||||
// First disable all filters
|
||||
viewModel.ClearAllFiltersCommand.Execute();
|
||||
|
||||
// Act
|
||||
viewModel.SelectAllFiltersCommand.Execute();
|
||||
|
||||
// Assert
|
||||
Assert.All(viewModel.FilterOptions.Values, value => Assert.True(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearAllFiltersCommand_DisablesAllFilters()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// Act
|
||||
viewModel.ClearAllFiltersCommand.Execute();
|
||||
|
||||
// Assert
|
||||
Assert.All(viewModel.FilterOptions.Values, value => Assert.False(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyFilter_FiltersEventLogCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// 添加测试数据
|
||||
// Add test data
|
||||
viewModel.EventLog.Add(new EventLogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
EventType = "MotionStateChanged",
|
||||
FieldName = "StageX",
|
||||
OldValue = "0",
|
||||
NewValue = "100",
|
||||
Category = "MotionState"
|
||||
});
|
||||
|
||||
viewModel.EventLog.Add(new EventLogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
EventType = "RaySourceStateChanged",
|
||||
FieldName = "IsOn",
|
||||
OldValue = "False",
|
||||
NewValue = "True",
|
||||
Category = "RaySourceState"
|
||||
});
|
||||
|
||||
// 禁用 RaySourceState 过滤器
|
||||
// Disable RaySourceState filter
|
||||
viewModel.FilterOptions["RaySourceState"] = false;
|
||||
|
||||
// Act
|
||||
viewModel.ToggleFilterCommand.Execute("MotionState"); // 触发过滤器应用
|
||||
viewModel.ToggleFilterCommand.Execute("MotionState"); // 恢复状态
|
||||
|
||||
// Assert
|
||||
Assert.Single(viewModel.FilteredEventLog);
|
||||
Assert.Equal("MotionState", viewModel.FilteredEventLog[0].Category);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventLog_LimitsTo1000Records()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// Act - 添加 1001 条记录
|
||||
// Act - Add 1001 records
|
||||
for (int i = 0; i < 1001; i++)
|
||||
{
|
||||
viewModel.EventLog.Add(new EventLogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
EventType = "MotionStateChanged",
|
||||
FieldName = "StageX",
|
||||
OldValue = i.ToString(),
|
||||
NewValue = (i + 1).ToString(),
|
||||
Category = "MotionState"
|
||||
});
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1000, viewModel.EventLog.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_ClearsCollections()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
viewModel.EventLog.Add(new EventLogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
EventType = "MotionStateChanged",
|
||||
FieldName = "StageX",
|
||||
OldValue = "0",
|
||||
NewValue = "100",
|
||||
Category = "MotionState"
|
||||
});
|
||||
|
||||
// Act
|
||||
viewModel.Dispose();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(viewModel.EventLog);
|
||||
Assert.Empty(viewModel.FilteredEventLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user