fix: 修复编译错误、BGA向导预览、算子重复插入及状态栏消息
## Compilation fixes - Fix XAML namespace reference XplorePlane.Converters → XplorePlane.Helpers (EventLogView, StateDisplayView) - Add missing using for ISampleTypeRepository / JsonSampleTypeRepository in App.xaml.cs - Fix ILoggerService.Warn() calls using Exception as first arg instead of message string - Serialize CncProgram to JSON when passing to MatrixLayout (expects string, not CncProgram object) - Suppress CS8632 nullable annotation warnings project-wide ## BGA wizard fixes - Fix preview not showing on step 3: force RefreshBgaPreview() when entering final step - Add step validation in CanNext() to prevent skipping to preview with invalid inputs - Notify NextCommand.CanExecuteChanged when inputs change - Fix RunProgress runtime binding error: set Mode=OneWay for ProgressBar.Value ## Operator toolbox: prevent duplicate insertion - Add static OperatorTarget property for direct dispatch to active pipeline - PipelineEditorView registers as toolbox target on load (floating window only) - CNC page sets initial target; PipelineEditorWindow resets to CNC pipeline on close - '+' button inserts only to the active pipeline instead of broadcasting to all subscribers ## Pipeline editor status bar messages - Add StatusBarMessageEvent / StatusBarMessagePayload to CommonEvents.cs - PipelineEditorViewModel publishes status on all ops (add/remove/reorder/save/execute) - MainViewModel subscribes and displays on main window bottom status bar - Auto-clear after timeout (3s info, 5s error) ## docs - Merge three architecture docs into consolidated XplorePlane-架构与结构说明.md
This commit is contained in:
@@ -1,81 +1,81 @@
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
using XP.Hardware.MotionControl.Implementations;
|
||||
|
||||
namespace XplorePlane.Tests.Hardware
|
||||
{
|
||||
public class SimulatedLinearAxisTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MoveToTarget_SetsStatusToMoving_ThenIdle()
|
||||
{
|
||||
// Arrange - use fast speed for quicker test
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0, defaultSpeed: 200);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(50);
|
||||
|
||||
// Assert - should be Moving immediately after call
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(AxisStatus.Moving, axis.Status);
|
||||
|
||||
// Wait for move to complete (50mm at 200mm/s = 250ms, add generous buffer)
|
||||
await Task.Delay(1000);
|
||||
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.Equal(50.0, axis.ActualPosition, precision: 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveToTarget_OutOfRange_ReturnsFail()
|
||||
{
|
||||
// Arrange
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(150);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.NotNull(result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stop_DuringMove_KeepsCurrentPosition()
|
||||
{
|
||||
// Arrange - use slow speed so we can stop mid-move
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0, defaultSpeed: 10);
|
||||
|
||||
// Act - start a long move (100mm at 10mm/s = 10s)
|
||||
axis.MoveToTarget(100);
|
||||
await Task.Delay(200); // Let it move a bit
|
||||
|
||||
axis.Stop();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.True(axis.ActualPosition > 0, "Position should have moved from 0");
|
||||
Assert.True(axis.ActualPosition < 100, "Position should not have reached target");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Home_MovesToZero()
|
||||
{
|
||||
// Arrange - move to 50 first with fast speed
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0, defaultSpeed: 200);
|
||||
axis.MoveToTarget(50);
|
||||
await Task.Delay(1000); // Wait for move to complete
|
||||
|
||||
Assert.Equal(50.0, axis.ActualPosition, precision: 1);
|
||||
|
||||
// Act
|
||||
axis.Home();
|
||||
await Task.Delay(1000); // Wait for home to complete
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0.0, axis.ActualPosition, precision: 1);
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
using XP.Hardware.MotionControl.Implementations;
|
||||
|
||||
namespace XplorePlane.Tests.Hardware
|
||||
{
|
||||
public class SimulatedLinearAxisTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MoveToTarget_SetsStatusToMoving_ThenIdle()
|
||||
{
|
||||
// Arrange - use fast speed for quicker test
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0, defaultSpeed: 200);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(50);
|
||||
|
||||
// Assert - should be Moving immediately after call
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(AxisStatus.Moving, axis.Status);
|
||||
|
||||
// Wait for move to complete (50mm at 200mm/s = 250ms, add generous buffer)
|
||||
await Task.Delay(1000);
|
||||
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.Equal(50.0, axis.ActualPosition, precision: 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveToTarget_OutOfRange_ReturnsFail()
|
||||
{
|
||||
// Arrange
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(150);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.NotNull(result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stop_DuringMove_KeepsCurrentPosition()
|
||||
{
|
||||
// Arrange - use slow speed so we can stop mid-move
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0, defaultSpeed: 10);
|
||||
|
||||
// Act - start a long move (100mm at 10mm/s = 10s)
|
||||
axis.MoveToTarget(100);
|
||||
await Task.Delay(200); // Let it move a bit
|
||||
|
||||
axis.Stop();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.True(axis.ActualPosition > 0, "Position should have moved from 0");
|
||||
Assert.True(axis.ActualPosition < 100, "Position should not have reached target");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Home_MovesToZero()
|
||||
{
|
||||
// Arrange - move to 50 first with fast speed
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0, defaultSpeed: 200);
|
||||
axis.MoveToTarget(50);
|
||||
await Task.Delay(1000); // Wait for move to complete
|
||||
|
||||
Assert.Equal(50.0, axis.ActualPosition, precision: 1);
|
||||
|
||||
// Act
|
||||
axis.Home();
|
||||
await Task.Delay(1000); // Wait for home to complete
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0.0, axis.ActualPosition, precision: 1);
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
using XP.Hardware.MotionControl.Implementations;
|
||||
|
||||
namespace XplorePlane.Tests.Hardware
|
||||
{
|
||||
public class SimulatedRotaryAxisTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MoveToTarget_SetsStatusToMoving_ThenIdle()
|
||||
{
|
||||
// Arrange - use fast speed for quicker test
|
||||
var axis = new SimulatedRotaryAxis(
|
||||
RotaryAxisId.DetectorSwing, minAngle: -180, maxAngle: 180, enabled: true, defaultSpeed: 200);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(90);
|
||||
|
||||
// Assert - should be Moving immediately after call
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(AxisStatus.Moving, axis.Status);
|
||||
|
||||
// Wait for move to complete (90° at 200°/s = 450ms, add generous buffer)
|
||||
await Task.Delay(1000);
|
||||
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.Equal(90.0, axis.ActualAngle, precision: 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveToTarget_OutOfRange_ReturnsFail()
|
||||
{
|
||||
// Arrange
|
||||
var axis = new SimulatedRotaryAxis(
|
||||
RotaryAxisId.DetectorSwing, minAngle: -180, maxAngle: 180, enabled: true);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(200);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.NotNull(result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stop_DuringMove_KeepsCurrentAngle()
|
||||
{
|
||||
// Arrange - use slow speed so we can stop mid-move
|
||||
var axis = new SimulatedRotaryAxis(
|
||||
RotaryAxisId.DetectorSwing, minAngle: -180, maxAngle: 180, enabled: true, defaultSpeed: 10);
|
||||
|
||||
// Act - start a long move (180° at 10°/s = 18s)
|
||||
axis.MoveToTarget(180);
|
||||
await Task.Delay(200); // Let it move a bit
|
||||
|
||||
axis.Stop();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.True(axis.ActualAngle > 0, "Angle should have moved from 0");
|
||||
Assert.True(axis.ActualAngle < 180, "Angle should not have reached target");
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
using XP.Hardware.MotionControl.Implementations;
|
||||
|
||||
namespace XplorePlane.Tests.Hardware
|
||||
{
|
||||
public class SimulatedRotaryAxisTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MoveToTarget_SetsStatusToMoving_ThenIdle()
|
||||
{
|
||||
// Arrange - use fast speed for quicker test
|
||||
var axis = new SimulatedRotaryAxis(
|
||||
RotaryAxisId.DetectorSwing, minAngle: -180, maxAngle: 180, enabled: true, defaultSpeed: 200);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(90);
|
||||
|
||||
// Assert - should be Moving immediately after call
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(AxisStatus.Moving, axis.Status);
|
||||
|
||||
// Wait for move to complete (90° at 200°/s = 450ms, add generous buffer)
|
||||
await Task.Delay(1000);
|
||||
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.Equal(90.0, axis.ActualAngle, precision: 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveToTarget_OutOfRange_ReturnsFail()
|
||||
{
|
||||
// Arrange
|
||||
var axis = new SimulatedRotaryAxis(
|
||||
RotaryAxisId.DetectorSwing, minAngle: -180, maxAngle: 180, enabled: true);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(200);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.NotNull(result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stop_DuringMove_KeepsCurrentAngle()
|
||||
{
|
||||
// Arrange - use slow speed so we can stop mid-move
|
||||
var axis = new SimulatedRotaryAxis(
|
||||
RotaryAxisId.DetectorSwing, minAngle: -180, maxAngle: 180, enabled: true, defaultSpeed: 10);
|
||||
|
||||
// Act - start a long move (180° at 10°/s = 18s)
|
||||
axis.MoveToTarget(180);
|
||||
await Task.Delay(200); // Let it move a bit
|
||||
|
||||
axis.Stop();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.True(axis.ActualAngle > 0, "Angle should have moved from 0");
|
||||
Assert.True(axis.ActualAngle < 180, "Angle should not have reached target");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +1,145 @@
|
||||
using Moq;
|
||||
using Prism.Events;
|
||||
using Xunit;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.RaySource.Abstractions.Enums;
|
||||
using XP.Hardware.RaySource.Abstractions.Events;
|
||||
using XP.Hardware.RaySource.Implementations;
|
||||
|
||||
namespace XplorePlane.Tests.Hardware
|
||||
{
|
||||
public class SimulatedXRaySourceTests
|
||||
{
|
||||
private readonly Mock<IEventAggregator> _mockEventAggregator;
|
||||
private readonly Mock<ILoggerService> _mockLogger;
|
||||
private readonly Mock<RaySourceStatusChangedEvent> _mockStatusEvent;
|
||||
private readonly Mock<VariablesConnectedEvent> _mockVariablesConnectedEvent;
|
||||
private readonly Mock<StatusUpdatedEvent> _mockStatusUpdatedEvent;
|
||||
private readonly SimulatedXRaySource _source;
|
||||
|
||||
public SimulatedXRaySourceTests()
|
||||
{
|
||||
_mockEventAggregator = new Mock<IEventAggregator>();
|
||||
_mockLogger = new Mock<ILoggerService>();
|
||||
_mockStatusEvent = new Mock<RaySourceStatusChangedEvent>();
|
||||
_mockVariablesConnectedEvent = new Mock<VariablesConnectedEvent>();
|
||||
_mockStatusUpdatedEvent = new Mock<StatusUpdatedEvent>();
|
||||
|
||||
// Setup logger to return itself for ForModule<T>()
|
||||
_mockLogger.Setup(l => l.ForModule<SimulatedXRaySource>()).Returns(_mockLogger.Object);
|
||||
|
||||
// Setup event aggregator to return the mock event
|
||||
_mockEventAggregator
|
||||
.Setup(ea => ea.GetEvent<RaySourceStatusChangedEvent>())
|
||||
.Returns(_mockStatusEvent.Object);
|
||||
|
||||
// ConnectVariables() 会发布 VariablesConnectedEvent,需要 Mock 该事件避免 NullReferenceException
|
||||
// ConnectVariables() publishes VariablesConnectedEvent, must mock it to avoid NullReferenceException
|
||||
_mockEventAggregator
|
||||
.Setup(ea => ea.GetEvent<VariablesConnectedEvent>())
|
||||
.Returns(_mockVariablesConnectedEvent.Object);
|
||||
|
||||
// TurnOn/TurnOff/SetVoltage/SetCurrent 会发布 StatusUpdatedEvent,同样需要 Mock
|
||||
// TurnOn/TurnOff/SetVoltage/SetCurrent publish StatusUpdatedEvent, also needs mocking
|
||||
_mockEventAggregator
|
||||
.Setup(ea => ea.GetEvent<StatusUpdatedEvent>())
|
||||
.Returns(_mockStatusUpdatedEvent.Object);
|
||||
|
||||
_source = new SimulatedXRaySource(_mockEventAggregator.Object, _mockLogger.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_SetsInitializedTrue()
|
||||
{
|
||||
// Act
|
||||
var initResult = _source.Initialize();
|
||||
var connectResult = _source.ConnectVariables();
|
||||
|
||||
// Assert
|
||||
Assert.True(initResult.Success);
|
||||
Assert.True(connectResult.Success);
|
||||
Assert.True(_source.IsConnected);
|
||||
|
||||
// 验证 ConnectVariables 发布了 VariablesConnectedEvent(true)
|
||||
// Verify ConnectVariables published VariablesConnectedEvent(true)
|
||||
_mockVariablesConnectedEvent.Verify(e => e.Publish(true), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TurnOn_PublishesOpenedEvent()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
|
||||
// Act
|
||||
var result = _source.TurnOn();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
_mockStatusEvent.Verify(
|
||||
e => e.Publish(RaySourceStatus.Opened),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TurnOff_PublishesClosedEvent()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
_source.TurnOn();
|
||||
|
||||
// ConnectVariables/TurnOn 阶段已经发布过 Closed/Opened 事件,清空调用记录避免影响 Act 阶段的断言
|
||||
// ConnectVariables/TurnOn already published Closed/Opened events during Arrange; clear invocations
|
||||
// so the Act-phase assertion only counts the publish triggered by TurnOff()
|
||||
_mockStatusEvent.Invocations.Clear();
|
||||
|
||||
// Act
|
||||
var result = _source.TurnOff();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
_mockStatusEvent.Verify(
|
||||
e => e.Publish(RaySourceStatus.Closed),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadVoltage_ReturnsWithinTwoPercentOfSetValue()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
_source.SetVoltage(100f);
|
||||
|
||||
// Act & Assert - read multiple times to verify range
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var result = _source.ReadVoltage();
|
||||
Assert.True(result.Success);
|
||||
|
||||
float voltage = result.GetFloat();
|
||||
Assert.InRange(voltage, 98f, 102f);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloseOff_ResetsState()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
_source.TurnOn();
|
||||
|
||||
Assert.True(_source.IsConnected);
|
||||
|
||||
// Act
|
||||
var result = _source.CloseOff();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.False(_source.IsConnected);
|
||||
}
|
||||
}
|
||||
}
|
||||
using Moq;
|
||||
using Prism.Events;
|
||||
using Xunit;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.RaySource.Abstractions.Enums;
|
||||
using XP.Hardware.RaySource.Abstractions.Events;
|
||||
using XP.Hardware.RaySource.Implementations;
|
||||
|
||||
namespace XplorePlane.Tests.Hardware
|
||||
{
|
||||
public class SimulatedXRaySourceTests
|
||||
{
|
||||
private readonly Mock<IEventAggregator> _mockEventAggregator;
|
||||
private readonly Mock<ILoggerService> _mockLogger;
|
||||
private readonly Mock<RaySourceStatusChangedEvent> _mockStatusEvent;
|
||||
private readonly Mock<VariablesConnectedEvent> _mockVariablesConnectedEvent;
|
||||
private readonly Mock<StatusUpdatedEvent> _mockStatusUpdatedEvent;
|
||||
private readonly SimulatedXRaySource _source;
|
||||
|
||||
public SimulatedXRaySourceTests()
|
||||
{
|
||||
_mockEventAggregator = new Mock<IEventAggregator>();
|
||||
_mockLogger = new Mock<ILoggerService>();
|
||||
_mockStatusEvent = new Mock<RaySourceStatusChangedEvent>();
|
||||
_mockVariablesConnectedEvent = new Mock<VariablesConnectedEvent>();
|
||||
_mockStatusUpdatedEvent = new Mock<StatusUpdatedEvent>();
|
||||
|
||||
// Setup logger to return itself for ForModule<T>()
|
||||
_mockLogger.Setup(l => l.ForModule<SimulatedXRaySource>()).Returns(_mockLogger.Object);
|
||||
|
||||
// Setup event aggregator to return the mock event
|
||||
_mockEventAggregator
|
||||
.Setup(ea => ea.GetEvent<RaySourceStatusChangedEvent>())
|
||||
.Returns(_mockStatusEvent.Object);
|
||||
|
||||
// ConnectVariables() 会发布 VariablesConnectedEvent,需要 Mock 该事件避免 NullReferenceException
|
||||
// ConnectVariables() publishes VariablesConnectedEvent, must mock it to avoid NullReferenceException
|
||||
_mockEventAggregator
|
||||
.Setup(ea => ea.GetEvent<VariablesConnectedEvent>())
|
||||
.Returns(_mockVariablesConnectedEvent.Object);
|
||||
|
||||
// TurnOn/TurnOff/SetVoltage/SetCurrent 会发布 StatusUpdatedEvent,同样需要 Mock
|
||||
// TurnOn/TurnOff/SetVoltage/SetCurrent publish StatusUpdatedEvent, also needs mocking
|
||||
_mockEventAggregator
|
||||
.Setup(ea => ea.GetEvent<StatusUpdatedEvent>())
|
||||
.Returns(_mockStatusUpdatedEvent.Object);
|
||||
|
||||
_source = new SimulatedXRaySource(_mockEventAggregator.Object, _mockLogger.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_SetsInitializedTrue()
|
||||
{
|
||||
// Act
|
||||
var initResult = _source.Initialize();
|
||||
var connectResult = _source.ConnectVariables();
|
||||
|
||||
// Assert
|
||||
Assert.True(initResult.Success);
|
||||
Assert.True(connectResult.Success);
|
||||
Assert.True(_source.IsConnected);
|
||||
|
||||
// 验证 ConnectVariables 发布了 VariablesConnectedEvent(true)
|
||||
// Verify ConnectVariables published VariablesConnectedEvent(true)
|
||||
_mockVariablesConnectedEvent.Verify(e => e.Publish(true), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TurnOn_PublishesOpenedEvent()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
|
||||
// Act
|
||||
var result = _source.TurnOn();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
_mockStatusEvent.Verify(
|
||||
e => e.Publish(RaySourceStatus.Opened),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TurnOff_PublishesClosedEvent()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
_source.TurnOn();
|
||||
|
||||
// ConnectVariables/TurnOn 阶段已经发布过 Closed/Opened 事件,清空调用记录避免影响 Act 阶段的断言
|
||||
// ConnectVariables/TurnOn already published Closed/Opened events during Arrange; clear invocations
|
||||
// so the Act-phase assertion only counts the publish triggered by TurnOff()
|
||||
_mockStatusEvent.Invocations.Clear();
|
||||
|
||||
// Act
|
||||
var result = _source.TurnOff();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
_mockStatusEvent.Verify(
|
||||
e => e.Publish(RaySourceStatus.Closed),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadVoltage_ReturnsWithinTwoPercentOfSetValue()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
_source.SetVoltage(100f);
|
||||
|
||||
// Act & Assert - read multiple times to verify range
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var result = _source.ReadVoltage();
|
||||
Assert.True(result.Success);
|
||||
|
||||
float voltage = result.GetFloat();
|
||||
Assert.InRange(voltage, 98f, 102f);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloseOff_ResetsState()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
_source.TurnOn();
|
||||
|
||||
Assert.True(_source.IsConnected);
|
||||
|
||||
// Act
|
||||
var result = _source.CloseOff();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.False(_source.IsConnected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user