From cc555e4e34b6cdd288eaa8a4933d38029a366ee7 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 19 Jan 2026 19:45:07 -0500 Subject: [PATCH] feat(configmanager): add SearchFormViewModel Implements Task 18 from phases 7-9 plan. SearchFormViewModel wraps SearchSection model with properties for MaxResultRows, TimeoutSeconds, and MaxConcurrentSearches. Includes full test coverage with 7 tests verifying initialization, two-way binding, change notification, and null argument handling. --- .../ViewModels/Forms/SearchFormViewModel.cs | 69 +++++++++++ .../Forms/SearchFormViewModelTests.cs | 108 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SearchFormViewModel.cs create mode 100644 NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/SearchFormViewModelTests.cs diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SearchFormViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SearchFormViewModel.cs new file mode 100644 index 0000000..e2f6d20 --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SearchFormViewModel.cs @@ -0,0 +1,69 @@ +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.ViewModels.Forms; + +/// +/// ViewModel for editing Search configuration section. +/// +public class SearchFormViewModel : ViewModelBase +{ + private readonly SearchSection _model; + private readonly Action _onChanged; + + public SearchFormViewModel(SearchSection model, Action onChanged) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + _onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged)); + } + + /// + /// Gets or sets the maximum number of result rows returned by a search. + /// + public int MaxResultRows + { + get => _model.MaxResultRows; + set + { + if (_model.MaxResultRows != value) + { + _model.MaxResultRows = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the timeout in seconds for search operations. + /// + public int TimeoutSeconds + { + get => _model.TimeoutSeconds; + set + { + if (_model.TimeoutSeconds != value) + { + _model.TimeoutSeconds = value; + OnPropertyChanged(); + _onChanged(); + } + } + } + + /// + /// Gets or sets the maximum number of concurrent search operations allowed. + /// + public int MaxConcurrentSearches + { + get => _model.MaxConcurrentSearches; + set + { + if (_model.MaxConcurrentSearches != value) + { + _model.MaxConcurrentSearches = value; + OnPropertyChanged(); + _onChanged(); + } + } + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/SearchFormViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/SearchFormViewModelTests.cs new file mode 100644 index 0000000..342968d --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/SearchFormViewModelTests.cs @@ -0,0 +1,108 @@ +using JdeScoping.ConfigManager.Models; +using JdeScoping.ConfigManager.ViewModels.Forms; + +namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms; + +public class SearchFormViewModelTests +{ + [Fact] + public void Constructor_InitializesFromModel() + { + // Arrange + var model = new SearchSection + { + MaxResultRows = 50000, + TimeoutSeconds = 600, + MaxConcurrentSearches = 10 + }; + + // Act + var sut = new SearchFormViewModel(model, () => { }); + + // Assert + sut.MaxResultRows.ShouldBe(50000); + sut.TimeoutSeconds.ShouldBe(600); + sut.MaxConcurrentSearches.ShouldBe(10); + } + + [Fact] + public void PropertyChange_UpdatesModel() + { + // Arrange + var model = new SearchSection { MaxResultRows = 100000 }; + var sut = new SearchFormViewModel(model, () => { }); + + // Act + sut.MaxResultRows = 25000; + + // Assert + model.MaxResultRows.ShouldBe(25000); + } + + [Fact] + public void PropertyChange_InvokesOnChanged() + { + // Arrange + var model = new SearchSection(); + var changedInvoked = false; + var sut = new SearchFormViewModel(model, () => changedInvoked = true); + + // Act + sut.TimeoutSeconds = 120; + + // Assert + changedInvoked.ShouldBeTrue(); + } + + [Fact] + public void PropertyChange_RaisesPropertyChanged() + { + // Arrange + var model = new SearchSection(); + var sut = new SearchFormViewModel(model, () => { }); + var propertyChangedRaised = false; + sut.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(SearchFormViewModel.MaxConcurrentSearches)) + propertyChangedRaised = true; + }; + + // Act + sut.MaxConcurrentSearches = 8; + + // Assert + propertyChangedRaised.ShouldBeTrue(); + } + + [Fact] + public void Constructor_ThrowsOnNullModel() + { + // Act & Assert + Should.Throw(() => new SearchFormViewModel(null!, () => { })); + } + + [Fact] + public void Constructor_ThrowsOnNullCallback() + { + // Arrange + var model = new SearchSection(); + + // Act & Assert + Should.Throw(() => new SearchFormViewModel(model, null!)); + } + + [Fact] + public void PropertyChange_DoesNotInvokeOnChangedWhenValueUnchanged() + { + // Arrange + var model = new SearchSection { MaxResultRows = 50000 }; + var changedCount = 0; + var sut = new SearchFormViewModel(model, () => changedCount++); + + // Act - set to same value + sut.MaxResultRows = 50000; + + // Assert + changedCount.ShouldBe(0); + } +}