mirror of
https://github.com/ivuorinen/hiha-arvio.git
synced 2026-03-07 10:58:38 +00:00
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>
This commit is contained in:
268
tests/HihaArvio.Tests/ViewModels/HistoryViewModelTests.cs
Normal file
268
tests/HihaArvio.Tests/ViewModels/HistoryViewModelTests.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user