CNC结果预览
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
// 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user