!2480 test(#I4W9WY): add unit test for ValidateForm

* test: 增加异步提交单元测试
* test: 增加资源文件
* test: 增加资源文件单元测试
* test: 增加本地化单元测试
* refactor: 移除冗余代码
* refactor: 移除冗余代码
* refactor: 重构代码
* test: 增加代码覆盖率
* test: 增加辅助代码
* test: 增加 Upload 组件单元测试
* test: 增加属性是类单元测试
* test: 增加 Metadata 单元测试
* refactor: 重构代码
* test: 增加代码覆盖率
* test: 增加 SetError 单元测试
* test: 增加 ShowLabel 单元测试
* test: 增加 必填项 单元测试
* test: 增加校验所有字段单元测试
* test: 增加 OnFieldChanged 单元测试
* test: 增加 Validate 单元测试
* refactor: 增加 NotNullWhen 标签
* refactor: 重构逻辑增加空判断
* refactor: 使用 TryAdd 方法
* test: 增加 Validator 单元测试
* refactor: 移除冗余代码
This commit is contained in:
Argo 2022-03-04 03:10:11 +00:00
parent 12a50f5139
commit 4db1f69fed
6 changed files with 426 additions and 43 deletions

View File

@ -16,31 +16,27 @@ public class BootstrapBlazorDataAnnotationsValidator : ComponentBase
/// 获得/设置 当前编辑数据上下文 /// 获得/设置 当前编辑数据上下文
/// </summary> /// </summary>
[CascadingParameter] [CascadingParameter]
[NotNull]
private EditContext? CurrentEditContext { get; set; } private EditContext? CurrentEditContext { get; set; }
/// <summary> /// <summary>
/// 获得/设置 当前编辑窗体上下文 /// 获得/设置 当前编辑窗体上下文
/// </summary> /// </summary>
[CascadingParameter] [CascadingParameter]
public ValidateForm? EditForm { get; set; } private ValidateForm? ValidateForm { get; set; }
/// <summary> /// <summary>
/// 初始化方法 /// 初始化方法
/// </summary> /// </summary>
protected override void OnInitialized() protected override void OnInitialized()
{ {
if (EditForm == null) if (ValidateForm == null)
{ {
throw new InvalidOperationException($"{nameof(BootstrapBlazorDataAnnotationsValidator)} requires a cascading " + throw new InvalidOperationException($"{nameof(Components.BootstrapBlazorDataAnnotationsValidator)} requires a cascading " +
$"parameter of type {nameof(ValidateForm)}. For example, you can use {nameof(BootstrapBlazorDataAnnotationsValidator)} " + $"parameter of type {nameof(Components.ValidateForm)}. For example, you can use {nameof(Components.BootstrapBlazorDataAnnotationsValidator)} " +
$"inside an {nameof(ValidateForm)}."); $"inside an {nameof(Components.ValidateForm)}.");
} }
if (CurrentEditContext == null) CurrentEditContext.AddEditContextDataAnnotationsValidation(ValidateForm);
{
throw new InvalidOperationException($"{nameof(BootstrapBlazorDataAnnotationsValidator)} requires a cascading parameter of type {nameof(EditContext)}. For example, you can use {nameof(BootstrapBlazorDataAnnotationsValidator)} inside an EditForm.");
}
CurrentEditContext.AddEditContextDataAnnotationsValidation(EditForm);
} }
} }

View File

