From 287bdba80bff95f3a4650869c98db15ef1b4c2d7 Mon Sep 17 00:00:00 2001 From: Jeffrey Jangli Date: Tue, 3 Nov 2026 02:18:17 +0100 Subject: [PATCH] =?UTF-8?q?Fixes=20#654,=20might=20fix=20#543=20=E2=80=A2?= =?UTF-8?q?=20Fixed=20duplicate=20SelectedValuesChanged=20callbacks=20in?= =?UTF-8?q?=20MudComboBox:=20=20=20=E2=80=A2=20The=20SelectedValues=20s?= =?UTF-8?q?etter=20now=20invokes=20the=20callback=20using=20the=20updated?= =?UTF-8?q?=20backing=20collection.=20=20=20=E2=80=A2=20Removed=20redundan?= =?UTF-8?q?t=20callback=20invocations=20from=20item=20selection,=20chip=20?= =?UTF-8?q?closing,=20and=20select-all=20operations.=20=E2=80=A2=20Added?= =?UTF-8?q?=20null-safe=20handling=20when=20selecting=20individual=20items?= =?UTF-8?q?.=20=E2=80=A2=20Simplified=20select-all=20and=20deselect-all=20?= =?UTF-8?q?behavior=20by=20materializing=20eligible=20and=20selected=20ite?= =?UTF-8?q?m=20collections=20before=20updating=20item=20state.=20=E2=80=A2?= =?UTF-8?q?=20Updated=20select-all=20value=20assignment=20to=20use=20the?= =?UTF-8?q?=20selected=20eligible=20items=20directly.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests: Added regression coverage verifying that SelectedValuesChanged is invoked exactly once for: • Selecting all items. • Deselecting all items. • Selecting a single item. • Closing a selected-value chip. --- .../Components/ComboBox/MudComboBox.razor.cs | 30 +++----- ...SelectChipCloseSelectedValuesChanged.razor | 22 ++++++ ...electAllEnabledSelectedValuesChanged.razor | 24 +++++++ ...SelectSelectOneSelectedValuesChanged.razor | 22 ++++++ .../Components/ComboboxTests.cs | 71 +++++++++++++++++++ 5 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectChipCloseSelectedValuesChanged.razor create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSelectAllEnabledSelectedValuesChanged.razor create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSelectOneSelectedValuesChanged.razor diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/ComboBox/MudComboBox.razor.cs b/src/CodeBeam.MudBlazor.Extensions/Components/ComboBox/MudComboBox.razor.cs index 1d5c8a0e..ff05ce0d 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/ComboBox/MudComboBox.razor.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/ComboBox/MudComboBox.razor.cs @@ -625,7 +625,7 @@ public IEnumerable SelectedValues _selectedValues = new HashSet(set, _comparer); - SelectedValuesChanged.InvokeAsync(new HashSet(SelectedValues, _comparer)).CatchAndLog(); + SelectedValuesChanged.InvokeAsync(_selectedValues).CatchAndLog(); ForceUpdateItems().CatchAndLog(); //Console.WriteLine("SelectedValues setter ended"); } @@ -1259,11 +1259,10 @@ protected internal async Task ToggleOption(MudComboBoxItem? item, bool select DeselectAllItems(); await UpdateComboBoxValueAsync(item.Value, updateText: true, updateSearchString: true); } - else if (SelectedValues?.Contains(item.Value) != true) + else if (SelectedValues is not null && SelectedValues.Contains(item.Value) != true) { await SetValueAndUpdateTextAsync(item.Value, false); SelectedValues = SelectedValues.Append(item.Value); - await SelectedValuesChanged.InvokeAsync(_selectedValues); _allSelected = GetAllSelectedState(); //await Task.Delay(1); @@ -1395,14 +1394,14 @@ protected void UpdateIcon() /// /// /// - protected async Task ChipClosed(MudChip chip) + protected Task ChipClosed(MudChip chip) { if (chip is null || SelectedValues is null) - return; + return Task.CompletedTask; //SelectedValues = SelectedValues.Where(x => Converter.Set(x)?.ToString() != chip.Value?.ToString()); SelectedValues = SelectedValues.Where(x => x?.Equals(chip.Value) == false); - await SelectedValuesChanged.InvokeAsync(SelectedValues); + return Task.CompletedTask; } private bool? _allSelected; @@ -1412,26 +1411,19 @@ protected async Task ChipClosed(MudChip chip) /// protected async Task SelectAllItems() { - if (_allSelected == null || _allSelected == false) + if (_allSelected != true) { - SelectedValues = new List(); - foreach (var item in Items.Where(x => x.Eligible)) - { - item.Selected = true; - SelectedValues = SelectedValues.Append(item.Value); - } - await SelectedValuesChanged.InvokeAsync(SelectedValues); + var eligibleItems = Items.Where(x => x.Eligible).ToList(); + foreach (var item in eligibleItems) item.Selected = true; + SelectedValues = [.. eligibleItems.Select(x => x.Value)]; await SetValueAndUpdateTextAsync(SelectedValues.LastOrDefault(), false); _allSelected = true; } else { - foreach (var item in Items.Where(x => x.Selected)) - { - item.Selected = false; - } + var selectedItems = Items.Where(x => x.Selected).ToList(); + foreach (var item in selectedItems) item.Selected = false; SelectedValues = new HashSet(_comparer); - await SelectedValuesChanged.InvokeAsync(SelectedValues); _allSelected = false; } } diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectChipCloseSelectedValuesChanged.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectChipCloseSelectedValuesChanged.razor new file mode 100644 index 00000000..77fc2de3 --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectChipCloseSelectedValuesChanged.razor @@ -0,0 +1,22 @@ +@namespace MudExtensions.UnitTests.TestComponents + + + + A + B + C + + +@code { + IEnumerable _selectedValues = new HashSet { "A", "B" }; + public int Count { get; private set; } + + Task SelectedValuesChanged(IEnumerable selectedValues) + { + _selectedValues = selectedValues; + Count++; + return Task.CompletedTask; + } +} diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSelectAllEnabledSelectedValuesChanged.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSelectAllEnabledSelectedValuesChanged.razor new file mode 100644 index 00000000..4e7bee46 --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSelectAllEnabledSelectedValuesChanged.razor @@ -0,0 +1,24 @@ +@namespace MudExtensions.UnitTests.TestComponents + + + + @foreach (var character in guids) + { + @character + } + + +@code { + IEnumerable _selectedValues = []; + public int Count { get; private set; } + string[] guids = [.. Enumerable.Range(0, 1000).Select(_ => Guid.NewGuid().ToString())]; + + Task SelectedValuesChanged(IEnumerable selectedValues) + { + _selectedValues = selectedValues; + Count++; + return Task.CompletedTask; + } +} diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSelectOneSelectedValuesChanged.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSelectOneSelectedValuesChanged.razor new file mode 100644 index 00000000..aad26b23 --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSelectOneSelectedValuesChanged.razor @@ -0,0 +1,22 @@ +@namespace MudExtensions.UnitTests.TestComponents + + + + A + B + C + + +@code { + IEnumerable _selectedValues = []; + public int Count { get; private set; } + + Task SelectedValuesChanged(IEnumerable selectedValues) + { + _selectedValues = selectedValues; + Count++; + return Task.CompletedTask; + } +} diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboboxTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboboxTests.cs index f4cece0c..aad6185a 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboboxTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboboxTests.cs @@ -194,6 +194,77 @@ public void MultiSelect_SelectAll() combobox.Instance.SelectedValues.Should().BeEquivalentTo(new HashSet()); } + /// + /// When SelectAll is enabled, selecting all items must invoke SelectedValuesChanged exactly once. + /// + [Test] + public void MultiSelect_SelectAllEnabled_SelectedValuesChangedOnce() + { + var comp = Context.Render(); + var combobox = comp.FindComponent>(); + var menu = comp.Find("div.mud-popover"); + var input = combobox.Find("div.mud-input-control"); + input.Click(); + comp.WaitForAssertion(() => menu.ClassList.Should().Contain("mud-popover-open")); + + comp.FindAll("div.mud-combobox-item")[0].Click(); + + comp.WaitForAssertion(() => comp.Instance.Count.Should().Be(1)); + combobox.Instance.SelectedValues.Should().HaveCount(1000); + } + /// + /// Deselecting all items after selecting all must invoke SelectedValuesChanged exactly once more. + /// + [Test] + public void MultiSelect_SelectAllEnabled_DeselectAll_SelectedValuesChangedOnce() + { + var comp = Context.Render(); + var combobox = comp.FindComponent>(); + var menu = comp.Find("div.mud-popover"); + combobox.Find("div.mud-input-control").Click(); + comp.WaitForAssertion(() => menu.ClassList.Should().Contain("mud-popover-open")); + comp.FindAll("div.mud-combobox-item")[0].Click(); + comp.WaitForAssertion(() => combobox.Instance.SelectedValues.Should().HaveCount(1000)); + + comp.FindAll("div.mud-combobox-item")[0].Click(); + + comp.WaitForAssertion(() => combobox.Instance.SelectedValues.Should().BeEmpty()); + comp.Instance.Count.Should().Be(2); + } + + /// + /// Selecting a single item in multi selection must invoke SelectedValuesChanged exactly once. + /// + [Test] + public void MultiSelect_SelectOneItem_SelectedValuesChangedOnce() + { + var comp = Context.Render(); + var combobox = comp.FindComponent>(); + var menu = comp.Find("div.mud-popover"); + combobox.Find("div.mud-input-control").Click(); + comp.WaitForAssertion(() => menu.ClassList.Should().Contain("mud-popover-open")); + + comp.FindAll("div.mud-combobox-item")[0].Click(); + + comp.WaitForAssertion(() => combobox.Instance.SelectedValues.Should().BeEquivalentTo(new[] { "A" })); + comp.Instance.Count.Should().Be(1); + } + + /// + /// Closing a chip must invoke SelectedValuesChanged exactly once. + /// + [Test] + public void MultiSelect_ChipClosed_SelectedValuesChangedOnce() + { + var comp = Context.Render(); + var combobox = comp.FindComponent>(); + + comp.Find("button.mud-chip-close-button").Click(); + + comp.WaitForAssertion(() => combobox.Instance.SelectedValues.Should().HaveCount(1)); + comp.Instance.Count.Should().Be(1); + } + [Test] public async Task ComboBox_MultiSelectEditable() {