test(09-04): add StorageViewModel chart unit tests

- 7 tests covering chart series from metrics, bar series structure,
  donut/bar toggle, top-10+Other aggregation, no-Other for <=10,
  tenant switch cleanup, and empty data handling
- Added LiveChartsCore.SkiaSharpView.WPF to test project
- Uses reflection to set FileTypeMetrics (private setter) directly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-07 15:40:26 +02:00
parent e2321666c6
commit 712b949eb2
2 changed files with 218 additions and 0 deletions

View File

@@ -11,6 +11,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" /> <PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="CsvHelper" Version="33.1.0" /> <PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.WPF" Version="2.0.0-rc5.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit" Version="2.9.3" />

View File

@@ -0,0 +1,217 @@
using System.Collections.ObjectModel;
using System.Reflection;
using CommunityToolkit.Mvvm.Messaging;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using SharepointToolbox.Core.Messages;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Services;
using SharepointToolbox.ViewModels;
using SharepointToolbox.ViewModels.Tabs;
namespace SharepointToolbox.Tests.ViewModels;
/// <summary>
/// Unit tests for StorageViewModel chart functionality (Phase 09 Plan 04).
/// Verifies: chart series from metrics, bar series structure, donut/bar toggle,
/// top-10 + Other aggregation, no-Other for <=10, tenant switch cleanup, empty data.
/// Uses reflection to set FileTypeMetrics directly, bypassing ClientContext dependency.
/// </summary>
public class StorageViewModelChartTests
{
public StorageViewModelChartTests()
{
WeakReferenceMessenger.Default.Reset();
}
// -- Helper factories --------------------------------------------------------
private static StorageViewModel CreateViewModel()
{
var mockStorage = new Mock<IStorageService>();
var mockSession = new Mock<ISessionManager>();
var vm = new StorageViewModel(
mockStorage.Object,
mockSession.Object,
NullLogger<FeatureViewModelBase>.Instance);
vm.SetCurrentProfile(new TenantProfile
{
Name = "Test",
TenantUrl = "https://test.sharepoint.com",
ClientId = "test-id"
});
return vm;
}
/// <summary>
/// Sets FileTypeMetrics via the property (private setter) using reflection,
/// which also triggers UpdateChartSeries.
/// </summary>
private static void SetFileTypeMetrics(StorageViewModel vm, IList<FileTypeMetric> metrics)
{
var prop = typeof(StorageViewModel).GetProperty(
nameof(StorageViewModel.FileTypeMetrics),
BindingFlags.Public | BindingFlags.Instance);
prop!.SetValue(vm, new ObservableCollection<FileTypeMetric>(metrics));
}
private static List<FileTypeMetric> MakeMetrics(int count)
{
var extensions = new[]
{
".docx", ".pdf", ".xlsx", ".pptx", ".jpg",
".png", ".mp4", ".zip", ".csv", ".html",
".txt", ".json", ".xml", ".msg", ".eml"
};
var metrics = new List<FileTypeMetric>();
for (int i = 0; i < count; i++)
{
string ext = i < extensions.Length ? extensions[i] : $".ext{i}";
metrics.Add(new FileTypeMetric(ext, (count - i) * 1024L * 1024, (count - i) * 10));
}
return metrics;
}
// -- Test 1: Chart series populated from metrics -----------------------------
[Fact]
public void After_setting_metrics_HasChartData_is_true_and_PieChartSeries_has_entries()
{
var vm = CreateViewModel();
var metrics = MakeMetrics(5);
SetFileTypeMetrics(vm, metrics);
Assert.True(vm.HasChartData);
Assert.NotEmpty(vm.PieChartSeries);
Assert.Equal(5, vm.PieChartSeries.Count());
}
// -- Test 2: Bar series has one ColumnSeries with correct value count --------
[Fact]
public void After_setting_metrics_BarChartSeries_has_one_ColumnSeries_with_matching_values()
{
var vm = CreateViewModel();
var metrics = MakeMetrics(5);
SetFileTypeMetrics(vm, metrics);
var barSeries = vm.BarChartSeries.ToList();
Assert.Single(barSeries);
var columnSeries = Assert.IsType<ColumnSeries<long>>(barSeries[0]);
Assert.Equal(5, columnSeries.Values!.Count());
}
// -- Test 3: Toggle IsDonutChart changes PieChartSeries InnerRadius ----------
[Fact]
public void Toggle_IsDonutChart_changes_PieChartSeries_InnerRadius()
{
var vm = CreateViewModel();
var metrics = MakeMetrics(3);
SetFileTypeMetrics(vm, metrics);
// Initially IsDonutChart=true => InnerRadius=50
var pieBefore = vm.PieChartSeries.Cast<PieSeries<long>>().ToList();
Assert.All(pieBefore, s => Assert.Equal(50, s.InnerRadius));
// Toggle to bar (not donut) => InnerRadius=0
vm.IsDonutChart = false;
var pieAfter = vm.PieChartSeries.Cast<PieSeries<long>>().ToList();
Assert.All(pieAfter, s => Assert.Equal(0, s.InnerRadius));
}
// -- Test 4: More than 10 file types => 11 entries (10 + Other) --------------
[Fact]
public void More_than_10_metrics_produces_11_series_entries_with_Other()
{
var vm = CreateViewModel();
var metrics = MakeMetrics(15);
SetFileTypeMetrics(vm, metrics);
// Pie series: 10 real + 1 "Other" = 11
Assert.Equal(11, vm.PieChartSeries.Count());
// Last pie entry should be named "OTHER" (DisplayLabel uppercases extension)
var lastPie = vm.PieChartSeries.Last();
Assert.Equal("OTHER", lastPie.Name);
// Bar series column should have 11 values
var columnSeries = Assert.IsType<ColumnSeries<long>>(vm.BarChartSeries.First());
Assert.Equal(11, columnSeries.Values!.Count());
// X-axis should have 11 labels
Assert.Equal(11, vm.BarXAxes[0].Labels!.Count);
}
// -- Test 5: 10 or fewer file types => no "Other" entry ----------------------
[Fact]
public void Ten_or_fewer_metrics_produces_no_Other_entry()
{
var vm = CreateViewModel();
var metrics = MakeMetrics(10);
SetFileTypeMetrics(vm, metrics);
Assert.Equal(10, vm.PieChartSeries.Count());
// No entry named "OTHER" (DisplayLabel uppercases)
Assert.DoesNotContain(vm.PieChartSeries, s => s.Name == "OTHER");
}
// -- Test 6: Tenant switch clears chart data ---------------------------------
[Fact]
public void OnTenantSwitched_clears_FileTypeMetrics_and_HasChartData_is_false()
{
var vm = CreateViewModel();
var metrics = MakeMetrics(5);
SetFileTypeMetrics(vm, metrics);
Assert.True(vm.HasChartData);
// Act: send TenantSwitchedMessage
var newProfile = new TenantProfile
{
Name = "NewTenant",
TenantUrl = "https://newtenant.sharepoint.com",
ClientId = "new-id"
};
WeakReferenceMessenger.Default.Send(new TenantSwitchedMessage(newProfile));
Assert.False(vm.HasChartData);
Assert.Empty(vm.FileTypeMetrics);
Assert.Empty(vm.PieChartSeries);
Assert.Empty(vm.BarChartSeries);
}
// -- Test 7: Empty metrics => HasChartData false, series empty ---------------
[Fact]
public void Empty_metrics_yields_HasChartData_false_and_empty_series()
{
var vm = CreateViewModel();
SetFileTypeMetrics(vm, new List<FileTypeMetric>());
Assert.False(vm.HasChartData);
Assert.Empty(vm.PieChartSeries);
Assert.Empty(vm.BarChartSeries);
Assert.Empty(vm.BarXAxes);
Assert.Empty(vm.BarYAxes);
}
}