diff --git a/src/BootstrapBlazor.Shared/Samples/Popovers.razor.cs b/src/BootstrapBlazor.Shared/Samples/Popovers.razor.cs index d9d2ba090..9d4066a9c 100644 --- a/src/BootstrapBlazor.Shared/Samples/Popovers.razor.cs +++ b/src/BootstrapBlazor.Shared/Samples/Popovers.razor.cs @@ -23,34 +23,38 @@ public sealed partial class Popovers /// private static IEnumerable GetAttributes() => new AttributeItem[] { - // TODO: 移动到数据库中 - new AttributeItem() { - Name = "Cotent", - Description = "Popover 弹窗内容", - Type = "string", - ValueList = "", - DefaultValue = "Popover" - }, - new AttributeItem() { - Name = "IsHtml", - Description = "内容中是否包含 Html 代码", - Type = "boolean", - ValueList = "", - DefaultValue = "false" - }, - new AttributeItem() { - Name = "Placement", - Description = "位置", - Type = "Placement", - ValueList = "Auto / Top / Left / Bottom / Right", - DefaultValue = "Auto" - }, - new AttributeItem() { - Name = "Title", - Description = "Popover 弹窗标题", - Type = "string", - ValueList = "", - DefaultValue = "Popover" - }, + // TODO: 移动到数据库中 + new AttributeItem() + { + Name = "Cotent", + Description = "Popover 弹窗内容", + Type = "string", + ValueList = "", + DefaultValue = "Popover" + }, + new AttributeItem() + { + Name = "IsHtml", + Description = "内容中是否包含 Html 代码", + Type = "boolean", + ValueList = "", + DefaultValue = "false" + }, + new AttributeItem() + { + Name = "Placement", + Description = "位置", + Type = "Placement", + ValueList = "Auto / Top / Left / Bottom / Right", + DefaultValue = "Auto" + }, + new AttributeItem() + { + Name = "Title", + Description = "Popover 弹窗标题", + Type = "string", + ValueList = "", + DefaultValue = "Popover" + } }; } diff --git a/src/BootstrapBlazor/Components/InputNumber/BootstrapInputNumber.razor.cs b/src/BootstrapBlazor/Components/InputNumber/BootstrapInputNumber.razor.cs index df1ab9892..107835972 100644 --- a/src/BootstrapBlazor/Components/InputNumber/BootstrapInputNumber.razor.cs +++ b/src/BootstrapBlazor/Components/InputNumber/BootstrapInputNumber.razor.cs @@ -74,7 +74,12 @@ public partial class BootstrapInputNumber [NotNull] private IStringLocalizer>? Localizer { get; set; } - static BootstrapInputNumber() + /// + /// SetParametersAsync 方法 + /// + /// + /// + public override Task SetParametersAsync(ParameterView parameters) { // Unwrap Nullable, because InputBase already deals with the Nullable aspect // of it for us. We will only get asked to parse the T for nonempty inputs. @@ -83,6 +88,8 @@ public partial class BootstrapInputNumber { throw new InvalidOperationException($"The type '{targetType}' is not a supported numeric type."); } + + return base.SetParametersAsync(parameters); } /// @@ -120,17 +127,24 @@ public partial class BootstrapInputNumber ? Formatter.Invoke(value) : (!string.IsNullOrEmpty(FormatString) && value != null ? Utility.Format(value, FormatString) - : value switch - { - null => null, - int @int => BindConverter.FormatValue(@int, CultureInfo.InvariantCulture), - long @long => BindConverter.FormatValue(@long, CultureInfo.InvariantCulture), - short @short => BindConverter.FormatValue(@short, CultureInfo.InvariantCulture), - float @float => BindConverter.FormatValue(@float, CultureInfo.InvariantCulture), - double @double => BindConverter.FormatValue(@double, CultureInfo.InvariantCulture), - decimal @decimal => BindConverter.FormatValue(@decimal, CultureInfo.InvariantCulture), - _ => throw new InvalidOperationException($"Unsupported type {value!.GetType()}"), - }); + : InternalFormat(value)); + + /// + /// + /// + /// + /// + /// + protected virtual string? InternalFormat(TValue value) => value switch + { + int @int => BindConverter.FormatValue(@int, CultureInfo.InvariantCulture), + long @long => BindConverter.FormatValue(@long, CultureInfo.InvariantCulture), + short @short => BindConverter.FormatValue(@short, CultureInfo.InvariantCulture), + float @float => BindConverter.FormatValue(@float, CultureInfo.InvariantCulture), + double @double => BindConverter.FormatValue(@double, CultureInfo.InvariantCulture), + decimal @decimal => BindConverter.FormatValue(@decimal, CultureInfo.InvariantCulture), + _ => throw new InvalidOperationException($"Unsupported type {value!.GetType()}"), + }; private void SetStep() { diff --git a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs index 1e60c0944..600d00432 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs @@ -130,9 +130,9 @@ public partial class ValidateForm : IAsyncDisposable if (validator != null) { var results = new List - { - new ValidationResult(errorMessage, new string[] { fieldName }) - }; + { + new ValidationResult(errorMessage, new string[] { fieldName }) + }; validator.ToggleMessage(results, true); } } @@ -150,9 +150,9 @@ public partial class ValidateForm : IAsyncDisposable if (validator != null) { var results = new List - { - new ValidationResult(errorMessage, new string[] { fieldName }) - }; + { + new ValidationResult(errorMessage, new string[] { fieldName }) + }; validator.ToggleMessage(results, true); } } diff --git a/test/UnitTest/Components/InputNumberTest.cs b/test/UnitTest/Components/InputNumberTest.cs new file mode 100644 index 000000000..9b33ff56d --- /dev/null +++ b/test/UnitTest/Components/InputNumberTest.cs @@ -0,0 +1,167 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using BootstrapBlazor.Shared; +using System.ComponentModel.DataAnnotations; + +namespace UnitTest.Components; + +public class InputNumberTest : BootstrapBlazorTestBase +{ + [Fact] + public void OnBlur_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Min, "0"); + pb.Add(a => a.Max, "10"); + pb.Add(a => a.Step, "2"); + }); + cut.Contains("min=\"0\""); + cut.Contains("max=\"10\""); + cut.Contains("step=\"2\""); + + var input = cut.Find("input"); + cut.InvokeAsync(() => input.Blur()); + } + + [Fact] + public void ValidateForm() + { + var foo = new Cat() { Count = 20 }; + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Model, foo); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Value, foo.Count); + pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, nameof(Cat.Count), typeof(int))); + }); + }); + cut.Contains("class=\"form-label\""); + + var input = cut.Find("input"); + cut.InvokeAsync(() => input.Change("")); + + var form = cut.Find("form"); + cut.InvokeAsync(() => form.Submit()); + cut.Contains("is-invalid"); + } + + [Fact] + public void InvalidOperationException_Error() + { + Assert.ThrowsAny(() => Context.RenderComponent>()); + } + + [Fact] + public void Formatter_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Value, 10.01m); + pb.Add(a => a.Formatter, v => $"{v + 1}"); + }); + var input = cut.Find("input"); + Assert.Equal("11.01", input.GetAttribute("value")); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.Formatter, null); + pb.Add(a => a.FormatString, "#0.0"); + }); + Assert.Equal("10.0", input.GetAttribute("value")); + + input = cut.Find("input"); + cut.InvokeAsync(() => input.Change("")); + } + + [Fact] + public void Formatter_Error() + { + Assert.ThrowsAny(() => Context.RenderComponent()); + } + + [Fact] + public void ShowButton_Ok() + { + var inc = false; + var dec = false; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.ShowButton, true); + pb.Add(a => a.OnIncrement, v => + { + dec = true; + return Task.CompletedTask; + }); + pb.Add(a => a.OnDecrement, v => + { + inc = true; + return Task.CompletedTask; + }); + }); + cut.Contains("class=\"input-group\""); + + var buttons = cut.FindAll("button"); + cut.InvokeAsync(() => buttons[0].Click()); + Assert.True(inc); + + cut.InvokeAsync(() => buttons[1].Click()); + Assert.True(dec); + } + + [Theory] + [InlineData(typeof(short))] + [InlineData(typeof(int))] + [InlineData(typeof(long))] + [InlineData(typeof(float))] + [InlineData(typeof(double))] + [InlineData(typeof(decimal))] + public void Type_Ok(Type t) + { + var cut = Context.Render(builder => + { + builder.OpenComponent(0, typeof(BootstrapInputNumber<>).MakeGenericType(t)); + builder.AddAttribute(1, "ShowButton", true); + builder.AddAttribute(1, "Min", "-10"); + builder.AddAttribute(1, "Max", "10"); + builder.CloseComponent(); + }); + var buttons = cut.FindAll("button"); + cut.InvokeAsync(() => buttons[0].Click()); + cut.InvokeAsync(() => buttons[1].Click()); + } + + private class Cat + { + [Range(1, 10)] + public int Count { get; set; } + } + + [ExcludeFromCodeCoverage] + private class MockInputNumber : BootstrapInputNumber + { + public override Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + + OnInitialized(); + + return Task.CompletedTask; + } + + protected override string? InternalFormat(string value) + { + return base.InternalFormat(value); + } + + protected override void OnInitialized() + { + base.OnInitialized(); + + InternalFormat(""); + } + } +}