using Moq; using Prism.Events; using System; using System.IO; using System.Windows.Media.Imaging; using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Services; using XplorePlane.Tests.Helpers; using XplorePlane.ViewModels; using Xunit; namespace XplorePlane.Tests.Pipeline { /// /// 测试 PipelineEditorViewModel 的命令逻辑 /// 覆盖任务 6.1 / 6.2 / 6.3 / 6.4 / 6.8 /// public class PipelineEditorViewModelTests { private readonly Mock _mockImageSvc; private readonly Mock _mockExecSvc; private readonly Mock _mockPersistSvc; private readonly Mock _mockLogger; public PipelineEditorViewModelTests() { _mockImageSvc = TestHelpers.CreateMockImageService(); _mockExecSvc = new Mock(); _mockPersistSvc = new Mock(); _mockLogger = new Mock(); _mockLogger.Setup(l => l.ForModule()).Returns(_mockLogger.Object); } private PipelineEditorViewModel CreateVm() => new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, new EventAggregator(), _mockLogger.Object); // ── 6.1 AddOperatorCommand ──────────────────────────────────── [Fact] public void AddOperator_ValidKey_AppendsNode() { var vm = CreateVm(); vm.AddOperatorCommand.Execute("Blur"); Assert.Single(vm.PipelineNodes); Assert.Equal("Blur", vm.PipelineNodes[0].OperatorKey); Assert.Equal(0, vm.PipelineNodes[0].Order); } [Fact] public void AddOperator_KnownKey_SetsIconPath() { _mockImageSvc.Setup(s => s.GetAvailableProcessors()).Returns(new[] { "ShockFilter" }); var vm = CreateVm(); vm.AddOperatorCommand.Execute("ShockFilter"); Assert.Single(vm.PipelineNodes); Assert.Equal("⚡", vm.PipelineNodes[0].IconPath); } [Fact] public void AddOperator_InvalidKey_NodeNotAdded() { var vm = CreateVm(); vm.AddOperatorCommand.Execute("NonExistentKey"); Assert.Empty(vm.PipelineNodes); Assert.Contains("未注册", vm.StatusMessage); } [Fact] public void AddOperator_NullKey_NodeNotAdded() { var vm = CreateVm(); vm.AddOperatorCommand.Execute(null); Assert.Empty(vm.PipelineNodes); } [Fact] public void AddOperator_ExceedsMaxLength_NodeNotAdded() { // 主项目中 MaxPipelineLength = 20,通过反射读取或直接添加 20 个 var vm = CreateVm(); for (int i = 0; i < 20; i++) vm.AddOperatorCommand.Execute("Blur"); Assert.Equal(20, vm.PipelineNodes.Count); vm.AddOperatorCommand.Execute("Blur"); Assert.Equal(20, vm.PipelineNodes.Count); Assert.Contains("上限", vm.StatusMessage); } [Fact] public void AddOperator_MultipleNodes_OrderIsSequential() { var vm = CreateVm(); vm.AddOperatorCommand.Execute("Blur"); vm.AddOperatorCommand.Execute("Sharpen"); vm.AddOperatorCommand.Execute("Threshold"); Assert.Equal(3, vm.PipelineNodes.Count); for (int i = 0; i < 3; i++) Assert.Equal(i, vm.PipelineNodes[i].Order); } [Fact] public void LoadImageFromFile_SetsSourceImage() { var vm = CreateVm(); var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".png"); try { var bitmap = TestHelpers.CreateTestBitmap(8, 8); var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(bitmap)); using (var stream = File.Create(tempPath)) { encoder.Save(stream); } vm.LoadImageFromFile(tempPath); Assert.NotNull(vm.SourceImage); Assert.NotNull(vm.PreviewImage); Assert.Contains(Path.GetFileName(tempPath), vm.StatusMessage); } finally { if (File.Exists(tempPath)) File.Delete(tempPath); } } // ── 6.2 RemoveOperatorCommand ───────────────────────────────── [Fact] public void RemoveOperator_ExistingNode_NodeRemoved() { var vm = CreateVm(); vm.AddOperatorCommand.Execute("Blur"); var node = vm.PipelineNodes[0]; vm.RemoveOperatorCommand.Execute(node); Assert.Empty(vm.PipelineNodes); } [Fact] public void RemoveOperator_MiddleNode_OrderRenumbered() { var vm = CreateVm(); vm.AddOperatorCommand.Execute("Blur"); vm.AddOperatorCommand.Execute("Sharpen"); vm.AddOperatorCommand.Execute("Threshold"); var middle = vm.PipelineNodes[1]; vm.RemoveOperatorCommand.Execute(middle); Assert.Equal(2, vm.PipelineNodes.Count); Assert.Equal(0, vm.PipelineNodes[0].Order); Assert.Equal(1, vm.PipelineNodes[1].Order); } [Fact] public void RemoveOperator_SelectedNode_SelectedNodeCleared() { var vm = CreateVm(); vm.AddOperatorCommand.Execute("Blur"); var node = vm.PipelineNodes[0]; vm.SelectedNode = node; vm.RemoveOperatorCommand.Execute(node); Assert.Null(vm.SelectedNode); } [Fact] public void RemoveOperator_NullNode_NoException() { var vm = CreateVm(); var ex = Record.Exception(() => vm.RemoveOperatorCommand.Execute(null)); Assert.Null(ex); } // ── 6.3 ReorderOperatorCommand ──────────────────────────────── [Fact] public void ReorderOperator_MoveFirstToLast_OrderConsistent() { var vm = CreateVm(); vm.AddOperatorCommand.Execute("Blur"); vm.AddOperatorCommand.Execute("Sharpen"); vm.AddOperatorCommand.Execute("Threshold"); vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs { OldIndex = 0, NewIndex = 2 }); for (int i = 0; i < vm.PipelineNodes.Count; i++) Assert.Equal(i, vm.PipelineNodes[i].Order); } [Fact] public void ReorderOperator_MoveLastToFirst_OrderConsistent() { var vm = CreateVm(); vm.AddOperatorCommand.Execute("Blur"); vm.AddOperatorCommand.Execute("Sharpen"); vm.AddOperatorCommand.Execute("Threshold"); var lastName = vm.PipelineNodes[2].OperatorKey; vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs { OldIndex = 2, NewIndex = 0 }); Assert.Equal(lastName, vm.PipelineNodes[0].OperatorKey); for (int i = 0; i < vm.PipelineNodes.Count; i++) Assert.Equal(i, vm.PipelineNodes[i].Order); } [Fact] public void ReorderOperator_SameIndex_NoChange() { var vm = CreateVm(); vm.AddOperatorCommand.Execute("Blur"); vm.AddOperatorCommand.Execute("Sharpen"); var originalFirst = vm.PipelineNodes[0].OperatorKey; vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs { OldIndex = 0, NewIndex = 0 }); Assert.Equal(originalFirst, vm.PipelineNodes[0].OperatorKey); } [Fact] public void ReorderOperator_OutOfBoundsIndex_NoException() { var vm = CreateVm(); vm.AddOperatorCommand.Execute("Blur"); var ex = Record.Exception(() => vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs { OldIndex = 0, NewIndex = 99 })); Assert.Null(ex); } // ── 6.4 LoadNodeParameters ──────────────────────────────────── [Fact] public void LoadNodeParameters_ModifiedValuePreserved() { var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 20); _mockImageSvc.Setup(s => s.GetProcessorParameters("Blur")) .Returns(new[] { param }); var vm = CreateVm(); vm.AddOperatorCommand.Execute("Blur"); var node = vm.PipelineNodes[0]; // 修改参数值 node.Parameters[0].Value = 10; // 重新选中节点触发 LoadNodeParameters vm.SelectedNode = null; vm.SelectedNode = node; Assert.Equal(10, node.Parameters[0].Value); } [Fact] public void LoadNodeParameters_UnmodifiedUsesDefault() { var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 20); _mockImageSvc.Setup(s => s.GetProcessorParameters("Blur")) .Returns(new[] { param }); var vm = CreateVm(); vm.AddOperatorCommand.Execute("Blur"); var node = vm.PipelineNodes[0]; Assert.Equal(5, node.Parameters[0].Value); } // ── 6.8 参数范围校验 ────────────────────────────────────────── [Fact] public void ParameterValidation_ValueWithinRange_IsValid() { var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 10); var vm = new ProcessorParameterVM(param) { Value = 7 }; Assert.True(vm.IsValueValid); } [Fact] public void ParameterValidation_ValueBelowMin_IsInvalid() { var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 10); var vm = new ProcessorParameterVM(param) { Value = 0 }; Assert.False(vm.IsValueValid); } [Fact] public void ParameterValidation_ValueAboveMax_IsInvalid() { var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 10); var vm = new ProcessorParameterVM(param) { Value = 11 }; Assert.False(vm.IsValueValid); } [Fact] public void ParameterValidation_BoundaryValues_AreValid() { var param = TestHelpers.MakeIntParam("Radius", value: 5, min: 1, max: 10); var vmMin = new ProcessorParameterVM(param) { Value = 1 }; var vmMax = new ProcessorParameterVM(param) { Value = 10 }; Assert.True(vmMin.IsValueValid); Assert.True(vmMax.IsValueValid); } } }