Files
hiha-arvio/tests/HihaArvio.Tests/ViewModels/HistoryViewModelTests.cs
Ismo Vuorinen 48a844b0c1 feat: implement ViewModels layer with TDD (Milestone 3)
Implemented all ViewModels using strict TDD (RED-GREEN-REFACTOR) with CommunityToolkit.Mvvm.

**ViewModels Implemented:**
- MainViewModel: Coordinates shake detection, estimate generation, and display
- HistoryViewModel: Manages estimate history display and clearing
- SettingsViewModel: Handles app settings (mode selection, history size)

**MainViewModel Features:**
- Subscribes to ShakeDetectionService events
- Generates estimates when shake stops (transition from shaking→not shaking)
- Automatically saves estimates to storage
- Resets shake detection after estimate generation
- Loads and saves settings (SelectedMode)
- Proper disposal pattern (unsubscribe, stop monitoring)

**HistoryViewModel Features:**
- ObservableCollection<EstimateResult> for UI binding
- LoadHistoryCommand to fetch from storage
- ClearHistoryCommand to remove all history
- IsEmpty property for conditional UI display
- Replaces collection contents on reload

**SettingsViewModel Features:**
- SelectedMode property (Work/Generic)
- MaxHistorySize property
- SaveSettingsCommand to persist changes
- Loads settings on initialization

**Tests:**
- MainViewModel: 18 tests (RED-GREEN-REFACTOR)
- HistoryViewModel: 15 tests (RED-GREEN-REFACTOR)
- SettingsViewModel: 13 tests (RED-GREEN-REFACTOR)
- Total: 165 tests, all passing (48 models + 71 services + 46 ViewModels)

**Quality:**
- Build: 0 warnings, 0 errors across all platforms
- All tests use NSubstitute for mocking
- Property change notifications verified
- Async operations properly tested

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 12:44:07 +02:00

269 lines
8.2 KiB
C#