@ -95,7 +95,7 @@ public partial class ValidateForm : IAsyncDisposable
/// <param name="value"></param> /// <param name="value"></param>
internal void AddValidator((string FieldName, Type ModelType) key, (FieldIdentifier FieldIdentifier, IValidateComponent IValidateComponent) value) internal void AddValidator((string FieldName, Type ModelType) key, (FieldIdentifier FieldIdentifier, IValidateComponent IValidateComponent) value)
{ {
ValidatorCache.AddOrUpdate(key, k => value, (k, v) => v = value); ValidatorCache.TryAdd(key, value);
} }
/// <summary> /// <summary>
@ -125,15 +125,18 @@ public partial class ValidateForm : IAsyncDisposable
private void InternalSetError(MemberExpression exp, string errorMessage) private void InternalSetError(MemberExpression exp, string errorMessage)
{ {
var fieldName = exp.Member.Name; var fieldName = exp.Member.Name;
var modelType = exp.Expression?.Type; if (exp.Expression != null)
var validator = ValidatorCache.FirstOrDefault(c => c.Key.ModelType == modelType && c.Key.FieldName == fieldName).Value.ValidateComponent;
if (validator != null)
{ {
var results = new List<ValidationResult> var modelType = exp.Expression.Type;
var validator = ValidatorCache.FirstOrDefault(c => c.Key.ModelType == modelType && c.Key.FieldName == fieldName).Value.ValidateComponent;
if (validator != null)
{ {
new ValidationResult(errorMessage, new string[] { fieldName }) var results = new List<ValidationResult>
}; {
validator.ToggleMessage(results, true); new ValidationResult(errorMessage, new string[] { fieldName })
};
validator.ToggleMessage(results, true);
}
} }
} }
@ -144,17 +147,13 @@ public partial class ValidateForm : IAsyncDisposable
/// <param name="errorMessage">错误描述信息,可为空,为空时查找资源文件</param> /// <param name="errorMessage">错误描述信息,可为空,为空时查找资源文件</param>
public void SetError(string propertyName, string errorMessage) public void SetError(string propertyName, string errorMessage)
{ {
if (TryGetModelField(propertyName, out var modelType, out var fieldName) if (TryGetModelField(propertyName, out var modelType, out var fieldName) && TryGetValidator(modelType, fieldName, out var validator))
&& TryGetValidator(modelType, fieldName, out var validator))
{ {
if (validator != null) var results = new List<ValidationResult>
{ {
var results = new List<ValidationResult> new ValidationResult(errorMessage, new string[] { fieldName })
{ };
new ValidationResult(errorMessage, new string[] { fieldName }) validator.ToggleMessage(results, true);
};
validator.ToggleMessage(results, true);
}
} }
} }
@ -180,7 +179,7 @@ public partial class ValidateForm : IAsyncDisposable
return propNames.IsEmpty; return propNames.IsEmpty;
} }
private bool TryGetValidator(Type modelType, string fieldName, [MaybeNull] out IValidateComponent validator) private bool TryGetValidator(Type modelType, string fieldName, [NotNullWhen(true)] out IValidateComponent validator)
{ {
validator = ValidatorCache.FirstOrDefault(c => c.Key.ModelType == modelType && c.Key.FieldName == fieldName).Value.ValidateComponent; validator = ValidatorCache.FirstOrDefault(c => c.Key.ModelType == modelType && c.Key.FieldName == fieldName).Value.ValidateComponent;
return validator != null; return validator != null;
@ -333,11 +332,7 @@ public partial class ValidateForm : IAsyncDisposable
{ {
rule.ErrorMessage = result.ErrorMessage; rule.ErrorMessage = result.ErrorMessage;
} }
results.Add(new ValidationResult(rule.ErrorMessage, new string[] { memberName }));
var errorMessage = string.IsNullOrEmpty(rule.ErrorMessageResourceName)
? rule.FormatErrorMessage(displayName ?? memberName)
: rule.ErrorMessage;
results.Add(new ValidationResult(errorMessage, new string[] { memberName }));
} }
} }
} }
@ -350,8 +345,7 @@ public partial class ValidateForm : IAsyncDisposable
private void ValidateProperty(ValidationContext context, List<ValidationResult> results) private void ValidateProperty(ValidationContext context, List<ValidationResult> results)
{ {
// 获得所有可写属性 // 获得所有可写属性
var properties = context.ObjectType.GetRuntimeProperties() var properties = context.ObjectType.GetRuntimeProperties().Where(p => IsPublic(p) && p.CanWrite && !p.GetIndexParameters().Any());
.Where(p => IsPublic(p) && p.CanWrite && !p.GetIndexParameters().Any());
foreach (var pi in properties) foreach (var pi in properties)
{ {
// 设置其关联属性字段 // 设置其关联属性字段
@ -400,8 +394,8 @@ public partial class ValidateForm : IAsyncDisposable
// 处理多个上传文件 // 处理多个上传文件
uploader.UploadFiles.ForEach(file => uploader.UploadFiles.ForEach(file =>
{ {
// 优先检查 File 流,如果没有检查 FileName // 优先检查 File 流,不需要检查 FileName
ValidateDataAnnotations((object?)file.File ?? file.FileName, context, messages, pi, file.ValidateId); ValidateDataAnnotations(file.File, context, messages, pi, file.ValidateId);
}); });
} }
else else
@ -421,7 +415,7 @@ public partial class ValidateForm : IAsyncDisposable
} }
} }
private List<Button> AsyncSubmitButtons { get; set; } = new List<Button>(); private List<Button> AsyncSubmitButtons { get; } = new List<Button>();
/// <summary> /// <summary>
/// 注册提交按钮 /// 注册提交按钮

View File

