From 9b93e383608b676890439881f604f94af3095b27 Mon Sep 17 00:00:00 2001 From: Guilherme Gordiano Date: Sun, 8 Nov 2026 12:48:31 -0300 Subject: [PATCH 1/6] Add unit test for required chip field validation --- .../ChipField/ChipFieldRequiredTest.razor | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/ChipField/ChipFieldRequiredTest.razor diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/ChipField/ChipFieldRequiredTest.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/ChipField/ChipFieldRequiredTest.razor new file mode 100644 index 00000000..4a8c1b32 --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/ChipField/ChipFieldRequiredTest.razor @@ -0,0 +1,14 @@ +@namespace MudExtensions.UnitTests.TestComponents + + + + + +@code { + public static string __description__ = "A required chip field should be valid when it has chips, even if its text input is empty"; + + public MudForm? Form { get; private set; } + + [Parameter] + public List? Values { get; set; } +} From e60f57842f9a3a22e82b74a7aeb426d2b44f0b87 Mon Sep 17 00:00:00 2001 From: Guilherme Gordiano Date: Mon, 9 Nov 2026 09:30:03 -0300 Subject: [PATCH 2/6] Update script reference for MudExtensions to minified version --- .../wwwroot/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/wwwroot/index.html b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/wwwroot/index.html index 2fb25308..b4be3167 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/wwwroot/index.html +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/wwwroot/index.html @@ -30,7 +30,7 @@ - + From 2020eb57b4819de218d066903c57f48611377645 Mon Sep 17 00:00:00 2001 From: Guilherme Gordiano Date: Mon, 9 Nov 2026 09:31:08 -0300 Subject: [PATCH 3/6] Add validation button to required chip field test --- .../TestComponents/ChipField/ChipFieldRequiredTest.razor | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/ChipField/ChipFieldRequiredTest.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/ChipField/ChipFieldRequiredTest.razor index 4a8c1b32..2e418cb9 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/ChipField/ChipFieldRequiredTest.razor +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/ChipField/ChipFieldRequiredTest.razor @@ -2,6 +2,7 @@ + Validate @code { From 0087b78beb53dbf6eaa563d286af6fba44ef08eb Mon Sep 17 00:00:00 2001 From: Guilherme Gordiano Date: Mon, 9 Nov 2026 09:31:44 -0300 Subject: [PATCH 4/6] Replace MultiSelectTestRequiredValue and DebounceTextFieldTest with ChipFieldRequiredTest in Index.razor --- .../Pages/Index.razor | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/Pages/Index.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/Pages/Index.razor index e41859b7..823aa77d 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/Pages/Index.razor +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/Pages/Index.razor @@ -6,6 +6,4 @@ Welcome to your new app. - - - + From 895b2e18aa2266cb14bbcfdb1637368f82c7793e Mon Sep 17 00:00:00 2001 From: Guilherme Gordiano Date: Mon, 9 Nov 2026 09:56:07 -0300 Subject: [PATCH 5/6] Refactor ChipField to use ChipFieldTextField for improved validation and add required chip validation tests --- .../ChipField/ChipFieldTextField.cs | 24 ++++ .../Components/ChipField/MudChipField.razor | 5 +- .../ChipField/MudChipField.razor.cs | 9 ++ .../Components/ChipFieldTests.cs | 113 ++++++++++++++++++ 4 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/CodeBeam.MudBlazor.Extensions/Components/ChipField/ChipFieldTextField.cs diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/ChipField/ChipFieldTextField.cs b/src/CodeBeam.MudBlazor.Extensions/Components/ChipField/ChipFieldTextField.cs new file mode 100644 index 00000000..18ffb825 --- /dev/null +++ b/src/CodeBeam.MudBlazor.Extensions/Components/ChipField/ChipFieldTextField.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; +using Microsoft.AspNetCore.Components; + +namespace MudExtensions +{ + /// + /// The text field rendered inside . It validates Required against the chips of + /// the chip field instead of the typed text, so it agrees with the chip field's own validation. + /// It is public only because Razor can't render internal components; it isn't meant to be used directly. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class ChipFieldTextField : MudTextFieldExtended + { + /// + /// Returns true when the parent chip field has at least one chip. + /// + [Parameter] + public Func? HasChips { get; set; } + + /// + protected override bool HasValue(T? value) => HasChips?.Invoke() ?? base.HasValue(value); + } +} diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/ChipField/MudChipField.razor b/src/CodeBeam.MudBlazor.Extensions/Components/ChipField/MudChipField.razor index 40adf2fe..f8e84676 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/ChipField/MudChipField.razor +++ b/src/CodeBeam.MudBlazor.Extensions/Components/ChipField/MudChipField.razor @@ -2,8 +2,9 @@ @inherits MudTextFieldExtended @typeparam T - - + diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/ChipField/MudChipField.razor.cs b/src/CodeBeam.MudBlazor.Extensions/Components/ChipField/MudChipField.razor.cs index f2f6ee81..e14d7061 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/ChipField/MudChipField.razor.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/ChipField/MudChipField.razor.cs @@ -21,9 +21,11 @@ public MudChipField() _valuesState = registerScope.RegisterParameter?>(nameof(Values)) .WithParameter(() => Values) .WithEventCallback(() => ValuesChanged); + _hasChips = () => HasValue(default); } private readonly ParameterState?> _valuesState; + private readonly Func _hasChips; /// /// Protected classes. @@ -285,6 +287,13 @@ public async Task Closed(MudChip chip) await _textFieldExtendedReference.FocusAsync(); } + /// + /// The chip field has a value when it has at least one chip, so Required checks the chips instead of the typed text. + /// + /// + /// + protected override bool HasValue(T? value) => _valuesState.Value is { Count: > 0 }; + /// /// Clear the text field. /// diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ChipFieldTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ChipFieldTests.cs index 3a630117..71b45032 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ChipFieldTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ChipFieldTests.cs @@ -1,6 +1,7 @@ using Bunit; using AwesomeAssertions; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; using MudBlazor; using MudBlazor.Extensions; using MudExtensions.UnitTests.TestComponents; @@ -54,5 +55,117 @@ public async Task ChipField_ParentFormReadOnly_ShouldPreventRemoval() await comp.InvokeAsync(() => chipField.Instance.Closed(chips[0].Instance)); chipField.Instance.GetState(x => x.Values).Should().BeEquivalentTo(new List { "first", "second" }); } + + [Test] + public async Task ChipField_Required_WithChips_ShouldBeValid() + { + var comp = Context.Render(opt => + { + opt.Add(a => a.Values, new List { "first" }); + }); + + await comp.InvokeAsync(() => comp.Instance.Form!.Validate()); + + comp.Instance.Form!.IsValid.Should().BeTrue(); + comp.Instance.Form.Errors.Should().BeEmpty(); + comp.FindAll(".mud-input-error").Should().BeEmpty(); + comp.Markup.Should().NotContain("Add at least one chip"); + } + + [Test] + public async Task ChipField_Required_WithoutChips_ShouldBeInvalid() + { + var comp = Context.Render(opt => + { + opt.Add(a => a.Values, new List()); + }); + + await comp.InvokeAsync(() => comp.Instance.Form!.Validate()); + + comp.Instance.Form!.IsValid.Should().BeFalse(); + comp.Instance.Form.Errors.Should().ContainSingle().Which.Should().Be("Add at least one chip"); + comp.Markup.Should().Contain("Add at least one chip"); + } + + [Test] + public async Task ChipField_Required_WithTextButNoChips_ShouldBeInvalid() + { + var comp = Context.Render(opt => + { + opt.Add(a => a.Values, new List()); + }); + + comp.Find("input").Input(new ChangeEventArgs { Value = "not a chip yet" }); + await comp.InvokeAsync(() => comp.Instance.Form!.Validate()); + + comp.Instance.Form!.IsValid.Should().BeFalse(); + } + + [Test] + public async Task ChipField_Required_TypingAfterError_ShouldKeepShowingError() + { + var comp = Context.Render(opt => + { + opt.Add(a => a.Values, new List()); + }); + await comp.InvokeAsync(() => comp.Instance.Form!.Validate()); + comp.Markup.Should().Contain("Add at least one chip"); + + comp.Find("input").Input(new ChangeEventArgs { Value = "not a chip yet" }); + comp.Markup.Should().Contain("Add at least one chip"); + + await comp.InvokeAsync(() => comp.Instance.Form!.Validate()); + + comp.Instance.Form!.IsValid.Should().BeFalse(); + comp.Markup.Should().Contain("Add at least one chip"); + comp.Find("input").GetAttribute("aria-invalid").Should().Be("true"); + } + + [Test] + public async Task ChipField_Required_AddingChipAfterError_ShouldBecomeValid() + { + var comp = Context.Render(opt => + { + opt.Add(a => a.Values, new List()); + }); + await comp.InvokeAsync(() => comp.Instance.Form!.Validate()); + comp.Instance.Form!.IsValid.Should().BeFalse(); + + var chipField = comp.FindComponent>(); + comp.Find("input").Input(new ChangeEventArgs { Value = "first" }); + await comp.InvokeAsync(() => chipField.Instance.HandleBeforeInput(new BeforeInputEventArgs { Data = " ", InputType = "insert" })); + chipField.Instance.GetState(x => x.Values).Should().BeEquivalentTo(new List { "first" }); + + await comp.InvokeAsync(() => comp.Instance.Form!.Validate()); + + comp.Instance.Form!.IsValid.Should().BeTrue(); + comp.FindAll(".mud-input-error").Should().BeEmpty(); + comp.Markup.Should().NotContain("Add at least one chip"); + } + + [Test] + public async Task ChipField_Required_BlurWithChips_ShouldNotShowError() + { + var comp = Context.Render(opt => + { + opt.Add(a => a.Values, new List { "first" }); + }); + + await comp.Find("input").BlurAsync(new FocusEventArgs()); + + comp.FindAll(".mud-input-error").Should().BeEmpty(); + comp.Instance.Form!.Errors.Should().BeEmpty(); + } + + [Test] + public void ChipField_Required_ShouldKeepRequiredMarker() + { + var comp = Context.Render(opt => + { + opt.Add(a => a.Values, new List { "first" }); + }); + + comp.FindAll(".mud-input-control.mud-input-required").Should().NotBeEmpty(); + } } } From 323b24034628a531ed43e9230ce89bc4a92fd72a Mon Sep 17 00:00:00 2001 From: Guilherme Gordiano Date: Mon, 9 Nov 2026 10:16:24 -0300 Subject: [PATCH 6/6] Add tests for required chip validation with null values and text input --- .../Components/ChipFieldTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ChipFieldTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ChipFieldTests.cs index 71b45032..67290302 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ChipFieldTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ChipFieldTests.cs @@ -167,5 +167,32 @@ public void ChipField_Required_ShouldKeepRequiredMarker() comp.FindAll(".mud-input-control.mud-input-required").Should().NotBeEmpty(); } + + [Test] + public async Task ChipField_Required_WithNullValues_ShouldBeInvalid() + { + var comp = Context.Render(); + + await comp.InvokeAsync(() => comp.Instance.Form!.Validate()); + + comp.Instance.Form!.IsValid.Should().BeFalse(); + comp.Markup.Should().Contain("Add at least one chip"); + } + + [Test] + public async Task ChipFieldTextField_WithoutHasChips_ShouldValidateRequiredAgainstText() + { + var comp = Context.Render>(opt => + { + opt.Add(a => a.Required, true); + }); + + await comp.InvokeAsync(() => comp.Instance.ValidateAsync()); + comp.Instance.HasErrors.Should().BeTrue(); + + comp.Find("input").Change("text"); + await comp.InvokeAsync(() => comp.Instance.ValidateAsync()); + comp.Instance.HasErrors.Should().BeFalse(); + } } }