Files
XplorePlane/XplorePlane.Tests/ViewModels/InspectionReportViewerViewModelTests.cs
T
zhengxuan.zhang 8b29285d03 CNC结果预览
2026-05-12 00:29:21 +08:00

393 lines
15 KiB
C#

// Feature: cnc-inspection-report-viewer
// Task 4.2: Unit tests for LoadDetailAsync selection-change and cancellation paths
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services;
using XplorePlane.Services.InspectionResults;
using XplorePlane.ViewModels.Cnc;
using Xunit;
namespace XplorePlane.Tests.ViewModels
{
public class InspectionReportViewerViewModelTests
{
// ── Helpers ──────────────────────────────────────────────────────────────────
private static InspectionReportViewerViewModel CreateVm(
Mock<IInspectionResultStore> mockStore = null,
Mock<ILoggerService> mockLogger = null,
Mock<IXpDataPathService> mockDataPathService = null)
{
mockStore ??= new Mock<IInspectionResultStore>();
mockLogger ??= new Mock<ILoggerService>();
mockDataPathService ??= new Mock<IXpDataPathService>();
mockLogger.Setup(l => l.ForModule<InspectionReportViewerViewModel>()).Returns(mockLogger.Object);
mockDataPathService.SetupGet(s => s.DataPath).Returns(System.IO.Path.GetTempPath());
// Setup default QueryRunsAsync to return empty list
mockStore
.Setup(s => s.QueryRunsAsync(It.IsAny<InspectionRunQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<InspectionRunRecord>());
return new InspectionReportViewerViewModel(
mockStore.Object,
mockLogger.Object,
mockDataPathService.Object);
}
private static InspectionRunDetail CreateMockDetail(Guid runId, string programName = "TestProgram")
{
return new InspectionRunDetail
{
Run = new InspectionRunRecord
{
RunId = runId,
ProgramName = programName,
WorkpieceId = "WP001",
SerialNumber = "SN001",
StartedAt = DateTime.UtcNow,
CompletedAt = DateTime.UtcNow.AddMinutes(5),
OverallPass = true,
Status = InspectionRunStatus.Completed,
NodeCount = 2
},
Nodes = new List<InspectionNodeResult>
{
new InspectionNodeResult
{
NodeId = Guid.NewGuid(),
NodeIndex = 0,
NodeName = "Node_0",
PipelineName = "Pipeline_0",
NodePass = true,
Status = InspectionNodeStatus.Completed,
DurationMs = 100
},
new InspectionNodeResult
{
NodeId = Guid.NewGuid(),
NodeIndex = 1,
NodeName = "Node_1",
PipelineName = "Pipeline_1",
NodePass = true,
Status = InspectionNodeStatus.Completed,
DurationMs = 150
}
},
Metrics = new List<InspectionMetricResult>(),
Assets = new List<InspectionAssetRecord>(),
PipelineSnapshots = new List<PipelineExecutionSnapshot>(),
Events = new List<InspectionRunEvent>()
};
}
// ── Task 4.2: LoadDetailAsync selection-change and cancellation paths ────────
/// <summary>
/// Test: Switching SelectedRun cancels prior load
/// Requirements: 4.8, 11.5
/// </summary>
[Fact]
public async Task LoadDetailAsync_WhenSelectedRunChanges_CancelsPriorLoad()
{
// Arrange
var runId1 = Guid.NewGuid();
var runId2 = Guid.NewGuid();
var detail1 = CreateMockDetail(runId1, "Program1");
var detail2 = CreateMockDetail(runId2, "Program2");
var tcs1 = new TaskCompletionSource<InspectionRunDetail>();
var tcs2 = new TaskCompletionSource<InspectionRunDetail>();
var mockStore = new Mock<IInspectionResultStore>();
// Setup QueryRunsAsync to return two runs
mockStore
.Setup(s => s.QueryRunsAsync(It.IsAny<InspectionRunQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<InspectionRunRecord>
{
detail1.Run,
detail2.Run
});
var callCount = 0;
mockStore
.Setup(s => s.GetRunDetailAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.Returns<Guid, CancellationToken>((id, ct) =>
{
callCount++;
if (callCount == 1)
{
// First call - register cancellation callback
ct.Register(() => tcs1.TrySetCanceled());
return tcs1.Task;
}
else
{
// Second call - complete immediately
tcs2.SetResult(detail2);
return tcs2.Task;
}
});
var vm = CreateVm(mockStore);
// Wait for initial load to complete
await Task.Delay(100);
// Act
// Select first run (starts loading)
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
vm.SelectedRun = new InspectionRunRowViewModel(detail1.Run);
});
// Give the first load a moment to start
await Task.Delay(50);
// Select second run (should cancel first load)
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
vm.SelectedRun = new InspectionRunRowViewModel(detail2.Run);
});
// Wait for second load to complete
await Task.Delay(200);
// Assert
Assert.True(tcs1.Task.IsCanceled, "First load should have been cancelled");
Assert.True(tcs2.Task.IsCompletedSuccessfully, "Second load should have completed");
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
Assert.Equal("Program2", vm.DetailRun?.ProgramName);
Assert.Equal(2, vm.DetailNodes.Count);
});
}
/// <summary>
/// Test: Cancellation is silent (no error displayed)
/// Requirements: 11.5
/// </summary>
[Fact]
public async Task LoadDetailAsync_WhenCancelled_IsSilent()
{
// Arrange
var runId = Guid.NewGuid();
var detail = CreateMockDetail(runId);
var tcs = new TaskCompletionSource<InspectionRunDetail>();
var mockStore = new Mock<IInspectionResultStore>();
mockStore
.Setup(s => s.QueryRunsAsync(It.IsAny<InspectionRunQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<InspectionRunRecord> { detail.Run });
mockStore
.Setup(s => s.GetRunDetailAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.Returns<Guid, CancellationToken>((id, ct) =>
{
ct.Register(() => tcs.TrySetCanceled());
return tcs.Task;
});
var vm = CreateVm(mockStore);
// Wait for initial load
await Task.Delay(100);
// Act
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
vm.SelectedRun = new InspectionRunRowViewModel(detail.Run);
});
// Give it a moment to start loading
await Task.Delay(50);
// Cancel by selecting null
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
vm.SelectedRun = null;
});
// Wait for cancellation to propagate
await Task.Delay(200);
// Assert - cancellation should be silent (no error state)
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
Assert.False(vm.HasDetailError, "HasDetailError should be false after cancellation");
Assert.Null(vm.DetailError);
Assert.False(vm.IsDetailLoading, "IsDetailLoading should be false after cancellation");
});
}
/// <summary>
/// Test: Error sets HasDetailError
/// Requirements: 4.8
/// </summary>
[Fact]
public async Task LoadDetailAsync_WhenErrorOccurs_SetsHasDetailError()
{
// Arrange
var runId = Guid.NewGuid();
var detail = CreateMockDetail(runId);
var expectedErrorMessage = "Database connection failed";
var mockStore = new Mock<IInspectionResultStore>();
mockStore
.Setup(s => s.QueryRunsAsync(It.IsAny<InspectionRunQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<InspectionRunRecord> { detail.Run });
mockStore
.Setup(s => s.GetRunDetailAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException(expectedErrorMessage));
var vm = CreateVm(mockStore);
// Wait for initial load
await Task.Delay(100);
// Act
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
vm.SelectedRun = new InspectionRunRowViewModel(detail.Run);
});
// Wait for error to be processed
await Task.Delay(200);
// Assert
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
Assert.True(vm.HasDetailError, "HasDetailError should be true after error");
Assert.NotNull(vm.DetailError);
Assert.Contains(expectedErrorMessage, vm.DetailError);
Assert.False(vm.IsDetailLoading, "IsDetailLoading should be false after error");
});
}
/// <summary>
/// Test: Successful load clears error state and populates detail
/// Requirements: 4.8
/// </summary>
[Fact]
public async Task LoadDetailAsync_WhenSuccessful_PopulatesDetailAndClearsErrors()
{
// Arrange
var runId = Guid.NewGuid();
var detail = CreateMockDetail(runId, "SuccessProgram");
var mockStore = new Mock<IInspectionResultStore>();
mockStore
.Setup(s => s.QueryRunsAsync(It.IsAny<InspectionRunQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<InspectionRunRecord> { detail.Run });
mockStore
.Setup(s => s.GetRunDetailAsync(runId, It.IsAny<CancellationToken>()))
.ReturnsAsync(detail);
var vm = CreateVm(mockStore);
// Wait for initial load
await Task.Delay(100);
// Act
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
vm.SelectedRun = new InspectionRunRowViewModel(detail.Run);
});
// Wait for load to complete
await Task.Delay(200);
// Assert
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
Assert.False(vm.HasDetailError, "HasDetailError should be false after successful load");
Assert.Null(vm.DetailError);
Assert.False(vm.IsDetailLoading, "IsDetailLoading should be false after load completes");
Assert.NotNull(vm.DetailRun);
Assert.Equal("SuccessProgram", vm.DetailRun.ProgramName);
Assert.Equal(2, vm.DetailNodes.Count);
Assert.Equal("Node_0", vm.DetailNodes[0].NodeName);
Assert.Equal("Node_1", vm.DetailNodes[1].NodeName);
});
}
/// <summary>
/// Test: LoadDetailAsync clears previous detail state before loading
/// Requirements: 4.8
/// </summary>
[Fact]
public async Task LoadDetailAsync_ClearsPreviousDetailState()
{
// Arrange
var runId1 = Guid.NewGuid();
var runId2 = Guid.NewGuid();
var detail1 = CreateMockDetail(runId1, "Program1");
var detail2 = CreateMockDetail(runId2, "Program2");
var mockStore = new Mock<IInspectionResultStore>();
mockStore
.Setup(s => s.QueryRunsAsync(It.IsAny<InspectionRunQuery>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<InspectionRunRecord> { detail1.Run, detail2.Run });
mockStore
.Setup(s => s.GetRunDetailAsync(runId1, It.IsAny<CancellationToken>()))
.ReturnsAsync(detail1);
mockStore
.Setup(s => s.GetRunDetailAsync(runId2, It.IsAny<CancellationToken>()))
.ReturnsAsync(detail2);
var vm = CreateVm(mockStore);
// Wait for initial load
await Task.Delay(100);
// Act - Load first detail
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
vm.SelectedRun = new InspectionRunRowViewModel(detail1.Run);
});
await Task.Delay(200);
// Verify first detail is loaded
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
Assert.Equal("Program1", vm.DetailRun?.ProgramName);
});
// Load second detail
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
vm.SelectedRun = new InspectionRunRowViewModel(detail2.Run);
});
await Task.Delay(200);
// Assert - second detail should replace first
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
Assert.Equal("Program2", vm.DetailRun?.ProgramName);
Assert.Equal(2, vm.DetailNodes.Count);
Assert.All(vm.DetailNodes, node => Assert.Contains("Node_", node.NodeName));
});
}
}
}