From d944d0b5ed7efb9a8362a3523585733db109a857 Mon Sep 17 00:00:00 2001 From: Boris Date: Fri, 5 Dec 2025 16:20:27 +0100 Subject: [PATCH] Fix enum selection issues --- .../EnumTests.cs | 153 ++++++++++++++++++ .../SimpleMultiselect.razor.cs | 63 +++++--- 2 files changed, 193 insertions(+), 23 deletions(-) create mode 100644 src/SimpleBlazorMultiselect.Tests/EnumTests.cs diff --git a/src/SimpleBlazorMultiselect.Tests/EnumTests.cs b/src/SimpleBlazorMultiselect.Tests/EnumTests.cs new file mode 100644 index 0000000..91e5c51 --- /dev/null +++ b/src/SimpleBlazorMultiselect.Tests/EnumTests.cs @@ -0,0 +1,153 @@ +using AngleSharp.Html.Dom; +using Bunit; +using FluentAssertions; +using Microsoft.AspNetCore.Components; +using Xunit; + +namespace SimpleBlazorMultiselect.Tests; + +public class EnumTests : BaseTest +{ + [Fact] + public void Component_WithEmptySet_ShouldShowNoChecks() + { + var options = Enum.GetValues().ToList(); + var selectedItems = new HashSet(); + + var component = RenderComponent>(parameters => parameters + .Add(p => p.Options, options) + .Add(p => p.SelectedOptions, selectedItems) + .Add(p => p.StringSelector, item => item.ToString()) + .Add(p => p.DefaultText, "empty") + .Add(p => p.SelectedOptionsChanged, EventCallback.Factory.Create>(this, newSelection => { selectedItems = newSelection; }))); + + var button = component.Find("button"); + button.TextContent.Should().Be("empty"); + button.Click(); + + // Check if all options are unchecked + var dropdownItems = component.FindAll(".dropdown-item"); + foreach (var item in dropdownItems) + { + var checkbox = item.QuerySelector("input[type='checkbox']") as IHtmlInputElement; + checkbox.Should().NotBeNull(); + checkbox.IsChecked.Should().BeFalse(); + } + } + + [Fact] + public void Component_WithPrefilledOptions_ShouldShowCorrectCheck() + { + var options = Enum.GetValues().ToList(); + var selectedItems = new HashSet + { + TestEnum.OptionB + }; + + var component = RenderComponent>(parameters => parameters + .Add(p => p.Options, options) + .Add(p => p.SelectedOptions, selectedItems) + .Add(p => p.StringSelector, item => item.ToString()) + .Add(p => p.DefaultText, "empty") + .Add(p => p.SelectedOptionsChanged, EventCallback.Factory.Create>(this, newSelection => { selectedItems = newSelection; }))); + + var button = component.Find("button"); + button.TextContent.Should().Be(nameof(TestEnum.OptionB)); + button.Click(); + + // Check if all options are unchecked except OptionB + var dropdownItems = component.FindAll(".dropdown-item"); + foreach (var item in dropdownItems) + { + var checkbox = item.QuerySelector("input[type='checkbox']") as IHtmlInputElement; + checkbox.Should().NotBeNull(); + + var optionText = item.TextContent.Trim(); + if (optionText == nameof(TestEnum.OptionB)) + { + checkbox.Should().NotBeNull(); + checkbox.IsChecked.Should().BeTrue(); + } + else + { + checkbox.IsChecked.Should().BeFalse(); + } + } + } + + [Fact] + public void Component_CanUncheckPrefilled() + { + var options = Enum.GetValues().ToList(); + var selectedItems = new HashSet + { + TestEnum.OptionB + }; + + var component = RenderComponent>(parameters => parameters + .Add(p => p.Options, options) + .Add(p => p.SelectedOptions, selectedItems) + .Add(p => p.StringSelector, item => item.ToString()) + .Add(p => p.DefaultText, "empty") + .Add(p => p.SelectedOptionsChanged, EventCallback.Factory.Create>(this, newSelection => { selectedItems = newSelection; }))); + + var button = component.Find("button"); + button.TextContent.Should().Be(nameof(TestEnum.OptionB)); + button.Click(); + + // Check if option B is checked + var dropdownItems = component.FindAll(".dropdown-item"); + var optionBItem = dropdownItems.First(item => item.TextContent.Trim() == nameof(TestEnum.OptionB)); + var optionBCheckbox = optionBItem.QuerySelector("input[type='checkbox']") as IHtmlInputElement; + optionBCheckbox.Should().NotBeNull(); + optionBCheckbox.IsChecked.Should().BeTrue(); + + // Click to uncheck OptionB + optionBItem.Click(); + + // After clicking, OptionB should be deselected + selectedItems.Should().BeEmpty(); + button = component.Find("button"); + button.TextContent.Should().Be("empty"); + } + + [Fact] + public void Component_CanCheckUncheckedOption() + { + var options = Enum.GetValues().ToList(); + var selectedItems = new HashSet(); + + var component = RenderComponent>(parameters => parameters + .Add(p => p.Options, options) + .Add(p => p.SelectedOptions, selectedItems) + .Add(p => p.StringSelector, item => item.ToString()) + .Add(p => p.DefaultText, "empty") + .Add(p => p.SelectedOptionsChanged, EventCallback.Factory.Create>(this, newSelection => { selectedItems = newSelection; }))); + + var button = component.Find("button"); + button.TextContent.Should().Be("empty"); + button.Click(); + + // Check if all options are unchecked + var dropdownItems = component.FindAll(".dropdown-item"); + var optionCItem = dropdownItems.First(item => item.TextContent.Trim() == nameof(TestEnum.OptionC)); + var optionCCheckbox = optionCItem.QuerySelector("input[type='checkbox']") as IHtmlInputElement; + optionCCheckbox.Should().NotBeNull(); + optionCCheckbox.IsChecked.Should().BeFalse(); + + // Click to check OptionC + optionCItem.Click(); + + // After clicking, OptionC should be selected + selectedItems.Should().Contain(TestEnum.OptionC); + button = component.Find("button"); + button.TextContent.Should().Be(nameof(TestEnum.OptionC)); + } + + private enum TestEnum + { + OptionA, + OptionB, + OptionC + } +} \ No newline at end of file diff --git a/src/SimpleBlazorMultiselect/SimpleMultiselect.razor.cs b/src/SimpleBlazorMultiselect/SimpleMultiselect.razor.cs index 71d2514..5fbb9e5 100644 --- a/src/SimpleBlazorMultiselect/SimpleMultiselect.razor.cs +++ b/src/SimpleBlazorMultiselect/SimpleMultiselect.razor.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Components; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; namespace SimpleBlazorMultiselect; @@ -101,25 +102,30 @@ private bool DefaultFilterPredicate(TItem item, string filterString) /// [CascadingParameter(Name = "Standalone")] public bool Standalone { get; set; } - + /// /// If true, the selection of options will be matched by reference instead of by string representation. /// [Parameter] public bool MatchByReference { get; set; } - + private async Task ToggleOption(TItem option) { var newSelected = new HashSet(SelectedOptions); - - var existingSelected = FindSelected(option); - var wasSelected = existingSelected != null && newSelected.Remove(existingSelected); + + var wasSelected = false; + if (TryFindSelected(option, out var existing)) + { + wasSelected = newSelected.Remove(existing); + } + if (!wasSelected) { if (!IsMultiSelect) { newSelected.Clear(); } + newSelected.Add(option); } @@ -134,23 +140,34 @@ private async Task ToggleOption(TItem option) private bool IsOptionSelected(TItem option) { - return FindSelected(option) != null; + return TryFindSelected(option, out _); } - - /// - /// Helper function to find the selected option that matches the given options. - /// Options might not be the same reference. - /// - private TItem? FindSelected(TItem option) + + private bool TryFindSelected(TItem option, [NotNullWhen(true)] out TItem existing) { + if (SelectedOptions.Count == 0) + { + existing = default!; + return false; + } + if (MatchByReference) { - SelectedOptions.TryGetValue(option, out var existing); - return existing; + return SelectedOptions.TryGetValue(option, out existing!); } - + var optionString = StringSelector(option); - return SelectedOptions.FirstOrDefault(selected => StringSelector(selected) == optionString); + foreach (var selected in SelectedOptions) + { + if (StringSelector(selected) == optionString) + { + existing = selected!; + return true; + } + } + + existing = default!; + return false; } private List? _filteredOptionsCache; @@ -158,21 +175,21 @@ private bool IsOptionSelected(TItem option) private Func? _prevFilterPredicate; private string? _prevFilterText; private bool _prevCanFilter; - + private List FilteredOptions() { - if(_prevCanFilter == CanFilter && _prevFilterPredicate == FilterPredicate && _prevFilterText == _filterText && _prevOptions == Options) + if (_prevCanFilter == CanFilter && _prevFilterPredicate == FilterPredicate && _prevFilterText == _filterText && _prevOptions == Options) { return _filteredOptionsCache ?? Options; } - + _prevOptions = Options; _prevCanFilter = CanFilter; _prevFilterPredicate = FilterPredicate; _prevFilterText = _filterText; - + _filteredOptionsCache = []; - if(!CanFilter || string.IsNullOrWhiteSpace(_filterText)) + if (!CanFilter || string.IsNullOrWhiteSpace(_filterText)) { _filteredOptionsCache.AddRange(Options); } @@ -181,7 +198,7 @@ private List FilteredOptions() var predicate = FilterPredicate ?? DefaultFilterPredicate; _filteredOptionsCache.AddRange(Options.Where(option => predicate(option, _filterText))); } - + return _filteredOptionsCache; } } \ No newline at end of file