From 8caaa8ee54ba5c85f86465b653047d6207792645 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 19 Jan 2026 17:51:15 -0500 Subject: [PATCH] feat(configmanager): add TreeNodeViewModel --- .../ViewModels/TreeNodeViewModel.cs | 63 ++++- .../ViewModels/TreeNodeViewModelTests.cs | 221 ++++++++++++++++++ 2 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/TreeNodeViewModelTests.cs diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs index 1b6f332..f8db87b 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs @@ -2,17 +2,35 @@ using System.Collections.ObjectModel; namespace JdeScoping.ConfigManager.ViewModels; +public enum TreeNodeType +{ + Folder, + SettingsSection, + Pipeline +} + +public enum ValidationState +{ + Valid, + Warning, + Error, + Unknown +} + /// /// ViewModel for a tree node in the configuration tree. -/// This is a stub implementation for Task 11 - full implementation in Task 12. /// public class TreeNodeViewModel : ViewModelBase { private bool _isModified; + private bool _isExpanded; + private bool _isSelected; + private ValidationState _validationState = ValidationState.Unknown; - public string Name { get; set; } = string.Empty; - public string Icon { get; set; } = string.Empty; - public string StatusIcon { get; set; } = string.Empty; + public string Name { get; } + public string Icon { get; } + public TreeNodeType NodeType { get; } + public string? SectionKey { get; init; } public ObservableCollection Children { get; } = []; public bool IsModified @@ -20,4 +38,41 @@ public class TreeNodeViewModel : ViewModelBase get => _isModified; set => SetProperty(ref _isModified, value); } + + public bool IsExpanded + { + get => _isExpanded; + set => SetProperty(ref _isExpanded, value); + } + + public bool IsSelected + { + get => _isSelected; + set => SetProperty(ref _isSelected, value); + } + + public ValidationState ValidationState + { + get => _validationState; + set + { + if (SetProperty(ref _validationState, value)) + OnPropertyChanged(nameof(StatusIcon)); + } + } + + public string StatusIcon => ValidationState switch + { + ValidationState.Valid => "✓", + ValidationState.Warning => "⚠", + ValidationState.Error => "✗", + _ => "" + }; + + public TreeNodeViewModel(string name, string icon, TreeNodeType nodeType) + { + Name = name; + Icon = icon; + NodeType = nodeType; + } } diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/TreeNodeViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/TreeNodeViewModelTests.cs new file mode 100644 index 0000000..2a071df --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/TreeNodeViewModelTests.cs @@ -0,0 +1,221 @@ +using JdeScoping.ConfigManager.ViewModels; + +namespace JdeScoping.ConfigManager.Tests.ViewModels; + +public class TreeNodeViewModelTests +{ + [Fact] + public void Constructor_SetsProperties() + { + // Arrange & Act + var node = new TreeNodeViewModel("DataSync", "⟳", TreeNodeType.SettingsSection); + + // Assert + node.Name.ShouldBe("DataSync"); + node.Icon.ShouldBe("⟳"); + node.NodeType.ShouldBe(TreeNodeType.SettingsSection); + } + + [Fact] + public void IsModified_WhenSet_RaisesPropertyChanged() + { + // Arrange + var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder); + var propertyChangedRaised = false; + node.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(TreeNodeViewModel.IsModified)) + propertyChangedRaised = true; + }; + + // Act + node.IsModified = true; + + // Assert + propertyChangedRaised.ShouldBeTrue(); + } + + [Fact] + public void IsExpanded_WhenSet_RaisesPropertyChanged() + { + // Arrange + var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder); + var propertyChangedRaised = false; + node.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(TreeNodeViewModel.IsExpanded)) + propertyChangedRaised = true; + }; + + // Act + node.IsExpanded = true; + + // Assert + propertyChangedRaised.ShouldBeTrue(); + node.IsExpanded.ShouldBeTrue(); + } + + [Fact] + public void IsSelected_WhenSet_RaisesPropertyChanged() + { + // Arrange + var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder); + var propertyChangedRaised = false; + node.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(TreeNodeViewModel.IsSelected)) + propertyChangedRaised = true; + }; + + // Act + node.IsSelected = true; + + // Assert + propertyChangedRaised.ShouldBeTrue(); + node.IsSelected.ShouldBeTrue(); + } + + [Fact] + public void ValidationState_WhenSetToValid_StatusIconIsCheckmark() + { + // Arrange + var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder); + + // Act + node.ValidationState = ValidationState.Valid; + + // Assert + node.StatusIcon.ShouldBe("✓"); + } + + [Fact] + public void ValidationState_WhenSetToWarning_StatusIconIsWarning() + { + // Arrange + var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder); + + // Act + node.ValidationState = ValidationState.Warning; + + // Assert + node.StatusIcon.ShouldBe("⚠"); + } + + [Fact] + public void ValidationState_WhenSetToError_StatusIconIsX() + { + // Arrange + var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder); + + // Act + node.ValidationState = ValidationState.Error; + + // Assert + node.StatusIcon.ShouldBe("✗"); + } + + [Fact] + public void ValidationState_WhenSetToUnknown_StatusIconIsEmpty() + { + // Arrange + var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder); + + // Act + node.ValidationState = ValidationState.Unknown; + + // Assert + node.StatusIcon.ShouldBe(""); + } + + [Fact] + public void ValidationState_WhenChanged_RaisesPropertyChangedForStatusIcon() + { + // Arrange + var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder); + var statusIconPropertyChanged = false; + node.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(TreeNodeViewModel.StatusIcon)) + statusIconPropertyChanged = true; + }; + + // Act + node.ValidationState = ValidationState.Valid; + + // Assert + statusIconPropertyChanged.ShouldBeTrue(); + } + + [Fact] + public void SectionKey_CanBeSetViaInitializer() + { + // Arrange & Act + var node = new TreeNodeViewModel("DataSync", "⟳", TreeNodeType.SettingsSection) + { + SectionKey = "DataSync" + }; + + // Assert + node.SectionKey.ShouldBe("DataSync"); + } + + [Fact] + public void Children_IsInitialized() + { + // Arrange & Act + var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder); + + // Assert + node.Children.ShouldNotBeNull(); + node.Children.ShouldBeEmpty(); + } + + [Fact] + public void Children_CanAddChildNodes() + { + // Arrange + var parent = new TreeNodeViewModel("Settings", "📁", TreeNodeType.Folder); + var child = new TreeNodeViewModel("DataSync", "⟳", TreeNodeType.SettingsSection); + + // Act + parent.Children.Add(child); + + // Assert + parent.Children.Count.ShouldBe(1); + parent.Children[0].ShouldBe(child); + } + + [Theory] + [InlineData(TreeNodeType.Folder)] + [InlineData(TreeNodeType.SettingsSection)] + [InlineData(TreeNodeType.Pipeline)] + public void NodeType_CanBeSetForAllTypes(TreeNodeType nodeType) + { + // Arrange & Act + var node = new TreeNodeViewModel("Test", "📁", nodeType); + + // Assert + node.NodeType.ShouldBe(nodeType); + } + + [Fact] + public void IsModified_WhenSetToSameValue_DoesNotRaisePropertyChanged() + { + // Arrange + var node = new TreeNodeViewModel("Test", "📁", TreeNodeType.Folder); + node.IsModified = true; + + var propertyChangedRaised = false; + node.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(TreeNodeViewModel.IsModified)) + propertyChangedRaised = true; + }; + + // Act + node.IsModified = true; // Same value + + // Assert + propertyChangedRaised.ShouldBeFalse(); + } +}