feat(01-06): implement FeatureViewModelBase with async/cancel/progress pattern
- Add ProgressUpdatedMessage ValueChangedMessage for StatusBar live updates - Add FeatureViewModelBase with CancellationTokenSource lifecycle, IsRunning, IProgress<OperationProgress>, OperationCanceledException handling - Add 6 unit tests covering lifecycle, progress, cancellation, error handling and CanExecute guard
This commit is contained in:
@@ -1,7 +1,125 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.ViewModels;
|
||||
|
||||
namespace SharepointToolbox.Tests.ViewModels;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public class FeatureViewModelBaseTests
|
||||
{
|
||||
[Fact(Skip = "Wave 0 stub — implemented in plan 01-06")]
|
||||
public void CancelCommand_Cancels_RunningOperation() { }
|
||||
private class TestViewModel : FeatureViewModelBase
|
||||
{
|
||||
public TestViewModel() : base(NullLogger<FeatureViewModelBase>.Instance) { }
|
||||
public Func<CancellationToken, IProgress<OperationProgress>, Task>? OperationFunc { get; set; }
|
||||
protected override Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
|
||||
=> OperationFunc?.Invoke(ct, progress) ?? Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsRunning_IsTrueWhileOperationExecutes_ThenFalseAfterCompletion()
|
||||
{
|
||||
var vm = new TestViewModel();
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
bool wasRunningDuringOperation = false;
|
||||
|
||||
vm.OperationFunc = async (ct, p) =>
|
||||
{
|
||||
wasRunningDuringOperation = vm.IsRunning;
|
||||
await tcs.Task;
|
||||
};
|
||||
|
||||
var runTask = vm.RunCommand.ExecuteAsync(null);
|
||||
// Give run task time to start
|
||||
await Task.Delay(10);
|
||||
|
||||
Assert.True(wasRunningDuringOperation);
|
||||
tcs.SetResult(true);
|
||||
await runTask;
|
||||
|
||||
Assert.False(vm.IsRunning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProgressValue_AndStatusMessage_UpdateViaIProgress()
|
||||
{
|
||||
var vm = new TestViewModel();
|
||||
vm.OperationFunc = async (ct, progress) =>
|
||||
{
|
||||
progress.Report(new OperationProgress(50, 100, "halfway"));
|
||||
await Task.Yield();
|
||||
};
|
||||
|
||||
await vm.RunCommand.ExecuteAsync(null);
|
||||
|
||||
// Allow dispatcher to process
|
||||
await Task.Delay(20);
|
||||
|
||||
Assert.Equal(50, vm.ProgressValue);
|
||||
Assert.Equal("halfway", vm.StatusMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancelCommand_DuringOperation_SetsStatusMessageToCancelled()
|
||||
{
|
||||
var vm = new TestViewModel();
|
||||
var started = new TaskCompletionSource<bool>();
|
||||
|
||||
vm.OperationFunc = async (ct, p) =>
|
||||
{
|
||||
started.SetResult(true);
|
||||
await Task.Delay(5000, ct); // Will be cancelled
|
||||
};
|
||||
|
||||
var runTask = vm.RunCommand.ExecuteAsync(null);
|
||||
await started.Task;
|
||||
|
||||
vm.CancelCommand.Execute(null);
|
||||
await runTask;
|
||||
|
||||
Assert.Contains("cancel", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.False(vm.IsRunning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OperationCanceledException_IsCaughtGracefully_IsRunningBecomesFalse()
|
||||
{
|
||||
var vm = new TestViewModel();
|
||||
vm.OperationFunc = (ct, p) => throw new OperationCanceledException();
|
||||
|
||||
// Should not throw
|
||||
await vm.RunCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.False(vm.IsRunning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExceptionDuringOperation_SetsStatusMessageToErrorText_IsRunningBecomesFalse()
|
||||
{
|
||||
var vm = new TestViewModel();
|
||||
vm.OperationFunc = (ct, p) => throw new InvalidOperationException("test error");
|
||||
|
||||
await vm.RunCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.False(vm.IsRunning);
|
||||
Assert.Contains("test error", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunCommand_CannotBeInvoked_WhileIsRunning()
|
||||
{
|
||||
var vm = new TestViewModel();
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
vm.OperationFunc = async (ct, p) => await tcs.Task;
|
||||
|
||||
var runTask = vm.RunCommand.ExecuteAsync(null);
|
||||
await Task.Delay(10); // Let it start
|
||||
|
||||
Assert.False(vm.RunCommand.CanExecute(null));
|
||||
|
||||
tcs.SetResult(true);
|
||||
await runTask;
|
||||
|
||||
Assert.True(vm.RunCommand.CanExecute(null));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user