From dd0176bd77c3a1ac31b2f2121bc68a9795d5c4bb Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 15 Aug 2022 19:24:35 +0200 Subject: [PATCH] NumericField: Resolve nullable issue introduced by #4971 (#5077) --- .../NumericField/NumericFieldTest.razor | 22 ++- .../Components/NumericFieldTests.cs | 157 ++++++++++++------ .../NumericField/MudNumericField.razor.cs | 32 ++-- 3 files changed, 141 insertions(+), 70 deletions(-) diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/NumericField/NumericFieldTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/NumericField/NumericFieldTest.razor index ca7a1ddae27d..d297728dba7d 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/NumericField/NumericFieldTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/NumericField/NumericFieldTest.razor @@ -39,7 +39,7 @@ Variant="Variant.Filled" Margin="Margin.Dense" Max="9.9" - Min="-5.0" + Min="-5.0" Immediate="true"/>

- +

+ Label="decimal?" + Variant="Variant.Outlined" + Margin="Margin.Dense" /> +
+ +
+ @code { public static string __description__ = "The textfield should be changed by up/down arrows"; diff --git a/src/MudBlazor.UnitTests/Components/NumericFieldTests.cs b/src/MudBlazor.UnitTests/Components/NumericFieldTests.cs index cb749308e4e8..2f8976976a25 100644 --- a/src/MudBlazor.UnitTests/Components/NumericFieldTests.cs +++ b/src/MudBlazor.UnitTests/Components/NumericFieldTests.cs @@ -25,7 +25,7 @@ namespace MudBlazor.UnitTests.Components public class NumericFieldTests : BunitTest { // TestCaseSource does not know about "Nullable" so having values as Nullable does not make sense here - static object[] TypeCases = + static object[] TypeCases = { new object[] { (byte)5 }, new object[] { (sbyte)5 }, @@ -50,7 +50,7 @@ public void NumericFieldLabelFor() var label = comp.FindAll(".mud-input-label"); label[0].Attributes.GetNamedItem("for")?.Value.Should().Be("numericFieldLabelTest"); } - + /// /// Initial Text for double should be 0, with F1 format it should be 0.0 /// @@ -93,10 +93,10 @@ public void NumericFieldTest2() /// /// Setting the value to null should not cause a validation error /// - [Test] - public async Task IntNumericFieldWithNullableTypes() + [TestCaseSource(nameof(TypeCases))] + public async Task NumericField_WithNullableTypes_ShouldAllowNulls(T value) where T : struct { - var comp = Context.RenderComponent>(ComponentParameter.CreateParameter("Value", 17)); + var comp = Context.RenderComponent>(ComponentParameter.CreateParameter("Value", value)); // print the generated html //Console.WriteLine(comp.Markup); comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null)); @@ -106,51 +106,6 @@ public async Task IntNumericFieldWithNullableTypes() comp.Find("input").Blur(); comp.FindAll("div.mud-input-error").Count.Should().Be(0); } - - /// - /// Setting the value to null should not cause a validation error - /// - [Test] - public async Task DecimalNumericFieldWithNullableTypes() - { - var comp = Context.RenderComponent>(ComponentParameter.CreateParameter("Value", 17M)); - comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null)); - comp.Find("input").Blur(); - comp.FindAll("div.mud-input-error").Count.Should().Be(0); - comp.Find("input").Change(""); - comp.Find("input").Blur(); - comp.FindAll("div.mud-input-error").Count.Should().Be(0); - } - - /// - /// Setting the value to null should not cause a validation error - /// - [Test] - public async Task Int64NumericFieldWithNullableTypes() - { - var comp = Context.RenderComponent>(ComponentParameter.CreateParameter("Value", 17L)); - comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null)); - comp.Find("input").Blur(); - comp.FindAll("div.mud-input-error").Count.Should().Be(0); - comp.Find("input").Change(""); - comp.Find("input").Blur(); - comp.FindAll("div.mud-input-error").Count.Should().Be(0); - } - - /// - /// Setting the value to null should not cause a validation error - /// - [Test] - public async Task UInt64NumericFieldWithNullableTypes() - { - var comp = Context.RenderComponent>(ComponentParameter.CreateParameter("Value", 17UL)); - comp.SetParametersAndRender(ComponentParameter.CreateParameter("Value", null)); - comp.Find("input").Blur(); - comp.FindAll("div.mud-input-error").Count.Should().Be(0); - comp.Find("input").Change(""); - comp.Find("input").Blur(); - comp.FindAll("div.mud-input-error").Count.Should().Be(0); - } //This doesn't make any sense because you cannot set anything that's not a number ///// @@ -280,7 +235,7 @@ public async Task NumericFieldFluentValidationTest1() //Console.WriteLine("Error message: " + numericField.ErrorText); numericField.ErrorText.Should().BeNullOrEmpty(); } - + /// /// Validate handling of decimal support & precision kept /// @@ -431,7 +386,7 @@ public async Task NumericFieldTest_KeyboardInput() comp.Find("input").KeyUp(new KeyboardEventArgs() { Key = "9", Type = "keyup", }); comp.WaitForAssertion(() => numericField.Value.Should().Be(1234.56)); } - + /// /// Keydown disabled, should not do anything /// @@ -450,7 +405,7 @@ public async Task NumericFieldTest_KeyboardInput_Disabled() comp.Find("input").KeyUp(new KeyboardEventArgs() { Key = "9", Type = "keyup", }); comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(1234.56)); } - + /// /// Keydown readonly, should not do anything /// @@ -676,6 +631,46 @@ public async Task NumericField_Validation(T value) numericField.Value.Should().Be(value); } + [TestCaseSource(nameof(TypeCases))] + public async Task NumericFieldMinMax(T value) + { + var min = (T)Convert.ChangeType(1, typeof(T)); + var max = (T)Convert.ChangeType(10, typeof(T)); + var comp = Context.RenderComponent>(); + comp.SetParam(x => x.Min, min); + comp.SetParam(x => x.Max, max); + + comp.Find("input").Change("15"); + comp.Find("input").Blur(); + + comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(max)); + + comp.Find("input").Change("0"); + comp.Find("input").Blur(); + + comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(min)); + } + + [TestCaseSource(nameof(TypeCases))] + public async Task NumericFieldMinMaxNullable(T value) where T : struct + { + var min = (T)Convert.ChangeType(1, typeof(T)); + var max = (T)Convert.ChangeType(10, typeof(T)); + var comp = Context.RenderComponent>(); + comp.SetParam(x => x.Min, min); + comp.SetParam(x => x.Max, max); + + comp.Find("input").Change("15"); + comp.Find("input").Blur(); + + comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(max)); + + comp.Find("input").Change("0"); + comp.Find("input").Blur(); + + comp.WaitForAssertion(() => comp.Instance.Value.Should().Be(min)); + } + [TestCaseSource(nameof(TypeCases))] public async Task NumericField_Increment_Decrement(T value) { @@ -697,6 +692,27 @@ public async Task NumericField_Increment_Decrement(T value) comp.Instance.Value.Should().Be(value); } + [TestCaseSource(nameof(TypeCases))] + public async Task NumericFieldNullable_Increment_Decrement(T value) where T : struct + { + var comp = Context.RenderComponent>(); + var max = Convert.ChangeType(10, typeof(T)); + var min = Convert.ChangeType(0, typeof(T)); + comp.SetParam(x => x.Max, max); + comp.SetParam(x => x.Min, min); + comp.SetParam(x => x.Step, value); + comp.SetParam(x => x.Value, value); + await comp.InvokeAsync(() => comp.Instance.Increment().Wait()); + await comp.InvokeAsync(() => comp.Instance.Decrement().Wait()); + comp.Instance.Value.Should().Be(value); + // setting min and max to value will cover the boundary checking code + comp.SetParam(x => x.Max, value); + comp.SetParam(x => x.Min, value); + await comp.InvokeAsync(() => comp.Instance.Increment().Wait()); + await comp.InvokeAsync(() => comp.Instance.Decrement().Wait()); + comp.Instance.Value.Should().Be(value); + } + [TestCaseSource(nameof(TypeCases))] public async Task NumericField_Increment_Decrement_OverflowHandled(T value) { @@ -714,6 +730,41 @@ public async Task NumericField_Increment_Decrement_OverflowHandled(T value) comp.Instance.Value.Should().Be(comp.Instance.Min); } + [TestCaseSource(nameof(TypeCases))] + public async Task NumericFieldNullable_Increment_Decrement_OverflowHandled(T value) where T : struct + { + var comp = Context.RenderComponent>(); + comp.SetParam(x => x.Step, value); + + // test max overflow + comp.SetParam(x => x.Value, comp.Instance.Max); + await comp.InvokeAsync(() => comp.Instance.Increment().Wait()); + comp.Instance.Value.Should().Be(comp.Instance.Max); + + // test min overflow + comp.SetParam(x => x.Value, comp.Instance.Min); + await comp.InvokeAsync(() => comp.Instance.Decrement().Wait()); + comp.Instance.Value.Should().Be(comp.Instance.Min); + } + + /// + /// NumericField with min/max set and nullable int can be cleared + /// + [TestCase(10, 20, 15)] + [TestCase(-20, -10, -15)] + public async Task NumericFieldCanBeCleared(int min, int max, int value) + { + var comp = Context.RenderComponent>(); + comp.SetParam(x => x.Min, min); + comp.SetParam(x => x.Max, max); + comp.SetParam(x => x.Value, value); + + comp.Find("input").Change(""); + comp.Find("input").Blur(); + + comp.WaitForAssertion(() => comp.Instance.Value.Should().BeNull()); + } + /// /// Special format with currency format should not result in error /// diff --git a/src/MudBlazor/Components/NumericField/MudNumericField.razor.cs b/src/MudBlazor/Components/NumericField/MudNumericField.razor.cs index a763c609ea82..80c4fd543da5 100644 --- a/src/MudBlazor/Components/NumericField/MudNumericField.razor.cs +++ b/src/MudBlazor/Components/NumericField/MudNumericField.razor.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.RegularExpressions; @@ -17,6 +18,7 @@ namespace MudBlazor public partial class MudNumericField : MudDebouncedInput { private IKeyInterceptor _keyInterceptor; + private Comparer _comparer = new(CultureInfo.InvariantCulture); public MudNumericField() : base() { @@ -180,14 +182,16 @@ private async Task Change(double factor = 1) try { var nextValue = GetNextValue(factor); - if (nextValue is IComparable comparable) + + // validate that the data type is a value type before we compare them + if (typeof(T).IsValueType) { - if (factor > 0 && comparable.CompareTo(Value) < 0) + if (factor > 0 && _comparer.Compare(nextValue, Value) < 0) nextValue = Max; - else if (factor < 0 && comparable.CompareTo(Value) > 0) + else if (factor < 0 && (_comparer.Compare(nextValue, Value) > 0 || nextValue is null)) nextValue = Min; } - + await SetValueAsync(ConstrainBoundaries(nextValue).value); _elementReference.SetText(Text).AndForget(); } @@ -226,16 +230,20 @@ private T GetNextValue(double factor) /// Returns a valid value and if it has been changed. protected (T value, bool changed) ConstrainBoundaries(T value) { - // check if Max/Min has value, if not use MaxValue/MinValue for that data type - if (value is IComparable comparable) + if (value == null) + return (default(T), false); + + // validate that the data type is a value type before we compare them + if (typeof(T).IsValueType) { - if (comparable.CompareTo(Max) > 0) + // check if value is bigger than defined MAX, if so take the defined MAX value instead + if (_comparer.Compare(value, Max) > 0) return (Max, true); - else if (comparable.CompareTo(Min) < 0) + + // check if value is lower than defined MIN, if so take the defined MIN value instead + if (_comparer.Compare(value, Min) < 0) return (Min, true); - } - else if (value == null) - return (default(T), true); + }; return (value, false); } @@ -419,7 +427,7 @@ private long FromInt64(T v) => Convert.ToInt64((long?)(object)v); private ulong FromUInt64(T v) => Convert.ToUInt64((ulong?)(object)v); - + protected override void Dispose(bool disposing) { base.Dispose(disposing);