using System.Globalization; using Microsoft.Extensions.Logging.Abstractions; using SharepointToolbox.Core.Models; using SharepointToolbox.ViewModels; namespace SharepointToolbox.Tests.ViewModels; [Trait("Category", "Unit")] public class FeatureViewModelBaseTests { private class TestViewModel : FeatureViewModelBase { public TestViewModel() : base(NullLogger.Instance) { } public Func, Task>? OperationFunc { get; set; } protected override Task RunOperationAsync(CancellationToken ct, IProgress progress) => OperationFunc?.Invoke(ct, progress) ?? Task.CompletedTask; } [Fact] public async Task IsRunning_IsTrueWhileOperationExecutes_ThenFalseAfterCompletion() { var vm = new TestViewModel(); var tcs = new TaskCompletionSource(); 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() { // Ensure EN culture so TranslationSource resolves "Operation cancelled" var prev = CultureInfo.CurrentUICulture; CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en"); try { var vm = new TestViewModel(); var started = new TaskCompletionSource(); 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); } finally { CultureInfo.CurrentUICulture = prev; } } [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(); 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)); } }