diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxFeatureTest.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxFeatureTest.razor
new file mode 100644
index 00000000..bb0e69a4
--- /dev/null
+++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxFeatureTest.razor
@@ -0,0 +1,31 @@
+@namespace MudExtensions.UnitTests.TestComponents
+
+
+
+
+
+
+
+
+@code {
+ string? value = null;
+
+ [Parameter] public bool Highlight { get; set; }
+
+ [Parameter] public string? HighlightClass { get; set; }
+
+ [Parameter] public bool Required { get; set; }
+
+ [Parameter] public string? Label { get; set; }
+
+ [Parameter] public string? Placeholder { get; set; }
+
+ [Parameter] public string? HelperText { get; set; }
+
+ [Parameter] public bool Bordered { get; set; }
+
+ [Parameter] public string? ComboClass { get; set; }
+
+ [Parameter] public bool Disabled { get; set; }
+}
diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxFilterDisabledTest.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxFilterDisabledTest.razor
new file mode 100644
index 00000000..f13286a0
--- /dev/null
+++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxFilterDisabledTest.razor
@@ -0,0 +1,11 @@
+@namespace MudExtensions.UnitTests.TestComponents
+
+
+
+ apple
+ banana
+
+
+@code {
+ string? value = null;
+}
diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxItemTemplateTest.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxItemTemplateTest.razor
new file mode 100644
index 00000000..c4f263ac
--- /dev/null
+++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxItemTemplateTest.razor
@@ -0,0 +1,16 @@
+@namespace MudExtensions.UnitTests.TestComponents
+
+
+
+
+ @item.Value
+
+
+ apple content
+ banana content
+
+
+
+@code {
+ string? value = null;
+}
diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxItemTest.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxItemTest.razor
new file mode 100644
index 00000000..318810dc
--- /dev/null
+++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxItemTest.razor
@@ -0,0 +1,32 @@
+@namespace MudExtensions.UnitTests.TestComponents
+
+
+
+
+
+
+
+ date content
+
+
+
+@code {
+ string? value = null;
+
+ public int ClickCount { get; set; }
+
+ [Parameter] public bool Editable { get; set; }
+
+ [Parameter] public bool MultiSelection { get; set; }
+
+ [Parameter] public bool ShowCheckbox { get; set; } = true;
+
+ [Parameter] public bool Bordered { get; set; }
+
+ [Parameter] public bool Highlight { get; set; }
+
+ [Parameter] public string? HighlightClass { get; set; }
+
+ [Parameter] public Func? SearchFunc { get; set; }
+}
diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSearchTest.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSearchTest.razor
new file mode 100644
index 00000000..59d19399
--- /dev/null
+++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSearchTest.razor
@@ -0,0 +1,17 @@
+@namespace MudExtensions.UnitTests.TestComponents
+
+
+
+ Nothing here
+
+ apple
+ banana
+
+
+
+@code {
+ IEnumerable values = new HashSet();
+
+ [Parameter]
+ public bool EnableFilter { get; set; } = true;
+}
diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxOpenCloseCallbackTest.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxOpenCloseCallbackTest.razor
new file mode 100644
index 00000000..529360a1
--- /dev/null
+++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxOpenCloseCallbackTest.razor
@@ -0,0 +1,18 @@
+@namespace MudExtensions.UnitTests.TestComponents
+
+
+
+ Nothing here
+
+ apple
+ banana
+
+
+
+@code {
+ string? value = null;
+
+ public int OpenCount { get; set; }
+
+ public int CloseCount { get; set; }
+}
diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboBoxItemTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboBoxItemTests.cs
new file mode 100644
index 00000000..8fff3bb0
--- /dev/null
+++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboBoxItemTests.cs
@@ -0,0 +1,457 @@
+#pragma warning disable CS1998 // async without await
+#pragma warning disable IDE1006 // leading underscore
+#pragma warning disable BL0005 // Set parameter outside component
+
+using Bunit;
+using MudExtensions.UnitTests.TestComponents;
+using AwesomeAssertions;
+using MudBlazor;
+using MudExtensions.UnitTests.Extensions;
+
+namespace MudExtensions.UnitTests.Components
+{
+ [TestFixture]
+ public class ComboBoxItemTests : BunitTest
+ {
+ private static void OpenMenu(IRenderedComponent comp)
+ {
+ comp.FindComponent>().Find("div.mud-input-control").Click();
+ comp.WaitForAssertion(() => comp.FindAll("div.mud-combobox-item").Count.Should().Be(4));
+ }
+
+ private static async Task FilterAsync(IRenderedComponent comp, string search)
+ {
+ var combobox = comp.FindComponent>();
+ await comp.InvokeAsync(() => combobox.Instance.HandleInternalValueChanged(search));
+ await comp.InvokeAsync(() => combobox.Instance.ForceRenderItems());
+ }
+
+ /// Every MudComboBoxItem registers itself with its parent combobox.
+ [Test]
+ public void ComboBoxItem_ShouldRegisterWithParent()
+ {
+ var comp = Context.Render();
+
+ comp.FindComponent>().Instance.Items.Count.Should().Be(4);
+ }
+
+ /// The item Text is displayed when provided.
+ [Test]
+ public void ComboBoxItem_ShouldDisplayText()
+ {
+ var comp = Context.Render();
+
+ OpenMenu(comp);
+
+ comp.Find("div.item-apple").TextContent.Should().Contain("Apple");
+ }
+
+ /// The item Value is displayed when no Text is provided.
+ [Test]
+ public void ComboBoxItem_ShouldDisplayValueWhenTextIsMissing()
+ {
+ var comp = Context.Render();
+
+ OpenMenu(comp);
+
+ comp.Find("div.item-banana").TextContent.Should().Contain("banana");
+ }
+
+ /// Clicking an item invokes its OnClick callback.
+ [Test]
+ public void ComboBoxItem_ClickShouldInvokeOnClick()
+ {
+ var comp = Context.Render();
+ OpenMenu(comp);
+
+ comp.Find("div.item-apple").Click();
+
+ comp.WaitForAssertion(() => comp.Instance.ClickCount.Should().Be(1));
+ }
+
+ /// Clicking an item sets the parent combobox value.
+ [Test]
+ public void ComboBoxItem_ClickShouldSetParentValue()
+ {
+ var comp = Context.Render();
+ OpenMenu(comp);
+
+ comp.Find("div.item-banana").Click();
+
+ comp.WaitForAssertion(() => comp.FindComponent>().Instance.Value.Should().Be("banana"));
+ }
+
+ /// The Disabled
+ [Test]
+ public void ComboBoxItem_DisabledShouldHaveDisabledClass()
+ {
+ var comp = Context.Render();
+
+ OpenMenu(comp);
+
+ comp.Find("div.item-cherry").ClassList.Should().Contain("mud-combobox-item-disabled");
+ }
+
+ /// A disabled item has no ripple class.
+ [Test]
+ public void ComboBoxItem_DisabledShouldNotHaveRippleClass()
+ {
+ var comp = Context.Render();
+
+ OpenMenu(comp);
+
+ comp.Find("div.item-cherry").ClassList.Should().NotContain("mud-ripple");
+ }
+
+ /// An enabled item has the ripple class by default.
+ [Test]
+ public void ComboBoxItem_ShouldHaveRippleClassByDefault()
+ {
+ var comp = Context.Render();
+
+ OpenMenu(comp);
+
+ comp.Find("div.item-apple").ClassList.Should().Contain("mud-ripple");
+ }
+
+ /// Setting Ripple to false removes the ripple class.
+ [Test]
+ public void ComboBoxItem_RippleFalseShouldNotHaveRippleClass()
+ {
+ var comp = Context.Render();
+
+ OpenMenu(comp);
+
+ comp.Find("div.item-banana").ClassList.Should().NotContain("mud-ripple");
+ }
+
+ /// A user supplied Class is merged into the item classes.
+ [Test]
+ public void ComboBoxItem_ShouldApplyUserClass()
+ {
+ var comp = Context.Render();
+
+ OpenMenu(comp);
+
+ comp.Find("div.item-apple").ClassList.Should().Contain("mud-combobox-item");
+ }
+
+ /// The selected item gets the selected CSS class.
+ [Test]
+ public void ComboBoxItem_SelectedShouldHaveSelectedClass()
+ {
+ var comp = Context.Render();
+ OpenMenu(comp);
+ comp.Find("div.item-banana").Click();
+ comp.WaitForAssertion(() => comp.FindComponent>().Instance.Value.Should().Be("banana"));
+
+ comp.FindComponent>().Find("div.mud-input-control").Click();
+
+ comp.WaitForAssertion(() => comp.Find("div.item-banana").ClassList.Should().Contain("mud-selected-item"));
+ }
+
+ /// An unselected item does not get the selected CSS class.
+ [Test]
+ public void ComboBoxItem_UnselectedShouldNotHaveSelectedClass()
+ {
+ var comp = Context.Render();
+
+ OpenMenu(comp);
+
+ comp.Find("div.item-apple").ClassList.Should().NotContain("mud-selected-item");
+ }
+
+ /// SetActive(true) adds the highlight class.
+ [Test]
+ public async Task ComboBoxItem_SetActiveShouldAddHighlightClass()
+ {
+ var comp = Context.Render();
+ OpenMenu(comp);
+ var item = comp.FindComponents>().First(x => x.Instance.Value == "apple");
+
+ await comp.InvokeAsync(() => item.Instance.SetActive(true));
+ await comp.InvokeAsync(() => item.Instance.ForceUpdate());
+
+ comp.WaitForAssertion(() => comp.Find("div.item-apple").ClassList.Should().Contain("mud-combobox-item-hilight"));
+ }
+
+ /// SetActive(false) removes the highlight class.
+ [Test]
+ public async Task ComboBoxItem_SetInactiveShouldRemoveHighlightClass()
+ {
+ var comp = Context.Render();
+ OpenMenu(comp);
+ var item = comp.FindComponents>().First(x => x.Instance.Value == "apple");
+ await comp.InvokeAsync(() => item.Instance.SetActive(true));
+ await comp.InvokeAsync(() => item.Instance.ForceUpdate());
+
+ await comp.InvokeAsync(() => item.Instance.SetActive(false));
+ await comp.InvokeAsync(() => item.Instance.ForceUpdate());
+
+ comp.WaitForAssertion(() => comp.Find("div.item-apple").ClassList.Should().NotContain("mud-combobox-item-hilight"));
+ }
+
+ /// A disabled item does not get the highlight class when active.
+ [Test]
+ public async Task ComboBoxItem_DisabledActiveShouldNotHaveHighlightClass()
+ {
+ var comp = Context.Render();
+ OpenMenu(comp);
+ var item = comp.FindComponents>().First(x => x.Instance.Value == "cherry");
+
+ await comp.InvokeAsync(() => item.Instance.SetActive(true));
+ await comp.InvokeAsync(() => item.Instance.ForceUpdate());
+
+ comp.Find("div.item-cherry").ClassList.Should().NotContain("mud-combobox-item-hilight");
+ }
+
+ /// With Bordered enabled, an active item gets the bordered class.
+ [Test]
+ public async Task ComboBoxItem_BorderedActiveShouldHaveBorderedClass()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Bordered, true));
+ OpenMenu(comp);
+ var item = comp.FindComponents>().First(x => x.Instance.Value == "apple");
+
+ await comp.InvokeAsync(() => item.Instance.SetActive(true));
+ await comp.InvokeAsync(() => item.Instance.ForceUpdate());
+
+ comp.WaitForAssertion(() => comp.Find("div.item-apple").ClassList.Should().Contain("mud-combobox-item-bordered"));
+ }
+
+ /// Without Bordered, an active item has no bordered class.
+ [Test]
+ public async Task ComboBoxItem_NotBorderedActiveShouldNotHaveBorderedClass()
+ {
+ var comp = Context.Render();
+ OpenMenu(comp);
+ var item = comp.FindComponents>().First(x => x.Instance.Value == "apple");
+
+ await comp.InvokeAsync(() => item.Instance.SetActive(true));
+ await comp.InvokeAsync(() => item.Instance.ForceUpdate());
+
+ comp.Find("div.item-apple").ClassList.Should().NotContain("mud-combobox-item-bordered");
+ }
+
+ /// The item is eligible by default when the combobox is not editable.
+ [Test]
+ public async Task ComboBoxItem_ShouldBeEligibleWhenNotEditable()
+ {
+ var comp = Context.Render();
+ OpenMenu(comp);
+
+ await FilterAsync(comp, "zzz");
+
+ comp.FindComponents>().All(x => x.Instance.Eligible).Should().BeTrue();
+ }
+
+ /// When editable, items not matching the search text are not eligible.
+ [Test]
+ public async Task ComboBoxItem_ShouldNotBeEligibleWhenSearchDoesNotMatch()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Editable, true));
+ OpenMenu(comp);
+
+ await FilterAsync(comp, "zzz");
+
+ comp.FindComponents>().Any(x => x.Instance.Eligible).Should().BeFalse();
+ }
+
+ /// When editable, only items whose Text contains the search text are eligible.
+ [Test]
+ public async Task ComboBoxItem_ShouldMatchSearchAgainstText()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Editable, true));
+ OpenMenu(comp);
+
+ await FilterAsync(comp, "app");
+
+ comp.WaitForAssertion(() => comp.FindComponents>().Where(x => x.Instance.Eligible).Select(x => x.Instance.Value)
+ .Should().BeEquivalentTo(new[] { "apple" }));
+ }
+
+ /// The search is case insensitive.
+ [Test]
+ public async Task ComboBoxItem_SearchShouldBeCaseInsensitive()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Editable, true));
+ OpenMenu(comp);
+
+ await FilterAsync(comp, "APP");
+
+ comp.WaitForAssertion(() => comp.FindComponents>().Where(x => x.Instance.Eligible).Select(x => x.Instance.Value)
+ .Should().BeEquivalentTo(new[] { "apple" }));
+ }
+
+ /// An item without Text is matched by its converted Value.
+ [Test]
+ public async Task ComboBoxItem_ShouldMatchSearchAgainstValueWhenTextIsMissing()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Editable, true));
+ OpenMenu(comp);
+
+ await FilterAsync(comp, "nan");
+
+ comp.WaitForAssertion(() => comp.FindComponents>().Where(x => x.Instance.Eligible).Select(x => x.Instance.Value)
+ .Should().BeEquivalentTo(new[] { "banana" }));
+ }
+
+ /// A not eligible item gets the d-none class.
+ [Test]
+ public async Task ComboBoxItem_NotEligibleShouldHaveDNoneClass()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Editable, true));
+ OpenMenu(comp);
+
+ await FilterAsync(comp, "app");
+
+ comp.WaitForAssertion(() => comp.Find("div.item-banana").ClassList.Should().Contain("d-none"));
+ }
+
+ /// An eligible item has no d-none class.
+ [Test]
+ public async Task ComboBoxItem_EligibleShouldNotHaveDNoneClass()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Editable, true));
+ OpenMenu(comp);
+
+ await FilterAsync(comp, "app");
+
+ comp.WaitForAssertion(() => comp.Find("div.item-apple").ClassList.Should().NotContain("d-none"));
+ }
+
+ /// A custom SearchFunc overrides the default matching.
+ [Test]
+ public async Task ComboBoxItem_SearchFuncShouldOverrideDefaultMatching()
+ {
+ Func searchFunc = (value, text, search) => value == "date";
+ var comp = Context.Render(p =>
+ {
+ p.Add(x => x.Editable, true);
+ p.Add(x => x.SearchFunc, searchFunc);
+ });
+ OpenMenu(comp);
+
+ await FilterAsync(comp, "anything");
+
+ comp.WaitForAssertion(() => comp.FindComponents>().Where(x => x.Instance.Eligible).Select(x => x.Instance.Value)
+ .Should().BeEquivalentTo(new[] { "date" }));
+ }
+
+ /// An empty search text keeps every item eligible.
+ [Test]
+ public async Task ComboBoxItem_EmptySearchShouldKeepAllItemsEligible()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Editable, true));
+ OpenMenu(comp);
+ await FilterAsync(comp, "app");
+
+ await FilterAsync(comp, "");
+
+ comp.WaitForAssertion(() => comp.FindComponents>().All(x => x.Instance.Eligible).Should().BeTrue());
+ }
+
+ /// A checkbox is rendered for each item in multi selection mode.
+ [Test]
+ public void ComboBoxItem_MultiSelectionShouldRenderCheckbox()
+ {
+ var comp = Context.Render(p => p.Add(x => x.MultiSelection, true));
+
+ OpenMenu(comp);
+
+ comp.FindAll("div.mud-combobox-item").All(x => x.QuerySelector(".mud-checkbox") is not null).Should().BeTrue();
+ }
+
+ /// No checkbox is rendered in single selection mode.
+ [Test]
+ public void ComboBoxItem_SingleSelectionShouldNotRenderCheckbox()
+ {
+ var comp = Context.Render();
+
+ OpenMenu(comp);
+
+ comp.FindAll("div.mud-combobox-item").Any(x => x.QuerySelector(".mud-checkbox") is not null).Should().BeFalse();
+ }
+
+ /// No checkbox is rendered in multi selection mode when ShowCheckbox is false.
+ [Test]
+ public void ComboBoxItem_ShowCheckboxFalseShouldNotRenderCheckbox()
+ {
+ var comp = Context.Render(p =>
+ {
+ p.Add(x => x.MultiSelection, true);
+ p.Add(x => x.ShowCheckbox, false);
+ });
+
+ OpenMenu(comp);
+
+ comp.FindAll("div.mud-combobox-item").Any(x => x.QuerySelector(".mud-checkbox") is not null).Should().BeFalse();
+ }
+
+ /// In multi selection, clicking several items selects all of them.
+ [Test]
+ public void ComboBoxItem_MultiSelectionClickShouldSelectMultipleItems()
+ {
+ var comp = Context.Render(p => p.Add(x => x.MultiSelection, true));
+ OpenMenu(comp);
+
+ comp.Find("div.item-apple").Click();
+ comp.Find("div.item-banana").Click();
+
+ comp.WaitForAssertion(() => comp.FindComponent>().Instance.SelectedValues.Should().BeEquivalentTo(new[] { "apple", "banana" }));
+ }
+
+ /// In multi selection, clicking a selected item deselects it.
+ [Test]
+ public void ComboBoxItem_MultiSelectionClickShouldToggleItem()
+ {
+ var comp = Context.Render(p => p.Add(x => x.MultiSelection, true));
+ OpenMenu(comp);
+ comp.Find("div.item-apple").Click();
+ comp.WaitForAssertion(() => comp.FindComponent>().Instance.SelectedValues.Should().Contain("apple"));
+
+ comp.Find("div.item-apple").Click();
+
+ comp.WaitForAssertion(() => comp.FindComponent>().Instance.SelectedValues.Should().NotContain("apple"));
+ }
+
+ /// Child content is rendered inside the item when the item presenter allows it.
+ [Test]
+ public void ComboBoxItem_ChildContentShouldBeRendered()
+ {
+ var comp = Context.Render();
+
+ OpenMenu(comp);
+
+ comp.Find("div.item-date").TextContent.Should().Contain("date");
+ }
+
+ /// Disposing an item removes it from the parent combobox.
+ [Test]
+ public async Task ComboBoxItem_DisposeShouldRemoveItemFromParent()
+ {
+ var comp = Context.Render();
+ var combobox = comp.FindComponent>();
+ var item = comp.FindComponents>().First(x => x.Instance.Value == "apple");
+
+ await comp.InvokeAsync(() => item.Instance.Dispose());
+
+ combobox.Instance.Items.Should().NotContain(item.Instance);
+ }
+
+ /// ForceRender re-evaluates eligibility against the current search text.
+ [Test]
+ public async Task ComboBoxItem_ForceRenderShouldReevaluateEligibility()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Editable, true));
+ OpenMenu(comp);
+ var combobox = comp.FindComponent>();
+ var item = comp.FindComponents>().First(x => x.Instance.Value == "banana");
+ await comp.InvokeAsync(() => combobox.Instance.HandleInternalValueChanged("app"));
+
+ await comp.InvokeAsync(() => item.Instance.ForceRender());
+
+ item.Instance.Eligible.Should().BeFalse();
+ }
+ }
+}
diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboboxTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboboxTests.cs
index dc701223..75bdfb8f 100644
--- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboboxTests.cs
+++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboboxTests.cs
@@ -19,6 +19,7 @@ public class ComboBoxTests : BunitTest
// Note: MudSelect doesn't guaranteed the consequences of changing Value if MultiSelection is true for now.
// When this feature will add, just uncomment the testcase to test it. No need to write new test.
+ /// The initial Value is reflected in the combobox value and text.
[Test]
[TestCase(false)]
//[TestCase(true)]
@@ -38,6 +39,7 @@ public void ComboBox_InitialValueTest(bool multiSelection)
// Note: MudSelect doesn't guaranteed the consequences of changing SelectedValues if MultiSelection is false for now.
// When this feature will add, just uncomment the testcase to test it. No need to write new test.
+ /// The initial SelectedValues are reflected in the combobox value, selection and text.
[Test]
//[TestCase(false)]
[TestCase(true)]
@@ -55,6 +57,7 @@ public void ComboBox_InitialValuesTest(bool multiSelection)
combobox.Instance.GetState(x => x.Text).Should().Be("1");
}
+ /// In multi selection, changing SelectedValues updates the selection but not Value or Text.
[Test]
public void ComboBox_ValueBubblingTest_MultiSelection()
{
@@ -79,6 +82,7 @@ public void ComboBox_ValueBubblingTest_MultiSelection()
}
+ /// In multi selection, SelectedValuesChanged fires only when the selection actually changes.
[Test]
public async Task ComboBox_ValueChangeEventCountTest_MultiSelection()
{
@@ -145,6 +149,7 @@ public async Task ComboBoxTest1()
comp.WaitForAssertion(() => @switch.Instance.GetState(x => x.Value).Should().Be(false));
}
+ /// The clear button appears after a selection and clears the value when clicked.
[Test]
public async Task ComboBoxClearableTest()
{
@@ -171,6 +176,292 @@ public async Task ComboBoxClearableTest()
comp.Instance.ClearButtonClicked.Should().BeTrue();
}
+ /// Clicking the input opens the menu and renders all items.
+ [Test]
+ public void ComboBox_ClickShouldRenderAllItems()
+ {
+ var comp = Context.Render();
+
+ comp.Find("div.mud-input-control").Click();
+
+ comp.WaitForAssertion(() => comp.FindAll("div.mud-combobox-item").Count.Should().Be(3));
+ }
+
+ /// Selecting an item closes the menu in single selection mode.
+ [Test]
+ public void ComboBox_SelectingItemShouldCloseMenu()
+ {
+ var comp = Context.Render();
+ comp.Find("div.mud-input-control").Click();
+ comp.WaitForAssertion(() => comp.Find("div.mud-popover").ClassList.Should().NotContain("d-none"));
+
+ comp.FindAll("div.mud-combobox-item")[0].Click();
+
+ comp.WaitForAssertion(() => comp.Find("div.mud-popover").ClassList.Should().Contain("d-none"));
+ }
+
+ /// Selecting an item sets the combobox text to the item's value.
+ [Test]
+ public void ComboBox_SelectingItemShouldSetText()
+ {
+ var comp = Context.Render();
+ var combobox = comp.FindComponent>();
+ comp.Find("div.mud-input-control").Click();
+ comp.WaitForAssertion(() => comp.FindAll("div.mud-combobox-item").Count.Should().BeGreaterThan(0));
+
+ comp.FindAll("div.mud-combobox-item")[2].Click();
+
+ comp.WaitForAssertion(() => combobox.Instance.GetState(x => x.Text).Should().Be("3"));
+ }
+
+ /// A disabled combobox does not open its menu when clicked.
+ [Test]
+ public void ComboBox_DisabledShouldNotOpenMenu()
+ {
+ var comp = Context.Render();
+ var combobox = comp.FindComponent>();
+ combobox.Render(p => p.Add(x => x.Disabled, true));
+
+ comp.Find("div.mud-input-control").Click();
+
+ comp.Find("div.mud-popover").ClassList.Should().Contain("d-none");
+ }
+
+ /// A read-only combobox does not open its menu when clicked.
+ [Test]
+ public void ComboBox_ReadOnlyShouldNotOpenMenu()
+ {
+ var comp = Context.Render();
+ var combobox = comp.FindComponent>();
+ combobox.Render(p => p.Add(x => x.ReadOnly, true));
+
+ comp.Find("div.mud-input-control").Click();
+
+ comp.Find("div.mud-popover").ClassList.Should().Contain("d-none");
+ }
+
+ /// The OnOpen callback fires once when the menu opens.
+ [Test]
+ public void ComboBox_OnOpenShouldFireWhenOpened()
+ {
+ var comp = Context.Render();
+
+ comp.Find("div.mud-input-control").Click();
+
+ comp.WaitForAssertion(() => comp.Instance.OpenCount.Should().Be(1));
+ }
+
+ /// The OnClose callback fires once when an item is selected and the menu closes.
+ [Test]
+ public void ComboBox_OnCloseShouldFireWhenItemSelected()
+ {
+ var comp = Context.Render();
+ comp.Find("div.mud-input-control").Click();
+ comp.WaitForAssertion(() => comp.FindAll("div.mud-combobox-item").Count.Should().Be(2));
+
+ comp.FindAll("div.mud-combobox-item")[0].Click();
+
+ comp.WaitForAssertion(() => comp.Instance.CloseCount.Should().Be(1));
+ }
+
+ /// NoItemsContent is rendered when the search text matches no item.
+ [Test]
+ public async Task ComboBox_MultiSelectEditable_NoMatchShouldRenderNoItemsContent()
+ {
+ var comp = Context.Render();
+ var combobox = comp.FindComponent>();
+ await comp.InvokeAsync(() => combobox.Find("div.mud-input-control").Click());
+ comp.WaitForAssertion(() => combobox.Instance.GetEnabledAndEligibleItems().Count.Should().Be(2));
+
+ await comp.InvokeAsync(() => combobox.Instance.HandleInternalValueChanged("zzz"));
+ await comp.InvokeAsync(() => combobox.Instance.ForceRenderItems());
+
+ comp.WaitForAssertion(() => comp.FindAll("span.no-items-marker").Count.Should().Be(1));
+ }
+
+ /// Searching in an editable multi-select combobox leaves only matching items eligible.
+ [Test]
+ public async Task ComboBox_MultiSelectEditable_MatchShouldFilterItems()
+ {
+ var comp = Context.Render();
+ var combobox = comp.FindComponent>();
+ await comp.InvokeAsync(() => combobox.Find("div.mud-input-control").Click());
+ comp.WaitForAssertion(() => combobox.Instance.GetEnabledAndEligibleItems().Count.Should().Be(2));
+
+ await comp.InvokeAsync(() => combobox.Instance.HandleInternalValueChanged("app"));
+ await comp.InvokeAsync(() => combobox.Instance.ForceRenderItems());
+
+ comp.WaitForAssertion(() => combobox.Instance.GetEnabledAndEligibleItems().Count.Should().Be(1));
+ }
+
+ /// The Label parameter is rendered in a label element.
+ [Test]
+ public void ComboBox_LabelShouldBeRendered()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Label, "My label"));
+
+ comp.FindAll("label").Should().Contain(x => x.TextContent.Contains("My label"));
+ }
+
+ /// The HelperText parameter is rendered in the markup.
+ [Test]
+ public void ComboBox_HelperTextShouldBeRendered()
+ {
+ var comp = Context.Render(p => p.Add(x => x.HelperText, "Helpful hint"));
+
+ comp.Markup.Should().Contain("Helpful hint");
+ }
+
+ /// A user-supplied CSS class is applied to the combobox.
+ [Test]
+ public void ComboBox_CustomClassShouldBeApplied()
+ {
+ var comp = Context.Render(p => p.Add(x => x.ComboClass, "my-custom-class"));
+
+ comp.FindAll("div.my-custom-class").Count.Should().BeGreaterThan(0);
+ }
+
+ /// Clicking a disabled item does not select it.
+ [Test]
+ [Ignore("Known bug: MudComboBoxItem.HandleOnClick does not check Disabled, so disabled items can be selected.")]
+ public void ComboBox_DisabledItemShouldNotBeSelectable()
+ {
+ var comp = Context.Render();
+ var combobox = comp.FindComponent>();
+ comp.Find("div.mud-input-control").Click();
+ comp.WaitForAssertion(() => comp.FindAll("div.mud-combobox-item").Count.Should().Be(3));
+
+ comp.FindAll("div.mud-combobox-item")[2].Click();
+
+ combobox.Instance.GetState(x => x.Value).Should().BeNullOrEmpty();
+ }
+
+ /// Disabled items are excluded from the enabled and eligible item list.
+ [Test]
+ public void ComboBox_DisabledItemShouldNotBeEnabledAndEligible()
+ {
+ var comp = Context.Render();
+ var combobox = comp.FindComponent>();
+
+ combobox.Instance.GetEnabledAndEligibleItems().Count.Should().Be(2);
+ }
+
+ /// An item's Text is displayed in the list instead of its raw value.
+ [Test]
+ public void ComboBox_ItemTextShouldBeUsedAsDisplayText()
+ {
+ var comp = Context.Render();
+ comp.Find("div.mud-input-control").Click();
+
+ comp.WaitForAssertion(() => comp.FindAll("div.mud-combobox-item")[0].TextContent.Should().Contain("Apple"));
+ }
+
+ /// A required combobox shows the required error when validated without a value.
+ [Test]
+ public async Task ComboBox_RequiredShouldShowErrorWhenValidatedWithoutValue()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Required, true));
+ var combobox = comp.FindComponent>();
+
+ await comp.InvokeAsync(() => combobox.Instance.ValidateAsync());
+
+ comp.WaitForAssertion(() => comp.Markup.Should().Contain("Value is required"));
+ }
+
+ /// A required combobox shows no error when validated with a selected value.
+ [Test]
+ public async Task ComboBox_RequiredShouldNotShowErrorWhenValueSelected()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Required, true));
+ var combobox = comp.FindComponent>();
+ comp.Find("div.mud-input-control").Click();
+ comp.WaitForAssertion(() => comp.FindAll("div.mud-combobox-item").Count.Should().Be(3));
+ comp.FindAll("div.mud-combobox-item")[0].Click();
+
+ await comp.InvokeAsync(() => combobox.Instance.ValidateAsync());
+
+ comp.WaitForAssertion(() => comp.Markup.Should().NotContain("Value is required"));
+ }
+
+ /// With Highlight enabled, the searched text is wrapped in a mark element.
+ [Test]
+ public async Task ComboBox_HighlightShouldMarkSearchedText()
+ {
+ var comp = Context.Render(p => p.Add(x => x.Highlight, true));
+ var combobox = comp.FindComponent>();
+ comp.Find("div.mud-input-control").Click();
+
+ await comp.InvokeAsync(() => combobox.Instance.HandleInternalValueChanged("app"));
+ await comp.InvokeAsync(() => combobox.Instance.ForceRenderItems());
+
+ comp.WaitForAssertion(() => comp.FindAll("mark").Count.Should().BeGreaterThan(0));
+ }
+
+ /// With Highlight disabled, no mark element is rendered.
+ [Test]
+ public void ComboBox_HighlightDisabledShouldNotRenderMark()
+ {
+ var comp = Context.Render();
+ comp.Find("div.mud-input-control").Click();
+
+ comp.WaitForAssertion(() => comp.FindAll("div.mud-combobox-item").Count.Should().Be(3));
+ comp.FindAll("mark").Should().BeEmpty();
+ }
+
+ /// With EnableFilter false, all items stay eligible regardless of the search text.
+ [Test]
+ public async Task ComboBox_EnableFilterFalseShouldKeepAllItemsEligible()
+ {
+ var comp = Context.Render();
+ var combobox = comp.FindComponent>();
+ await comp.InvokeAsync(() => combobox.Find("div.mud-input-control").Click());
+ comp.WaitForAssertion(() => combobox.Instance.GetEnabledAndEligibleItems().Count.Should().Be(2));
+
+ await comp.InvokeAsync(() => combobox.Instance.HandleInternalValueChanged("app"));
+ await comp.InvokeAsync(() => combobox.Instance.ForceRenderItems());
+
+ comp.WaitForAssertion(() => combobox.Instance.GetEnabledAndEligibleItems().Count.Should().Be(2));
+ }
+
+ /// An editable combobox filters items by the search text.
+ [Test]
+ public async Task ComboBox_EditableShouldFilterItemsBySearchText()
+ {
+ var comp = Context.Render(p => p.Add(x => x.EnableFilter, true));
+ var combobox = comp.FindComponent>();
+ await comp.InvokeAsync(() => combobox.Find("div.mud-input-control").Click());
+
+ await comp.InvokeAsync(() => combobox.Instance.HandleInternalValueChanged("ban"));
+ await comp.InvokeAsync(() => combobox.Instance.ForceRenderItems());
+
+ comp.WaitForAssertion(() => combobox.Instance.GetEnabledAndEligibleItems().Select(x => x.Value).Should().BeEquivalentTo(new[] { "banana" }));
+ }
+
+ /// ItemTemplate is used to present the selected value.
+ [Test]
+ public async Task ComboBox_ItemTemplateShouldRenderForSelectedValue()
+ {
+ var comp = Context.Render();
+ var combobox = comp.FindComponent>();
+ comp.Find("div.mud-input-control").Click();
+ comp.WaitForAssertion(() => comp.FindAll("div.mud-combobox-item").Count.Should().Be(2));
+
+ comp.FindAll("div.mud-combobox-item")[0].Click();
+
+ await comp.WaitForAssertionAsync(() => comp.FindAll("span.item-template-marker").Count.Should().BeGreaterThan(0));
+ }
+
+ /// Item child content is rendered for each item in the list.
+ [Test]
+ public void ComboBox_ItemContentShouldRenderInList()
+ {
+ var comp = Context.Render();
+ comp.Find("div.mud-input-control").Click();
+
+ comp.WaitForAssertion(() => comp.FindAll("span.item-content-marker").Count.Should().Be(2));
+ }
+
+ /// The adornment icon switches between drop-down and drop-up when the menu opens.
[Test]
public void ComboBox_OpenCloseIconTest()
{
@@ -183,6 +474,7 @@ public void ComboBox_OpenCloseIconTest()
comp.WaitForAssertion(() => combobox.FindComponents().Should().ContainSingle(x => x.Instance.Icon == Icons.Material.Filled.ArrowDropUp));
}
+ /// Clicking the select-all item selects every item, and clicking it again deselects them all.
[Test]
public void MultiSelect_SelectAll()
{
@@ -277,6 +569,7 @@ public void MultiSelect_ChipClosed_SelectedValuesChangedOnce()
comp.Instance.Count.Should().Be(1);
}
+ /// An editable multi-select combobox lists all four items, then only the two matching "t" after filtering.
[Test]
public async Task ComboBox_MultiSelectEditable()
{
@@ -365,6 +658,7 @@ await comp.InvokeAsync(() =>
//Console.WriteLine(comp.Markup);
}
+ /// Keyboard keys open and close the menu and select items in single selection mode, skipping disabled items.
[Test]
public async Task ComboBoxTest_KeyboardNavigation_SingleSelect()
{
@@ -462,6 +756,7 @@ public async Task ComboBoxTest_KeyboardNavigation_SingleSelect()
comp.WaitForAssertion(() => combobox.GetState(x => x.Value).Should().Be("2"));
}
+ /// With ToggleSelection enabled, pressing Enter on the selected item clears the value.
[Test]
public async Task ComboBoxTest_KeyboardNavigation_ToggleSelect()
{
@@ -484,6 +779,7 @@ public async Task ComboBoxTest_KeyboardNavigation_ToggleSelect()
comp.WaitForAssertion(() => combobox.GetState(x => x.Value).Should().BeNull());
}
+ /// Keyboard keys select, select all and toggle items in multi selection mode, and are ignored when disabled.
[Test]
public async Task ComboBoxTest_KeyboardNavigation_MultiSelect()
{