From d3aa6049a1b17b2034d5a367f3260b4d348fdecc Mon Sep 17 00:00:00 2001 From: Jeffrey Jangli Date: Tue, 3 Nov 2026 22:24:48 +0100 Subject: [PATCH] Added AI generated tests for ComboBox and ComboBoxItem --- .../Combobox/ComboBoxFeatureTest.razor | 31 ++ .../Combobox/ComboBoxFilterDisabledTest.razor | 11 + .../Combobox/ComboBoxItemTemplateTest.razor | 16 + .../Combobox/ComboBoxItemTest.razor | 32 ++ .../ComboBoxMultiSelectSearchTest.razor | 17 + .../ComboBoxOpenCloseCallbackTest.razor | 18 + .../Components/ComboBoxItemTests.cs | 457 ++++++++++++++++++ .../Components/ComboboxTests.cs | 296 ++++++++++++ 8 files changed, 878 insertions(+) create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxFeatureTest.razor create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxFilterDisabledTest.razor create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxItemTemplateTest.razor create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxItemTest.razor create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxMultiSelectSearchTest.razor create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/Combobox/ComboBoxOpenCloseCallbackTest.razor create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ComboBoxItemTests.cs 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() {