Files
XplorePlane/XplorePlane.Tests/ViewModels/MainViewModelRecordingTests.cs
T
2026-05-12 20:48:40 +08:00

266 lines
10 KiB
C#

using System;
using System.Threading;
using System.Windows;
using Moq;
using Xunit;
using XplorePlane.Services.Recording;
using XP.Common.Logging.Interfaces;
using XplorePlane.Services.Storage;
namespace XplorePlane.Tests.ViewModels
{
/// <summary>
/// Unit tests for MainViewModel recording state transitions and command behavior.
/// Tests the integration between MainViewModel and IViewportRecordingService.
///
/// Note: These tests focus on the ViewportRecordingService behavior rather than
/// full MainViewModel integration due to complex dependencies.
/// </summary>
public class MainViewModelRecordingTests
{
/// <summary>
/// Test: RecordingService state transitions trigger correct state changes
/// **Validates: Requirements 2.1, 3.3**
/// </summary>
[StaFact]
public void RecordingService_StateChanged_IdleToRecording_TriggersEvent()
{
// Arrange
var mockLogger = new Mock<ILoggerService>();
var mockDataPathService = new Mock<IXpDataPathService>();
mockLogger.Setup(x => x.ForModule<ViewportRecordingService>()).Returns(mockLogger.Object);
string testPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"XP_Test_{Guid.NewGuid():N}");
mockDataPathService.Setup(x => x.RootPath).Returns(testPath);
var service = new ViewportRecordingService(mockDataPathService.Object, mockLogger.Object);
RecordingState? newState = null;
service.StateChanged += (sender, args) =>
{
newState = args.NewState;
};
// Act
var captureTarget = CreateMockFrameworkElement(800, 600);
bool started = service.StartRecording(captureTarget);
// If codec not available, skip
if (!started)
{
service.Dispose();
return;
}
// Assert
Assert.Equal(RecordingState.Recording, newState);
Assert.Equal(RecordingState.Recording, service.CurrentState);
// Cleanup
service.Dispose();
if (System.IO.Directory.Exists(testPath))
{
try { System.IO.Directory.Delete(testPath, true); } catch { }
}
}
/// <summary>
/// Test: RecordingService transitions from Recording to Saving to Idle
/// **Validates: Requirements 3.1, 3.3**
/// </summary>
[StaFact]
public void RecordingService_StopRecording_TransitionsToSavingThenIdle()
{
// Arrange
var mockLogger = new Mock<ILoggerService>();
var mockDataPathService = new Mock<IXpDataPathService>();
mockLogger.Setup(x => x.ForModule<ViewportRecordingService>()).Returns(mockLogger.Object);
string testPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"XP_Test_{Guid.NewGuid():N}");
mockDataPathService.Setup(x => x.RootPath).Returns(testPath);
var service = new ViewportRecordingService(mockDataPathService.Object, mockLogger.Object);
var states = new System.Collections.Generic.List<RecordingState>();
service.StateChanged += (sender, args) =>
{
states.Add(args.NewState);
};
var captureTarget = CreateMockFrameworkElement(640, 480);
bool started = service.StartRecording(captureTarget);
// If codec not available, skip
if (!started)
{
service.Dispose();
return;
}
// Act
Thread.Sleep(100); // Let it record briefly
service.StopRecording();
// Wait for state to settle
Thread.Sleep(500);
// Assert
Assert.Contains(RecordingState.Recording, states);
Assert.Contains(RecordingState.Saving, states);
Assert.Contains(RecordingState.Idle, states);
Assert.Equal(RecordingState.Idle, service.CurrentState);
// Cleanup
service.Dispose();
if (System.IO.Directory.Exists(testPath))
{
try { System.IO.Directory.Delete(testPath, true); } catch { }
}
}
/// <summary>
/// Test: RecordingService cannot start when already recording
/// **Validates: Requirements 2.4**
/// </summary>
[StaFact]
public void RecordingService_StartRecording_WhenAlreadyRecording_ReturnsFalse()
{
// Arrange
var mockLogger = new Mock<ILoggerService>();
var mockDataPathService = new Mock<IXpDataPathService>();
mockLogger.Setup(x => x.ForModule<ViewportRecordingService>()).Returns(mockLogger.Object);
string testPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"XP_Test_{Guid.NewGuid():N}");
mockDataPathService.Setup(x => x.RootPath).Returns(testPath);
var service = new ViewportRecordingService(mockDataPathService.Object, mockLogger.Object);
var captureTarget = CreateMockFrameworkElement(800, 600);
bool firstStart = service.StartRecording(captureTarget);
// If codec not available, skip
if (!firstStart)
{
service.Dispose();
return;
}
// Act
bool secondStartResult = service.StartRecording(captureTarget);
// Assert
Assert.False(secondStartResult);
Assert.Equal(RecordingState.Recording, service.CurrentState);
// Cleanup
service.Dispose();
if (System.IO.Directory.Exists(testPath))
{
try { System.IO.Directory.Delete(testPath, true); } catch { }
}
}
/// <summary>
/// Test: RecordingService ElapsedTime increases during recording
/// **Validates: Requirements 2.2**
/// </summary>
[StaFact]
public void RecordingService_ElapsedTime_IncreasesWhileRecording()
{
// Arrange
var mockLogger = new Mock<ILoggerService>();
var mockDataPathService = new Mock<IXpDataPathService>();
mockLogger.Setup(x => x.ForModule<ViewportRecordingService>()).Returns(mockLogger.Object);
string testPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"XP_Test_{Guid.NewGuid():N}");
mockDataPathService.Setup(x => x.RootPath).Returns(testPath);
var service = new ViewportRecordingService(mockDataPathService.Object, mockLogger.Object);
var captureTarget = CreateMockFrameworkElement(640, 480);
bool started = service.StartRecording(captureTarget);
// If codec not available, skip
if (!started)
{
service.Dispose();
return;
}
// Act
Thread.Sleep(1500); // Wait 1.5 seconds
var elapsed = service.ElapsedTime;
// Assert
Assert.True(elapsed.TotalSeconds >= 1.0, $"Expected elapsed time >= 1 second, got {elapsed.TotalSeconds}");
Assert.True(elapsed.TotalSeconds < 3.0, $"Expected elapsed time < 3 seconds, got {elapsed.TotalSeconds}");
// Cleanup
service.Dispose();
if (System.IO.Directory.Exists(testPath))
{
try { System.IO.Directory.Delete(testPath, true); } catch { }
}
}
/// <summary>
/// Test: RecordingService ElapsedTime is zero when idle
/// **Validates: Requirements 2.2**
/// </summary>
[Fact]
public void RecordingService_ElapsedTime_IsZeroWhenIdle()
{
// Arrange
var mockLogger = new Mock<ILoggerService>();
var mockDataPathService = new Mock<IXpDataPathService>();
mockLogger.Setup(x => x.ForModule<ViewportRecordingService>()).Returns(mockLogger.Object);
mockDataPathService.Setup(x => x.RootPath).Returns(@"C:\TestPath");
var service = new ViewportRecordingService(mockDataPathService.Object, mockLogger.Object);
// Act & Assert
Assert.Equal(TimeSpan.Zero, service.ElapsedTime);
Assert.Equal(RecordingState.Idle, service.CurrentState);
// Cleanup
service.Dispose();
}
/// <summary>
/// Test: FormatElapsedTime produces correct MM:SS format
/// **Validates: Requirements 2.2**
/// </summary>
[Fact]
public void FormatElapsedTime_ProducesCorrectFormat()
{
// Act & Assert
Assert.Equal("00:00", ViewportRecordingService.FormatElapsedTime(0));
Assert.Equal("00:30", ViewportRecordingService.FormatElapsedTime(30));
Assert.Equal("01:00", ViewportRecordingService.FormatElapsedTime(60));
Assert.Equal("02:30", ViewportRecordingService.FormatElapsedTime(150));
Assert.Equal("59:59", ViewportRecordingService.FormatElapsedTime(3599));
}
// ── Helper Methods ───────────────────────────────────────────────────
/// <summary>
/// Creates a mock FrameworkElement with specified dimensions for testing.
/// </summary>
private FrameworkElement CreateMockFrameworkElement(int width, int height)
{
var canvas = new System.Windows.Controls.Canvas
{
Width = width,
Height = height,
Background = System.Windows.Media.Brushes.White
};
// Force layout to set ActualWidth/ActualHeight
canvas.Measure(new Size(width, height));
canvas.Arrange(new Rect(0, 0, width, height));
return canvas;
}
}
}