diff --git a/SysManager/SysManager.Tests/AppPackageTests.cs b/SysManager/SysManager.Tests/AppPackageTests.cs index 47ac18d3..57fe7009 100644 --- a/SysManager/SysManager.Tests/AppPackageTests.cs +++ b/SysManager/SysManager.Tests/AppPackageTests.cs @@ -32,16 +32,9 @@ public void IsSelected_RaisesPropertyChanged() Assert.Contains(nameof(AppPackage.IsSelected), raised); } - [Fact] - public void Status_Transitions_ArePossible() - { - var p = new AppPackage(); - foreach (var s in new[] { "Pending", "Upgrading...", "Done", "Failed (exit 1)" }) - { - p.Status = s; - Assert.Equal(s, p.Status); - } - } + // Status_Transitions_ArePossible was removed: assigning four strings to a string property and + // reading each back cannot fail. "Transitions are possible" was never in question — what would + // be worth pinning is who WRITES those values, which belongs to the winget service's tests. [Fact] public void ScenarioLikeRealData_IsStored() diff --git a/SysManager/SysManager.Tests/CleanupViewModelTests.cs b/SysManager/SysManager.Tests/CleanupViewModelTests.cs index 49dd9a94..cca600d0 100644 --- a/SysManager/SysManager.Tests/CleanupViewModelTests.cs +++ b/SysManager/SysManager.Tests/CleanupViewModelTests.cs @@ -330,16 +330,9 @@ public void StatusMessage_Setter_RaisesPropertyChanged() Assert.Equal("hello", vm.StatusMessage); } - [Fact] - public void Progress_AcceptsFullRange() - { - var vm = NewVm(); - foreach (var p in new[] { 0, 1, 50, 99, 100 }) - { - vm.Progress = p; - Assert.Equal(p, vm.Progress); - } - } + // Progress_AcceptsFullRange was removed: it looped five values through a bare + // [ObservableProperty] and read each back. The name implied a 0-100 contract that nothing + // enforces (ViewModelBase._progress is unclamped; the ProgressBar clamps visually). [Fact] public void IsProgressIndeterminate_TogglesCleanly() @@ -386,22 +379,9 @@ public async Task PreScan_EventuallyPopulatesLabels() Assert.NotEqual("Scanning…", vm.RecycleBinLabel); } - [Fact] - public void TempSizeLabel_CanBeSetDirectly() - { - var vm = NewVm(); - vm.TempSizeLabel = "42.0 MB can be freed"; - Assert.Equal("42.0 MB can be freed", vm.TempSizeLabel); - } - - [Fact] - public void RecycleBinLabel_CanBeSetDirectly() - { - var vm = NewVm(); - vm.RecycleBinLabel = "Empty"; - Assert.Equal("Empty", vm.RecycleBinLabel); - } - + // A "…_CanBeSetDirectly" round-trip was removed here: it set a bare [ObservableProperty] + // and read it straight back, so only the source generator could fail it. The labels' real + // behaviour is covered by …_DefaultIsScanning and PreScan_EventuallyPopulatesLabels above. // ---------- progress feedback (regression) ---------- // CleanupView.xaml binds a progress bar to IsBusy and the sidebar spinner reads the same flag, // but this VM never assigned it — so nothing appeared while SFC or DISM ran, which is minutes of diff --git a/SysManager/SysManager.Tests/DashboardViewModelTests.cs b/SysManager/SysManager.Tests/DashboardViewModelTests.cs index 0768ceaf..1b98f481 100644 --- a/SysManager/SysManager.Tests/DashboardViewModelTests.cs +++ b/SysManager/SysManager.Tests/DashboardViewModelTests.cs @@ -89,47 +89,27 @@ public void Command_IsExposedAndNotNull(string name) } // ---------- property setters ---------- - - [Fact] - public void OsLine_Setter_Works() - { - var vm = NewVm(); - vm.OsLine = "Windows 11 Pro"; - Assert.Equal("Windows 11 Pro", vm.OsLine); - } - - [Fact] - public void UptimeLine_Setter_Works() - { - var vm = NewVm(); - vm.UptimeLine = "Uptime 3d 5h"; - Assert.Equal("Uptime 3d 5h", vm.UptimeLine); - } - - [Fact] - public void CpuPercent_Setter_Works() - { - var vm = NewVm(); - vm.CpuPercent = 42.5; - Assert.Equal(42.5, vm.CpuPercent); - } - - [Fact] - public void RamPercent_Setter_Works() - { - var vm = NewVm(); - vm.RamPercent = 67.3; - Assert.Equal(67.3, vm.RamPercent); - } + // The four "…_Setter_Works" round-trips that lived here were removed: each set a bare + // [ObservableProperty] and read it straight back, which can only fail if the CommunityToolkit + // source generator breaks. What a binding depends on — the change notification — is covered by + // Setter_FiresPropertyChanged below, which the two percentages were added to. // ---------- PropertyChanged ---------- + /// + /// Every bound property must raise PropertyChanged: the dashboard is written by a background + /// poll loop, so the UI only updates on the notification. The parameter is object so the two + /// percentages can join the string rows — they arrived here when their standalone round-trip tests + /// were removed, because notification is the half that a binding actually depends on. + /// [Theory] [InlineData(nameof(DashboardViewModel.OsLine), "test")] [InlineData(nameof(DashboardViewModel.UptimeLine), "test")] [InlineData(nameof(DashboardViewModel.CpuName), "test")] [InlineData(nameof(DashboardViewModel.GpuName), "test")] - public void Setter_FiresPropertyChanged(string propName, string value) + [InlineData(nameof(DashboardViewModel.CpuPercent), 42.5)] + [InlineData(nameof(DashboardViewModel.RamPercent), 67.3)] + public void Setter_FiresPropertyChanged(string propName, object value) { var vm = NewVm(); var fired = false; diff --git a/SysManager/SysManager.Tests/DuplicateFileViewModelTests.cs b/SysManager/SysManager.Tests/DuplicateFileViewModelTests.cs index 2a093de8..4f25de52 100644 --- a/SysManager/SysManager.Tests/DuplicateFileViewModelTests.cs +++ b/SysManager/SysManager.Tests/DuplicateFileViewModelTests.cs @@ -106,13 +106,10 @@ public void BrowseFolderCommand_Exists() Assert.NotNull(vm.BrowseFolderCommand); } - [Fact] - public void MinSizeKb_CanBeChanged() - { - var vm = NewVm(); - vm.MinSizeKb = 500; - Assert.Equal(500, vm.MinSizeKb); - } + // MinSizeKb_CanBeChanged was removed as a setter round-trip. The property's real consequence is + // `var minBytes = MinSizeKb * 1024` in ScanAsync, and asserting THAT needs a guard the code does + // not have yet (a large typed value overflows long and inverts the filter) — tracked separately + // so this test-only change stays behaviour-neutral. [Fact] public void SelectedFolder_CanBeChanged() diff --git a/SysManager/SysManager.Tests/FriendlyEventEntryExtendedTests.cs b/SysManager/SysManager.Tests/FriendlyEventEntryExtendedTests.cs index 8d3b33f8..a0caafe6 100644 --- a/SysManager/SysManager.Tests/FriendlyEventEntryExtendedTests.cs +++ b/SysManager/SysManager.Tests/FriendlyEventEntryExtendedTests.cs @@ -67,13 +67,32 @@ public void SeverityColor_UnknownSeverity_GivesFallback() Assert.Equal(StatusColors.Neutral, e.SeverityColor); } + /// + /// The extreme timestamps a real event log can hand us must not produce nonsense in the two strings + /// the user actually sees. + /// Replaces an assertion that stored DateTime.MinValue/MaxValue and read them back + /// — a round-trip through a generated setter, proving only that a DateTime field holds a + /// DateTime. The reachable question is what RelativeTime and FullTimestamp DO with + /// those values, since both are computed from Timestamp and RelativeTime subtracts it + /// from DateTime.Now. + /// This pins the behaviour rather than changing it: MinValue is special-cased to an em dash, and + /// a FUTURE stamp (a clock correction, or a log copied from a machine running ahead) yields "just now", + /// because the negative span falls into the first branch. Imprecise but harmless — and far better than + /// printing "in -3d". Asserted so a future reordering of those branches cannot silently start showing + /// the user a negative duration. + /// [Fact] - public void TimestampsVastlyDifferent_StillStorable() + public void ExtremeTimestamps_ProduceSaneDisplayStrings() { - var e = new FriendlyEventEntry { Timestamp = DateTime.MinValue }; - Assert.Equal(DateTime.MinValue, e.Timestamp); - e.Timestamp = DateTime.MaxValue; - Assert.Equal(DateTime.MaxValue, e.Timestamp); + var min = new FriendlyEventEntry { Timestamp = DateTime.MinValue }; + Assert.Equal("—", min.RelativeTime); + Assert.Equal("0001-01-01 00:00:00", min.FullTimestamp); + + var future = new FriendlyEventEntry { Timestamp = DateTime.MaxValue }; + Assert.Equal("just now", future.RelativeTime); + Assert.Equal("9999-12-31 23:59:59", future.FullTimestamp); + // The point of the assertion: no negative duration ever reaches the user. + Assert.DoesNotContain("-", future.RelativeTime); } [Fact] diff --git a/SysManager/SysManager.Tests/PerformanceViewModelTests.cs b/SysManager/SysManager.Tests/PerformanceViewModelTests.cs index bf2b7959..c37adf02 100644 --- a/SysManager/SysManager.Tests/PerformanceViewModelTests.cs +++ b/SysManager/SysManager.Tests/PerformanceViewModelTests.cs @@ -123,13 +123,8 @@ public void Constructor_WantToggles_DefaultFalse() // ── Property changes ── - [Fact] - public void SelectedPlan_CanBeChanged() - { - var vm = NewVm(); - vm.SelectedPlan = "ultimate"; - Assert.Equal("ultimate", vm.SelectedPlan); - } + // SelectedPlan_CanBeChanged was removed as a pure setter round-trip: the default is pinned by + // Constructor_SelectedPlan_DefaultBalanced and the notification by SelectedPlan_NotifiesPropertyChanged. [Fact] public void WantVisualEffectsReduced_CanBeToggled() diff --git a/SysManager/SysManager.Tests/PingTargetTests.cs b/SysManager/SysManager.Tests/PingTargetTests.cs index 41f2320f..1a7378be 100644 --- a/SysManager/SysManager.Tests/PingTargetTests.cs +++ b/SysManager/SysManager.Tests/PingTargetTests.cs @@ -90,15 +90,7 @@ public void Role_CanBeSet(TargetRole role) Assert.Equal(role, t.Role); } - [Fact] - public void Stats_CanBeUpdated() - { - var t = new PingTarget(); - t.AverageMs = 25.3; - t.JitterMs = 2.1; - t.LossPercent = 5.0; - Assert.Equal(25.3, t.AverageMs); - Assert.Equal(2.1, t.JitterMs); - Assert.Equal(5.0, t.LossPercent); - } + // Stats_CanBeUpdated was removed: three doubles assigned and read back on a model with no + // computed members over them. PingTarget's meaningful behaviour is in the ping service, which + // has its own tests. } diff --git a/SysManager/SysManager.Tests/ProcessManagerViewModelTests.cs b/SysManager/SysManager.Tests/ProcessManagerViewModelTests.cs index f08504c5..354d3f42 100644 --- a/SysManager/SysManager.Tests/ProcessManagerViewModelTests.cs +++ b/SysManager/SysManager.Tests/ProcessManagerViewModelTests.cs @@ -43,12 +43,50 @@ public void FilterText_DefaultEmpty() Assert.Equal("", vm.FilterText); } + /// + /// Typing in the search box must actually narrow the bound list. + /// Replaces an assertion that set FilterText and then read FilterText back — a + /// round-trip through a generated [ObservableProperty] setter, which can only fail if the + /// source generator itself breaks. What the user depends on is the CONSEQUENCE: + /// OnFilterTextChanged calls ApplyFilter, which rebuilds FilteredProcesses — + /// the collection the DataGrid binds to. A filter wired to nothing is a defect this project has + /// shipped before (batch 30, five unreachable Services filters), and the old assertion could not + /// have caught it. + /// [Fact] - public void FilterText_CanBeChanged() + public void FilterText_NarrowsTheBoundList() { var vm = new ProcessManagerViewModel(new Services.ProcessManagerService()); + vm.Processes.Clear(); + vm.Processes.Add(new ProcessEntry { Pid = 1, Name = "chrome", MemoryBytes = 300 }); + vm.Processes.Add(new ProcessEntry { Pid = 2, Name = "notepad", MemoryBytes = 200 }); + vm.FilterText = "chrome"; - Assert.Equal("chrome", vm.FilterText); + + Assert.Equal(["chrome"], vm.FilteredProcesses.Select(p => p.Name).ToArray()); + + // …and clearing it restores the full list, so the filter is not one-way. + vm.FilterText = ""; + Assert.Equal(2, vm.FilteredProcesses.Count); + } + + /// + /// The filter matches a PID and the description fields too, not only the name: three separate + /// Matches… helpers feed it and only the name path was ever exercised. + /// + [Fact] + public void FilterText_AlsoMatchesPidAndDescription() + { + var vm = new ProcessManagerViewModel(new Services.ProcessManagerService()); + vm.Processes.Clear(); + vm.Processes.Add(new ProcessEntry { Pid = 4321, Name = "svchost", PlainDescription = "Windows service host" }); + vm.Processes.Add(new ProcessEntry { Pid = 9, Name = "notepad" }); + + vm.FilterText = "4321"; + Assert.Equal(["svchost"], vm.FilteredProcesses.Select(p => p.Name).ToArray()); + + vm.FilterText = "service host"; + Assert.Equal(["svchost"], vm.FilteredProcesses.Select(p => p.Name).ToArray()); } [Fact] diff --git a/SysManager/SysManager.Tests/UninstallerViewModelTests.cs b/SysManager/SysManager.Tests/UninstallerViewModelTests.cs index 074adfeb..d24537c5 100644 --- a/SysManager/SysManager.Tests/UninstallerViewModelTests.cs +++ b/SysManager/SysManager.Tests/UninstallerViewModelTests.cs @@ -56,12 +56,46 @@ public void Summary_HasDefaultValue() Assert.False(string.IsNullOrEmpty(vm.Summary)); } + /// + /// Typing in the search box must narrow the bound list AND correct the count and summary the user + /// reads above it. Replaces a FilterText round-trip that only exercised the generated setter; + /// ApplyFilter rebuilds FilteredApps, recomputes AppCount and rewrites + /// Summary, and none of those three was asserted anywhere. + /// [Fact] - public void FilterText_CanBeChanged() + public void FilterText_NarrowsTheListAndCorrectsTheCountAndSummary() { var vm = NewVm(); + vm.AllApps.Clear(); + vm.AllApps.Add(new InstalledApp { Name = "Google Chrome", Id = "Google.Chrome" }); + vm.AllApps.Add(new InstalledApp { Name = "Notepad++", Id = "Notepad.Plus" }); + vm.FilterText = "chrome"; - Assert.Equal("chrome", vm.FilterText); + + Assert.Equal(["Google Chrome"], vm.FilteredApps.Select(a => a.Name).ToArray()); + Assert.Equal(1, vm.AppCount); + Assert.Contains("of 2 total", vm.Summary); + + // Clearing it restores everything, and the summary drops the "(of N total)" qualifier because + // nothing is filtered out any more. + vm.FilterText = ""; + Assert.Equal(2, vm.FilteredApps.Count); + Assert.Equal(2, vm.AppCount); + Assert.DoesNotContain("of 2 total", vm.Summary); + } + + /// The filter matches the package Id too, not just the display name. + [Fact] + public void FilterText_AlsoMatchesThePackageId() + { + var vm = NewVm(); + vm.AllApps.Clear(); + vm.AllApps.Add(new InstalledApp { Name = "Google Chrome", Id = "Google.Chrome" }); + vm.AllApps.Add(new InstalledApp { Name = "Notepad++", Id = "Notepad.Plus" }); + + vm.FilterText = "Notepad.Plus"; + + Assert.Equal(["Notepad++"], vm.FilteredApps.Select(a => a.Name).ToArray()); } [Fact]