using System.Collections.ObjectModel;
using FluentAssertions;
using HihaArvio.Models;
using HihaArvio.Services.Interfaces;
using HihaArvio.ViewModels;
using NSubstitute;
namespace HihaArvio.Tests.ViewModels;
public class HistoryViewModelTests
{
private readonly IStorageService _storageService;
private readonly HistoryViewModel _viewModel;
public HistoryViewModelTests()
{
_storageService = Substitute.For<IStorageService>();
_viewModel = new HistoryViewModel(_storageService);
}
#region Initialization Tests
[Fact]
public void Constructor_ShouldInitializeWithEmptyHistory()
{
// Assert
_viewModel.History.Should().NotBeNull();
_viewModel.History.Should().BeEmpty();
}
[Fact]
public void Constructor_ShouldInitializeIsEmptyAsTrue()
{
// Assert
_viewModel.IsEmpty.Should().BeTrue();
}
[Fact]
public void Constructor_ShouldHaveLoadHistoryCommand()
{
// Assert
_viewModel.LoadHistoryCommand.Should().NotBeNull();
}
[Fact]
public void Constructor_ShouldHaveClearHistoryCommand()
{
// Assert
_viewModel.ClearHistoryCommand.Should().NotBeNull();
}
#endregion
#region Load History Tests
[Fact]
public async Task LoadHistoryCommand_ShouldLoadHistoryFromStorage()
{
// Arrange
var estimates = new List<EstimateResult>
{
EstimateResult.Create("2 weeks", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5)),
EstimateResult.Create("1 day", EstimateMode.Work, 0.3, TimeSpan.FromSeconds(3))
};
_storageService.GetHistoryAsync(Arg.Any<int>()).Returns(Task.FromResult(estimates));
// Act
await _viewModel.LoadHistoryCommand.ExecuteAsync(null);
// Assert
await _storageService.Received(1).GetHistoryAsync(Arg.Any<int>());
}
[Fact]
public async Task LoadHistoryCommand_ShouldPopulateHistoryCollection()
{
// Arrange
var estimates = new List<EstimateResult>
{
EstimateResult.Create("2 weeks", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5)),
EstimateResult.Create("1 day", EstimateMode.Work, 0.3, TimeSpan.FromSeconds(3)),
EstimateResult.Create("3 months", EstimateMode.Work, 0.8, TimeSpan.FromSeconds(8))
};
_storageService.GetHistoryAsync(Arg.Any<int>()).Returns(Task.FromResult(estimates));
// Act
await _viewModel.LoadHistoryCommand.ExecuteAsync(null);
// Assert
_viewModel.History.Should().HaveCount(3);
_viewModel.History[0].EstimateText.Should().Be("2 weeks");
_viewModel.History[1].EstimateText.Should().Be("1 day");
_viewModel.History[2].EstimateText.Should().Be("3 months");
}
[Fact]
public async Task LoadHistoryCommand_WhenHistoryEmpty_ShouldSetIsEmptyToTrue()
{
// Arrange
_storageService.GetHistoryAsync(Arg.Any<int>()).Returns(Task.FromResult(new List<EstimateResult>()));
// Act
await _viewModel.LoadHistoryCommand.ExecuteAsync(null);
// Assert
_viewModel.IsEmpty.Should().BeTrue();
}
[Fact]
public async Task LoadHistoryCommand_WhenHistoryHasItems_ShouldSetIsEmptyToFalse()
{
// Arrange
var estimates = new List<EstimateResult>
{
EstimateResult.Create("2 weeks", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5))
};
_storageService.GetHistoryAsync(Arg.Any<int>()).Returns(Task.FromResult(estimates));
// Act
await _viewModel.LoadHistoryCommand.ExecuteAsync(null);
// Assert
_viewModel.IsEmpty.Should().BeFalse();
}
[Fact]
public async Task LoadHistoryCommand_CalledMultipleTimes_ShouldReplaceHistory()
{
// Arrange
var firstEstimates = new List<EstimateResult>
{
EstimateResult.Create("2 weeks", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5))
};
var secondEstimates = new List<EstimateResult>
{
EstimateResult.Create("1 day", EstimateMode.Work, 0.3, TimeSpan.FromSeconds(3)),
EstimateResult.Create("3 months", EstimateMode.Work, 0.8, TimeSpan.FromSeconds(8))
};
_storageService.GetHistoryAsync(Arg.Any<int>()).Returns(
Task.FromResult(firstEstimates),
Task.FromResult(secondEstimates));
// Act
await _viewModel.LoadHistoryCommand.ExecuteAsync(null);
await _viewModel.LoadHistoryCommand.ExecuteAsync(null);
// Assert
_viewModel.History.Should().HaveCount(2);
_viewModel.History[0].EstimateText.Should().Be("1 day");
}
#endregion
#region Clear History Tests
[Fact]
public async Task ClearHistoryCommand_ShouldCallStorageServiceClear()
{
// Act
await _viewModel.ClearHistoryCommand.ExecuteAsync(null);
// Assert
await _storageService.Received(1).ClearHistoryAsync();
}
[Fact]
public async Task ClearHistoryCommand_ShouldClearHistoryCollection()
{
// Arrange - Load some history first
var estimates = new List<EstimateResult>
{
EstimateResult.Create("2 weeks", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5)),
EstimateResult.Create("1 day", EstimateMode.Work, 0.3, TimeSpan.FromSeconds(3))
};
_storageService.GetHistoryAsync(Arg.Any<int>()).Returns(Task.FromResult(estimates));
await _viewModel.LoadHistoryCommand.ExecuteAsync(null);
_viewModel.History.Should().HaveCount(2);
// Act
await _viewModel.ClearHistoryCommand.ExecuteAsync(null);
// Assert
_viewModel.History.Should().BeEmpty();
}
[Fact]
public async Task ClearHistoryCommand_ShouldSetIsEmptyToTrue()
{
// Arrange - Load some history first
var estimates = new List<EstimateResult>
{
EstimateResult.Create("2 weeks", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5))
};
_storageService.GetHistoryAsync(Arg.Any<int>()).Returns(Task.FromResult(estimates));
await _viewModel.LoadHistoryCommand.ExecuteAsync(null);
_viewModel.IsEmpty.Should().BeFalse();
// Act
await _viewModel.ClearHistoryCommand.ExecuteAsync(null);
// Assert
_viewModel.IsEmpty.Should().BeTrue();
}
#endregion
#region Property Change Notification Tests
[Fact]
public async Task History_WhenLoadHistoryCalled_ShouldPopulateCollection()
{
// Arrange
var collectionChangedCount = 0;
_viewModel.History.CollectionChanged += (sender, args) => collectionChangedCount++;
var estimates = new List<EstimateResult>
{
EstimateResult.Create("2 weeks", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5))
};
_storageService.GetHistoryAsync(Arg.Any<int>()).Returns(Task.FromResult(estimates));
// Act
await _viewModel.LoadHistoryCommand.ExecuteAsync(null);
// Assert
collectionChangedCount.Should().BeGreaterThan(0);
_viewModel.History.Should().HaveCount(1);
}
[Fact]
public async Task IsEmpty_WhenChanged_ShouldRaisePropertyChanged()
{
// Arrange
var propertyChangedCount = 0;
_viewModel.PropertyChanged += (sender, args) =>
{
if (args.PropertyName == nameof(HistoryViewModel.IsEmpty))
propertyChangedCount++;
};
var estimates = new List<EstimateResult>
{
EstimateResult.Create("2 weeks", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5))
};
_storageService.GetHistoryAsync(Arg.Any<int>()).Returns(Task.FromResult(estimates));
// Act
await _viewModel.LoadHistoryCommand.ExecuteAsync(null); // IsEmpty changes from true to false
await _viewModel.ClearHistoryCommand.ExecuteAsync(null); // IsEmpty changes from false to true
// Assert
propertyChangedCount.Should().BeGreaterThanOrEqualTo(2);
}
#endregion
#region Observable Collection Tests
[Fact]
public void History_ShouldBeObservableCollection()
{
// Assert
_viewModel.History.Should().BeOfType<ObservableCollection<EstimateResult>>();
}
#endregion
}