@ -18,11 +18,11 @@ internal static class IStringLocalizerExtensions
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="text"></param> /// <param name="text"></param>
/// <returns></returns> /// <returns></returns>
public static bool TryGetLocalizerString(this IStringLocalizer? localizer, string key, [MaybeNullWhen(false)] out string? text) public static bool TryGetLocalizerString(this IStringLocalizer localizer, string key, [MaybeNullWhen(false)] out string? text)
{ {
var ret = false; var ret = false;
text = null; text = null;
var l = localizer?[key]; var l = localizer[key];
if (l != null) if (l != null)
{ {
ret = !l.ResourceNotFound; ret = !l.ResourceNotFound;

View File

@ -0,0 +1,384 @@
// 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 Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using System;
using System.ComponentModel.DataAnnotations;
namespace UnitTest.Components;
public class ValidateFormTest : BootstrapBlazorTestBase
{
[Fact]
public void BootstrapBlazorDataAnnotationsValidator_Error()
{
Assert.ThrowsAny<InvalidOperationException>(() => Context.RenderComponent<BootstrapBlazorDataAnnotationsValidator>());
}
[Fact]
public void Validate_Ok()
{
var valid = false;
var invalid = false;
var foo = new Foo();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.Add(a => a.OnValidSubmit, context =>
{
valid = true;
return Task.CompletedTask;
});
pb.Add(a => a.OnInvalidSubmit, context =>
{
invalid = true;
return Task.CompletedTask;
});
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Name);
pb.Add(a => a.ValueChanged, EventCallback.Factory.Create<string>(this, v => foo.Name = v));
pb.Add(a => a.ValueExpression, foo.GenerateValueExpression());
});
});
var form = cut.Find("form");
cut.InvokeAsync(() => form.Submit());
Assert.True(invalid);
cut.InvokeAsync(() => cut.Find("input").Change("Test"));
cut.InvokeAsync(() => form.Submit());
Assert.True(valid);
}
[Fact]
public void OnFieldValueChanged_Ok()
{
var changed = false;
var foo = new Foo();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.Add(a => a.OnFieldValueChanged, (fieldName, v) =>
{
changed = true;
});
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Name);
pb.Add(a => a.ValueChanged, EventCallback.Factory.Create<string>(this, v => foo.Name = v));
pb.Add(a => a.ValueExpression, foo.GenerateValueExpression());
});
});
var form = cut.Find("form");
cut.InvokeAsync(() => cut.Find("input").Change("Test"));
cut.InvokeAsync(() => form.Submit());
Assert.True(changed);
}
[Fact]
public void ValidateAllProperties_Ok()
{
var foo = new Foo();
var invalid = false;
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.Add(a => a.ValidateAllProperties, true);
pb.Add(a => a.OnInvalidSubmit, context =>
{
invalid = true;
return Task.CompletedTask;
});
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Name);
pb.Add(a => a.ValueChanged, EventCallback.Factory.Create<string>(this, v => foo.Name = v));
pb.Add(a => a.ValueExpression, foo.GenerateValueExpression());
});
});
var form = cut.Find("form");
cut.InvokeAsync(() => form.Submit());
Assert.True(invalid);
}
[Fact]
public void ShowRequiredMark_Ok()
{
var foo = new Foo();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.Add(a => a.ShowRequiredMark, true);
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Name);
pb.Add(a => a.ValueChanged, EventCallback.Factory.Create<string>(this, v => foo.Name = v));
pb.Add(a => a.ValueExpression, foo.GenerateValueExpression());
});
});
cut.Contains("required=\"true\"");
cut.SetParametersAndRender(pb =>
{
pb.Add(a => a.ShowRequiredMark, false);
});
cut.DoesNotContain("required=\"true\"");
}
[Fact]
public void ShowLabel_Ok()
{
var foo = new Foo();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.Add(a => a.ShowLabel, true);
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Name);
pb.Add(a => a.ValueChanged, EventCallback.Factory.Create<string>(this, v => foo.Name = v));
pb.Add(a => a.ValueExpression, foo.GenerateValueExpression());
});
});
cut.Contains("label");
cut.SetParametersAndRender(pb =>
{
pb.Add(a => a.ShowLabel, false);
});
cut.DoesNotContain("label");
}
[Fact]
public void SetError_Ok()
{
var foo = new Foo();
var dummy = new Dummy();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Name);
pb.Add(a => a.ValueExpression, foo.GenerateValueExpression());
});
pb.AddChildContent<DateTimePicker<DateTime?>>(pb =>
{
pb.Add(a => a.Value, dummy.Value);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(dummy, "Value", typeof(DateTime?)));
});
});
cut.Instance.SetError("Name", "Test_SetError");
cut.Instance.SetError("Test.Name", "Test_SetError");
cut.Instance.SetError<Foo>(f => f.Name, "Name_SetError");
}
[Fact]
public void SetError_UnaryExpression()
{
var foo = new Foo();
var dummy = new Dummy();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, dummy);
pb.AddChildContent<DateTimePicker<DateTime?>>(pb =>
{
pb.Add(a => a.Value, foo.DateTime);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "DateTime", typeof(DateTime?)));
});
pb.AddChildContent<DateTimePicker<DateTime?>>(pb =>
{
pb.Add(a => a.Value, dummy.Value);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(dummy, "Value", typeof(DateTime?)));
});
});
cut.Instance.SetError<Dummy>(f => f.Value, "Name_SetError");
}
[Fact]
public void MetadataTypeAttribute_Ok()
{
var foo = new Dummy();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.AddChildContent<DateTimePicker<DateTime?>>(pb =>
{
pb.Add(a => a.Value, foo.Value);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "Value", typeof(DateTime?)));
});
});
var form = cut.Find("form");
cut.InvokeAsync(() => form.Submit());
}
[Fact]
public void Validate_Class_Ok()
{
var foo = new Dummy();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.Add(a => a.ValidateAllProperties, true);
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Foo.Name);
});
});
var form = cut.Find("form");
cut.InvokeAsync(() => form.Submit());
}
[Fact]
public void Validate_UploadFile_Ok()
{
var foo = new Dummy() { File = "text.txt" };
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.AddChildContent<ButtonUpload<string>>(pb =>
{
pb.Add(a => a.Value, foo.File);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "File", typeof(string)));
});
});
var form = cut.Find("form");
cut.InvokeAsync(() => form.Submit());
}
[Fact]
public void Validate_Localizer_Ok()
{
var foo = new MockFoo();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Name);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "Name", typeof(string)));
});
});
var form = cut.Find("form");
cut.InvokeAsync(() => form.Submit());
}
[Fact]
public void Validate_Attribute_Ok()
{
var foo = new MockFoo();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Rule);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "Rule", typeof(string)));
});
});
var form = cut.Find("form");
cut.InvokeAsync(() => form.Submit());
}
[Fact]
public void Validate_MemberName_Ok()
{
var foo = new MockFoo();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Member);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "Member", typeof(string)));
});
});
var form = cut.Find("form");
cut.InvokeAsync(() => form.Submit());
}
[Fact]
public void Validate_Address_Ok()
{
var foo = new MockFoo();
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.Model, foo);
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Address);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "Address", typeof(string)));
});
});
var form = cut.Find("form");
cut.InvokeAsync(() => form.Submit());
}
[Fact]
public async Task ValidateFormButton_Ok()
{
var tcs = new TaskCompletionSource<bool>();
var valid = false;
var foo = new Foo() { Name = "Test" };
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(v => v.Model, foo);
pb.Add(v => v.OnValidSubmit, context =>
{
valid = true;
tcs.SetResult(true);
return Task.CompletedTask;
});
pb.AddChildContent<BootstrapInput<string>>(pb =>
{
pb.Add(a => a.Value, foo.Name);
pb.Add(a => a.ValueExpression, foo.GenerateValueExpression());
});
pb.AddChildContent<Button>(pb =>
{
pb.Add(b => b.IsAsync, true);
pb.Add(b => b.ButtonType, ButtonType.Submit);
});
});
await cut.InvokeAsync(() => cut.Find("form").Submit());
await tcs.Task;
Assert.True(valid);
}
[MetadataType(typeof(DummyMetadata))]
private class Dummy
{
[Required]
public DateTime? Value { get; set; }
public Foo Foo { get; set; } = new Foo();
[Required]
public string? File { get; set; }
}
private class DummyMetadata
{
[Required]
public DateTime? Value { get; set; }
}
private class MockFoo
{
[Required(ErrorMessage = "{0} is Required")]
public string? Name { get; set; }
[EmailAddress(ErrorMessage = "{0} must fill")]
[Display(Name = "Address")]
public string? Address { get; set; } = "test";
[Required()]
public string? Rule { get; set; }
[EmailAddress()]
public string? Member { get; set; } = "test";
}
}

View File

@ -46,9 +46,10 @@ public class BootstrapBlazorTestHost : IDisposable
protected virtual void ConfigureServices(IServiceCollection services) protected virtual void ConfigureServices(IServiceCollection services)
{ {
services.AddBootstrapBlazor(localizationAction: options => services.AddBootstrapBlazor();
services.ConfigureJsonLocalizationOptions(op =>
{ {
options.AdditionalJsonAssemblies = new[] { typeof(Alert).Assembly }; op.AdditionalJsonAssemblies = new[] { typeof(Alert).Assembly, GetType().Assembly };
}); });
} }

View File

@ -25,5 +25,13 @@
"PlaceHolder": "请选择 ...", "PlaceHolder": "请选择 ...",
"Primary": "小学", "Primary": "小学",
"Middel": "中学" "Middel": "中学"
},
"UnitTest.Components.ValidateFormTest.MockFoo": {
"{0} is Required": "Name is Required",
"Member.EmailAddress": "Member is Required",
"Email": "Age msg"
},
"System.ComponentModel.DataAnnotations.RequiredAttribute": {
"ErrorMessage": "Rule is Required"
} }
} }