Validation fixes for Blazor (#14972)
* Validation fixes for Blazor * Ensure validation result that are not associated with a member are recorded. Fixes https://github.com/aspnet/AspNetCore/issues/10643 * Add support for showing model-specific errors to ValidationSummary * Add support for nested validation and a more suitable CompareAttribute. Fixes https://github.com/aspnet/AspNetCore/issues/10526
This commit is contained in:
parent
5440cd319d
commit
c298c94fe1
|
|
@ -15,6 +15,7 @@
|
|||
<ProjectReferenceProvider Include="GetDocument.Insider" ProjectPath="$(RepoRoot)src\Tools\GetDocumentInsider\src\GetDocumentInsider.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.SignalR.Specification.Tests" ProjectPath="$(RepoRoot)src\SignalR\server\Specification.Tests\src\Microsoft.AspNetCore.SignalR.Specification.Tests.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Blazor.Build" ProjectPath="$(RepoRoot)src\Components\Blazor\Build\src\Microsoft.AspNetCore.Blazor.Build.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Blazor.DataAnnotations.Validation" ProjectPath="$(RepoRoot)src\Components\Blazor\Validation\src\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj" />
|
||||
<ProjectReferenceProvider Include="Ignitor" ProjectPath="$(RepoRoot)src\Components\Ignitor\src\Ignitor.csproj" />
|
||||
<ProjectReferenceProvider Include="BlazorServerApp" ProjectPath="$(RepoRoot)src\Components\Samples\BlazorServerApp\BlazorServerApp.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore" ProjectPath="$(RepoRoot)src\DefaultBuilder\src\Microsoft.AspNetCore.csproj" RefProjectPath="$(RepoRoot)src\DefaultBuilder\ref\Microsoft.AspNetCore.csproj" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace System.ComponentModel.DataAnnotations
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ValidationAttribute"/> that compares two properties
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||
public sealed class ComparePropertyAttribute : CompareAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="BlazorCompareAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="otherProperty">The property to compare with the current property.</param>
|
||||
public ComparePropertyAttribute(string otherProperty)
|
||||
: base(otherProperty)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||
{
|
||||
var validationResult = base.IsValid(value, validationContext);
|
||||
if (validationResult == ValidationResult.Success)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
return new ValidationResult(validationResult.ErrorMessage, new[] { validationContext.MemberName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<Description>Provides experimental support for validation using DataAnnotations.</Description>
|
||||
<IsShippingPackage>true</IsShippingPackage>
|
||||
<HasReferenceAssembly>false</HasReferenceAssembly>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Components.Forms" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Forms
|
||||
{
|
||||
public class ObjectGraphDataAnnotationsValidator : ComponentBase
|
||||
{
|
||||
private static readonly object ValidationContextValidatorKey = new object();
|
||||
private static readonly object ValidatedObjectsKey = new object();
|
||||
private ValidationMessageStore _validationMessageStore;
|
||||
|
||||
[CascadingParameter]
|
||||
internal EditContext EditContext { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_validationMessageStore = new ValidationMessageStore(EditContext);
|
||||
|
||||
// Perform object-level validation (starting from the root model) on request
|
||||
EditContext.OnValidationRequested += (sender, eventArgs) =>
|
||||
{
|
||||
_validationMessageStore.Clear();
|
||||
ValidateObject(EditContext.Model, new HashSet<object>());
|
||||
EditContext.NotifyValidationStateChanged();
|
||||
};
|
||||
|
||||
// Perform per-field validation on each field edit
|
||||
EditContext.OnFieldChanged += (sender, eventArgs) =>
|
||||
ValidateField(EditContext, _validationMessageStore, eventArgs.FieldIdentifier);
|
||||
}
|
||||
|
||||
internal void ValidateObject(object value, HashSet<object> visited)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!visited.Add(value))
|
||||
{
|
||||
// Already visited this object.
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is IEnumerable<object> enumerable)
|
||||
{
|
||||
var index = 0;
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
ValidateObject(item, visited);
|
||||
index++;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var validationResults = new List<ValidationResult>();
|
||||
ValidateObject(value, visited, validationResults);
|
||||
|
||||
// Transfer results to the ValidationMessageStore
|
||||
foreach (var validationResult in validationResults)
|
||||
{
|
||||
if (!validationResult.MemberNames.Any())
|
||||
{
|
||||
_validationMessageStore.Add(new FieldIdentifier(value, string.Empty), validationResult.ErrorMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var memberName in validationResult.MemberNames)
|
||||
{
|
||||
var fieldIdentifier = new FieldIdentifier(value, memberName);
|
||||
_validationMessageStore.Add(fieldIdentifier, validationResult.ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateObject(object value, HashSet<object> visited, List<ValidationResult> validationResults)
|
||||
{
|
||||
var validationContext = new ValidationContext(value);
|
||||
validationContext.Items.Add(ValidationContextValidatorKey, this);
|
||||
validationContext.Items.Add(ValidatedObjectsKey, visited);
|
||||
Validator.TryValidateObject(value, validationContext, validationResults, validateAllProperties: true);
|
||||
}
|
||||
|
||||
internal static bool TryValidateRecursive(object value, ValidationContext validationContext)
|
||||
{
|
||||
if (validationContext.Items.TryGetValue(ValidationContextValidatorKey, out var result) && result is ObjectGraphDataAnnotationsValidator validator)
|
||||
{
|
||||
var visited = (HashSet<object>)validationContext.Items[ValidatedObjectsKey];
|
||||
validator.ValidateObject(value, visited);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier)
|
||||
{
|
||||
// DataAnnotations only validates public properties, so that's all we'll look for
|
||||
var propertyInfo = fieldIdentifier.Model.GetType().GetProperty(fieldIdentifier.FieldName);
|
||||
if (propertyInfo != null)
|
||||
{
|
||||
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
|
||||
var validationContext = new ValidationContext(fieldIdentifier.Model)
|
||||
{
|
||||
MemberName = propertyInfo.Name
|
||||
};
|
||||
var results = new List<ValidationResult>();
|
||||
|
||||
Validator.TryValidateProperty(propertyValue, validationContext, results);
|
||||
messages.Clear(fieldIdentifier);
|
||||
messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage));
|
||||
|
||||
// We have to notify even if there were no messages before and are still no messages now,
|
||||
// because the "state" that changed might be the completion of some async validation task
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
|
||||
namespace System.ComponentModel.DataAnnotations
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ValidationAttribute"/> that indicates that the property is a complex or collection type that further needs to be validated.
|
||||
/// <para>
|
||||
/// By default <see cref="Validator"/> does not recurse in to complex property types during validation.
|
||||
/// When used in conjunction with <see cref="ObjectGraphDataAnnotationsValidator"/>, this property allows the validation system to validate
|
||||
/// complex or collection type properties.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||
public sealed class ValidateComplexTypeAttribute : ValidationAttribute
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||
{
|
||||
if (!ObjectGraphDataAnnotationsValidator.TryValidateRecursive(value, validationContext))
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(ValidateComplexTypeAttribute)} can only used with {nameof(ObjectGraphDataAnnotationsValidator)}.");
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Blazor.DataAnnotations.Validation" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,540 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
public class ObjectGraphDataAnnotationsValidatorTest
|
||||
{
|
||||
public class SimpleModel
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Range(1, 16)]
|
||||
public int Age { get; set; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_SimpleObject()
|
||||
{
|
||||
var model = new SimpleModel
|
||||
{
|
||||
Age = 23,
|
||||
};
|
||||
|
||||
var editContext = Validate(model);
|
||||
var messages = editContext.GetValidationMessages(() => model.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.Age);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Equal(2, editContext.GetValidationMessages().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_SimpleObject_AllValid()
|
||||
{
|
||||
var model = new SimpleModel { Name = "Test", Age = 5 };
|
||||
|
||||
var editContext = Validate(model);
|
||||
var messages = editContext.GetValidationMessages(() => model.Name);
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.Age);
|
||||
Assert.Empty(messages);
|
||||
|
||||
Assert.Empty(editContext.GetValidationMessages());
|
||||
}
|
||||
|
||||
public class ModelWithComplexProperty
|
||||
{
|
||||
[Required]
|
||||
public string Property1 { get; set; }
|
||||
|
||||
[ValidateComplexType]
|
||||
public SimpleModel SimpleModel { get; set; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_NullComplexProperty()
|
||||
{
|
||||
var model = new ModelWithComplexProperty();
|
||||
|
||||
var editContext = Validate(model);
|
||||
var messages = editContext.GetValidationMessages(() => model.Property1);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Single(editContext.GetValidationMessages());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_ModelWithComplexProperties()
|
||||
{
|
||||
var model = new ModelWithComplexProperty { SimpleModel = new SimpleModel() };
|
||||
|
||||
var editContext = Validate(model);
|
||||
var messages = editContext.GetValidationMessages(() => model.Property1);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.SimpleModel);
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.SimpleModel.Age);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.SimpleModel.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Equal(3, editContext.GetValidationMessages().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_ModelWithComplexProperties_SomeValid()
|
||||
{
|
||||
var model = new ModelWithComplexProperty
|
||||
{
|
||||
Property1 = "Value",
|
||||
SimpleModel = new SimpleModel { Name = "Some Value" },
|
||||
};
|
||||
|
||||
var editContext = Validate(model);
|
||||
var messages = editContext.GetValidationMessages(() => model.Property1);
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.SimpleModel);
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.SimpleModel.Age);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.SimpleModel.Name);
|
||||
Assert.Empty(messages);
|
||||
|
||||
Assert.Single(editContext.GetValidationMessages());
|
||||
}
|
||||
|
||||
public class TestValidatableObject : IValidatableObject
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
yield return new ValidationResult("Custom validation error");
|
||||
}
|
||||
}
|
||||
|
||||
public class ModelWithValidatableComplexProperty
|
||||
{
|
||||
[Required]
|
||||
public string Property1 { get; set; }
|
||||
|
||||
[ValidateComplexType]
|
||||
public TestValidatableObject Property2 { get; set; } = new TestValidatableObject();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_ValidatableComplexProperty()
|
||||
{
|
||||
var model = new ModelWithValidatableComplexProperty();
|
||||
|
||||
var editContext = Validate(model);
|
||||
var messages = editContext.GetValidationMessages(() => model.Property1);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.Property2);
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.Property2.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Equal(2, editContext.GetValidationMessages().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_ValidatableComplexProperty_ValidatesIValidatableProperty()
|
||||
{
|
||||
var model = new ModelWithValidatableComplexProperty
|
||||
{
|
||||
Property2 = new TestValidatableObject { Name = "test" },
|
||||
};
|
||||
|
||||
var editContext = Validate(model);
|
||||
var messages = editContext.GetValidationMessages(() => model.Property1);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(new FieldIdentifier(model.Property2, string.Empty));
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.Property2.Name);
|
||||
Assert.Empty(messages);
|
||||
|
||||
Assert.Equal(2, editContext.GetValidationMessages().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_ModelIsIValidatable_PropertyHasError()
|
||||
{
|
||||
var model = new TestValidatableObject();
|
||||
|
||||
var editContext = Validate(model);
|
||||
var messages = editContext.GetValidationMessages(new FieldIdentifier(model, string.Empty));
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Single(editContext.GetValidationMessages());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_ModelIsIValidatable_ModelHasError()
|
||||
{
|
||||
var model = new TestValidatableObject { Name = "test" };
|
||||
|
||||
var editContext = Validate(model);
|
||||
var messages = editContext.GetValidationMessages(new FieldIdentifier(model, string.Empty));
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.Name);
|
||||
Assert.Empty(messages);
|
||||
|
||||
Assert.Single(editContext.GetValidationMessages());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_CollectionModel()
|
||||
{
|
||||
var model = new List<SimpleModel>
|
||||
{
|
||||
new SimpleModel(),
|
||||
new SimpleModel { Name = "test", },
|
||||
};
|
||||
|
||||
var editContext = Validate(model);
|
||||
|
||||
var item = model[0];
|
||||
var messages = editContext.GetValidationMessages(new FieldIdentifier(model, "0"));
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => item.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => item.Age);
|
||||
Assert.Single(messages);
|
||||
|
||||
item = model[1];
|
||||
messages = editContext.GetValidationMessages(new FieldIdentifier(model, "1"));
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => item.Name);
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => item.Age);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Equal(3, editContext.GetValidationMessages().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_CollectionValidatableModel()
|
||||
{
|
||||
var model = new List<TestValidatableObject>
|
||||
{
|
||||
new TestValidatableObject(),
|
||||
new TestValidatableObject { Name = "test", },
|
||||
};
|
||||
|
||||
var editContext = Validate(model);
|
||||
|
||||
var item = model[0];
|
||||
var messages = editContext.GetValidationMessages(() => item.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
item = model[1];
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => item.Name);
|
||||
Assert.Empty(messages);
|
||||
|
||||
Assert.Equal(2, editContext.GetValidationMessages().Count());
|
||||
}
|
||||
|
||||
private class Level1Validation
|
||||
{
|
||||
[ValidateComplexType]
|
||||
public Level2Validation Level2 { get; set; }
|
||||
}
|
||||
|
||||
public class Level2Validation
|
||||
{
|
||||
[ValidateComplexType]
|
||||
public SimpleModel Level3 { get; set; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_ManyLevels()
|
||||
{
|
||||
var model = new Level1Validation
|
||||
{
|
||||
Level2 = new Level2Validation
|
||||
{
|
||||
Level3 = new SimpleModel
|
||||
{
|
||||
Age = 47,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var editContext = Validate(model);
|
||||
var level3 = model.Level2.Level3;
|
||||
|
||||
var messages = editContext.GetValidationMessages(() => level3.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => level3.Age);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Equal(2, editContext.GetValidationMessages().Count());
|
||||
}
|
||||
|
||||
private class Person
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
|
||||
[ValidateComplexType]
|
||||
public Person Related { get; set; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_RecursiveRelation()
|
||||
{
|
||||
var model = new Person { Related = new Person() };
|
||||
model.Related.Related = model;
|
||||
|
||||
var editContext = Validate(model);
|
||||
|
||||
var messages = editContext.GetValidationMessages(() => model.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => model.Related.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Equal(2, editContext.GetValidationMessages().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_RecursiveRelation_OverManySteps()
|
||||
{
|
||||
var person1 = new Person();
|
||||
var person2 = new Person { Name = "Valid name" };
|
||||
var person3 = new Person();
|
||||
var person4 = new Person();
|
||||
|
||||
person1.Related = person2;
|
||||
person2.Related = person3;
|
||||
person3.Related = person4;
|
||||
person4.Related = person1;
|
||||
|
||||
var editContext = Validate(person1);
|
||||
|
||||
var messages = editContext.GetValidationMessages(() => person1.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => person2.Name);
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => person3.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => person4.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Equal(3, editContext.GetValidationMessages().Count());
|
||||
}
|
||||
|
||||
private class Node
|
||||
{
|
||||
[Required]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ValidateComplexType]
|
||||
public List<Node> Related { get; set; } = new List<Node>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_RecursiveRelation_ViaCollection()
|
||||
{
|
||||
var node1 = new Node();
|
||||
var node2 = new Node { Id = "Valid Id" };
|
||||
var node3 = new Node();
|
||||
node1.Related.Add(node2);
|
||||
node2.Related.Add(node3);
|
||||
node3.Related.Add(node1);
|
||||
|
||||
var editContext = Validate(node1);
|
||||
|
||||
var messages = editContext.GetValidationMessages(() => node1.Id);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => node2.Id);
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => node3.Id);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Equal(2, editContext.GetValidationMessages().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateObject_RecursiveRelation_InCollection()
|
||||
{
|
||||
var person1 = new Person();
|
||||
var person2 = new Person { Name = "Valid name" };
|
||||
var person3 = new Person();
|
||||
var person4 = new Person();
|
||||
|
||||
person1.Related = person2;
|
||||
person2.Related = person3;
|
||||
person3.Related = person4;
|
||||
person4.Related = person1;
|
||||
|
||||
var editContext = Validate(person1);
|
||||
|
||||
var messages = editContext.GetValidationMessages(() => person1.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => person2.Name);
|
||||
Assert.Empty(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => person3.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
messages = editContext.GetValidationMessages(() => person4.Name);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Equal(3, editContext.GetValidationMessages().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateField_PropertyValid()
|
||||
{
|
||||
var model = new SimpleModel { Age = 1 };
|
||||
var fieldIdentifier = FieldIdentifier.Create(() => model.Age);
|
||||
|
||||
var editContext = ValidateField(model, fieldIdentifier);
|
||||
var messages = editContext.GetValidationMessages(fieldIdentifier);
|
||||
Assert.Empty(messages);
|
||||
|
||||
Assert.Empty(editContext.GetValidationMessages());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateField_PropertyInvalid()
|
||||
{
|
||||
var model = new SimpleModel { Age = 42 };
|
||||
var fieldIdentifier = FieldIdentifier.Create(() => model.Age);
|
||||
|
||||
var editContext = ValidateField(model, fieldIdentifier);
|
||||
var messages = editContext.GetValidationMessages(fieldIdentifier);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Single(editContext.GetValidationMessages());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateField_AfterSubmitValidation()
|
||||
{
|
||||
var model = new SimpleModel { Age = 42 };
|
||||
var fieldIdentifier = FieldIdentifier.Create(() => model.Age);
|
||||
|
||||
var editContext = Validate(model);
|
||||
var messages = editContext.GetValidationMessages(fieldIdentifier);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Equal(2, editContext.GetValidationMessages().Count());
|
||||
|
||||
model.Age = 4;
|
||||
|
||||
editContext.NotifyFieldChanged(fieldIdentifier);
|
||||
messages = editContext.GetValidationMessages(fieldIdentifier);
|
||||
Assert.Empty(messages);
|
||||
|
||||
Assert.Single(editContext.GetValidationMessages());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateField_ModelWithComplexProperty()
|
||||
{
|
||||
var model = new ModelWithComplexProperty
|
||||
{
|
||||
SimpleModel = new SimpleModel { Age = 1 },
|
||||
};
|
||||
var fieldIdentifier = FieldIdentifier.Create(() => model.SimpleModel.Name);
|
||||
|
||||
var editContext = ValidateField(model, fieldIdentifier);
|
||||
var messages = editContext.GetValidationMessages(fieldIdentifier);
|
||||
Assert.Single(messages);
|
||||
|
||||
Assert.Single(editContext.GetValidationMessages());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateField_ModelWithComplexProperty_AfterSubmitValidation()
|
||||
{
|
||||
var model = new ModelWithComplexProperty
|
||||
{
|
||||
Property1 = "test",
|
||||
SimpleModel = new SimpleModel { Age = 29, Name = "Test" },
|
||||
};
|
||||
var fieldIdentifier = FieldIdentifier.Create(() => model.SimpleModel.Age);
|
||||
|
||||
var editContext = Validate(model);
|
||||
var messages = editContext.GetValidationMessages(fieldIdentifier);
|
||||
Assert.Single(messages);
|
||||
|
||||
model.SimpleModel.Age = 9;
|
||||
editContext.NotifyFieldChanged(fieldIdentifier);
|
||||
|
||||
messages = editContext.GetValidationMessages(fieldIdentifier);
|
||||
Assert.Empty(messages);
|
||||
Assert.Empty(editContext.GetValidationMessages());
|
||||
}
|
||||
|
||||
private static EditContext Validate(object model)
|
||||
{
|
||||
var editContext = new EditContext(model);
|
||||
var validator = new TestObjectGraphDataAnnotationsValidator { EditContext = editContext, };
|
||||
validator.OnInitialized();
|
||||
|
||||
editContext.Validate();
|
||||
|
||||
return editContext;
|
||||
}
|
||||
|
||||
private static EditContext ValidateField(object model, in FieldIdentifier field)
|
||||
{
|
||||
var editContext = new EditContext(model);
|
||||
var validator = new TestObjectGraphDataAnnotationsValidator { EditContext = editContext, };
|
||||
validator.OnInitialized();
|
||||
|
||||
editContext.NotifyFieldChanged(field);
|
||||
|
||||
return editContext;
|
||||
}
|
||||
|
||||
private class TestObjectGraphDataAnnotationsValidator : ObjectGraphDataAnnotationsValidator
|
||||
{
|
||||
public new void OnInitialized() => base.OnInitialized();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -240,6 +240,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ignitor", "Ignitor\src\Igni
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ignitor.Test", "Ignitor\test\Ignitor.Test.csproj", "{F31E8118-014E-4CCE-8A48-5282F7B9BB3E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Validation", "Validation", "{FD9BD646-9D50-42ED-A3E1-90558BA0C6B2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.DataAnnotations.Validation", "Blazor\Validation\src\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj", "{B70F90C7-2696-4050-B24E-BF0308F4E059}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests", "Blazor\Validation\test\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj", "{A5617A9D-C71E-44DE-936C-27611EB40A02}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -1486,6 +1492,30 @@ Global
|
|||
{F31E8118-014E-4CCE-8A48-5282F7B9BB3E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F31E8118-014E-4CCE-8A48-5282F7B9BB3E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F31E8118-014E-4CCE-8A48-5282F7B9BB3E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -1596,6 +1626,9 @@ Global
|
|||
{BBF37AF9-8290-4B70-8BA8-0F6017B3B620} = {46E4300C-5726-4108-B9A2-18BB94EB26ED}
|
||||
{CD0EF85C-4187-4515-A355-E5A0D4485F40} = {BDE2397D-C53A-4783-8B3A-1F54F48A6926}
|
||||
{F31E8118-014E-4CCE-8A48-5282F7B9BB3E} = {BDE2397D-C53A-4783-8B3A-1F54F48A6926}
|
||||
{FD9BD646-9D50-42ED-A3E1-90558BA0C6B2} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
|
||||
{B70F90C7-2696-4050-B24E-BF0308F4E059} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
|
||||
{A5617A9D-C71E-44DE-936C-27611EB40A02} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {CC3C47E1-AD1A-4619-9CD3-E08A0148E5CE}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
"Blazor\\Http\\test\\Microsoft.AspNetCore.Blazor.HttpClient.Tests.csproj",
|
||||
"Blazor\\Server\\src\\Microsoft.AspNetCore.Blazor.Server.csproj",
|
||||
"Blazor\\Templates\\src\\Microsoft.AspNetCore.Blazor.Templates.csproj",
|
||||
"Blazor\\Validation\\src\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj",
|
||||
"Blazor\\Validation\\test\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj",
|
||||
"Blazor\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj",
|
||||
"Blazor\\testassets\\HostedInAspNet.Server\\HostedInAspNet.Server.csproj",
|
||||
"Blazor\\testassets\\Microsoft.AspNetCore.Blazor.E2EPerformance\\Microsoft.AspNetCore.Blazor.E2EPerformance.csproj",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
messages.Clear();
|
||||
foreach (var validationResult in validationResults)
|
||||
{
|
||||
if (!validationResult.MemberNames.Any())
|
||||
{
|
||||
messages.Add(new FieldIdentifier(editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var memberName in validationResult.MemberNames)
|
||||
{
|
||||
messages.Add(editContext.Field(memberName), validationResult.ErrorMessage);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<Reference Include="Microsoft.AspNetCore.HttpsPolicy" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
||||
<Reference Include="Microsoft.Extensions.Hosting" />
|
||||
<Reference Include="Microsoft.AspNetCore.Blazor.DataAnnotations.Validation" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -125,6 +125,8 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
public ValidationSummary() { }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)]
|
||||
public System.Collections.Generic.IReadOnlyDictionary<string, object> AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public object Model { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
|
||||
protected virtual void Dispose(bool disposing) { }
|
||||
protected override void OnParametersSet() { }
|
||||
|
|
|
|||
|
|
@ -125,6 +125,8 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
public ValidationSummary() { }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)]
|
||||
public System.Collections.Generic.IReadOnlyDictionary<string, object> AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public object Model { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
|
||||
protected virtual void Dispose(bool disposing) { }
|
||||
protected override void OnParametersSet() { }
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
private EditContext _previousEditContext;
|
||||
private readonly EventHandler<ValidationStateChangedEventArgs> _validationStateChangedHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the model to produce the list of validation messages for.
|
||||
/// When specified, this lists all errors that are associated with the model instance.
|
||||
/// </summary>
|
||||
[Parameter] public object Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of additional attributes that will be applied to the created <c>ul</c> element.
|
||||
/// </summary>
|
||||
|
|
@ -57,22 +63,31 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
{
|
||||
// As an optimization, only evaluate the messages enumerable once, and
|
||||
// only produce the enclosing <ul> if there's at least one message
|
||||
var messagesEnumerator = CurrentEditContext.GetValidationMessages().GetEnumerator();
|
||||
if (messagesEnumerator.MoveNext())
|
||||
var validationMessages = Model is null ?
|
||||
CurrentEditContext.GetValidationMessages() :
|
||||
CurrentEditContext.GetValidationMessages(new FieldIdentifier(Model, string.Empty));
|
||||
|
||||
var first = true;
|
||||
foreach (var error in validationMessages)
|
||||
{
|
||||
builder.OpenElement(0, "ul");
|
||||
builder.AddMultipleAttributes(1, AdditionalAttributes);
|
||||
builder.AddAttribute(2, "class", "validation-errors");
|
||||
|
||||
do
|
||||
if (first)
|
||||
{
|
||||
builder.OpenElement(3, "li");
|
||||
builder.AddAttribute(4, "class", "validation-message");
|
||||
builder.AddContent(5, messagesEnumerator.Current);
|
||||
builder.CloseElement();
|
||||
}
|
||||
while (messagesEnumerator.MoveNext());
|
||||
first = false;
|
||||
|
||||
builder.OpenElement(0, "ul");
|
||||
builder.AddMultipleAttributes(1, AdditionalAttributes);
|
||||
builder.AddAttribute(2, "class", "validation-errors");
|
||||
}
|
||||
|
||||
builder.OpenElement(3, "li");
|
||||
builder.AddAttribute(4, "class", "validation-message");
|
||||
builder.AddContent(5, error);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
if (!first)
|
||||
{
|
||||
// We have at least one validation message.
|
||||
builder.CloseElement();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using BasicTestApp;
|
||||
using BasicTestApp.FormsTest;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
|
|
@ -34,14 +33,20 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
|
||||
}
|
||||
|
||||
protected virtual IWebElement MountSimpleValidationComponent()
|
||||
=> Browser.MountTestComponent<SimpleValidationComponent>();
|
||||
|
||||
protected virtual IWebElement MountTypicalValidationComponent()
|
||||
=> Browser.MountTestComponent<TypicalValidationComponent>();
|
||||
|
||||
[Fact]
|
||||
public async Task EditFormWorksWithDataAnnotationsValidator()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<SimpleValidationComponent>();
|
||||
var appElement = MountSimpleValidationComponent();;
|
||||
var form = appElement.FindElement(By.TagName("form"));
|
||||
var userNameInput = appElement.FindElement(By.ClassName("user-name")).FindElement(By.TagName("input"));
|
||||
var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input"));
|
||||
var submitButton = appElement.FindElement(By.TagName("button"));
|
||||
var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
|
||||
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||
|
||||
// The form emits unmatched attributes
|
||||
|
|
@ -77,7 +82,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void InputTextInteractsWithEditContext()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<TypicalValidationComponent>();
|
||||
var appElement = MountTypicalValidationComponent();
|
||||
var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
|
||||
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||
|
||||
|
|
@ -104,7 +109,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void InputNumberInteractsWithEditContext_NonNullableInt()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<TypicalValidationComponent>();
|
||||
var appElement = MountTypicalValidationComponent();
|
||||
var ageInput = appElement.FindElement(By.ClassName("age")).FindElement(By.TagName("input"));
|
||||
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||
|
||||
|
|
@ -136,7 +141,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void InputNumberInteractsWithEditContext_NullableFloat()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<TypicalValidationComponent>();
|
||||
var appElement = MountTypicalValidationComponent();
|
||||
var heightInput = appElement.FindElement(By.ClassName("height")).FindElement(By.TagName("input"));
|
||||
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||
|
||||
|
|
@ -160,7 +165,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void InputTextAreaInteractsWithEditContext()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<TypicalValidationComponent>();
|
||||
var appElement = MountTypicalValidationComponent();
|
||||
var descriptionInput = appElement.FindElement(By.ClassName("description")).FindElement(By.TagName("textarea"));
|
||||
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||
|
||||
|
|
@ -187,7 +192,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void InputDateInteractsWithEditContext_NonNullableDateTime()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<TypicalValidationComponent>();
|
||||
var appElement = MountTypicalValidationComponent();
|
||||
var renewalDateInput = appElement.FindElement(By.ClassName("renewal-date")).FindElement(By.TagName("input"));
|
||||
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||
|
||||
|
|
@ -218,7 +223,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void InputDateInteractsWithEditContext_NullableDateTimeOffset()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<TypicalValidationComponent>();
|
||||
var appElement = MountTypicalValidationComponent();
|
||||
var expiryDateInput = appElement.FindElement(By.ClassName("expiry-date")).FindElement(By.TagName("input"));
|
||||
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||
|
||||
|
|
@ -241,7 +246,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void InputSelectInteractsWithEditContext()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<TypicalValidationComponent>();
|
||||
var appElement = MountTypicalValidationComponent();
|
||||
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select")));
|
||||
var select = ticketClassInput.WrappedElement;
|
||||
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||
|
|
@ -263,7 +268,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void InputCheckboxInteractsWithEditContext()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<TypicalValidationComponent>();
|
||||
var appElement = MountTypicalValidationComponent();
|
||||
var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input"));
|
||||
var isEvilInput = appElement.FindElement(By.ClassName("is-evil")).FindElement(By.TagName("input"));
|
||||
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||
|
|
@ -297,7 +302,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
var appElement = Browser.MountTestComponent<NotifyPropertyChangedValidationComponent>();
|
||||
var userNameInput = appElement.FindElement(By.ClassName("user-name")).FindElement(By.TagName("input"));
|
||||
var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input"));
|
||||
var submitButton = appElement.FindElement(By.TagName("button"));
|
||||
var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
|
||||
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||
var submissionStatus = appElement.FindElement(By.Id("submission-status"));
|
||||
|
||||
|
|
@ -331,11 +336,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void ValidationMessageDisplaysMessagesForField()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<TypicalValidationComponent>();
|
||||
var appElement = MountTypicalValidationComponent();
|
||||
var emailContainer = appElement.FindElement(By.ClassName("email"));
|
||||
var emailInput = emailContainer.FindElement(By.TagName("input"));
|
||||
var emailMessagesAccessor = CreateValidationMessagesAccessor(emailContainer);
|
||||
var submitButton = appElement.FindElement(By.TagName("button"));
|
||||
var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
|
||||
|
||||
// Doesn't show messages for other fields
|
||||
submitButton.Click();
|
||||
|
|
@ -355,10 +360,43 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
Browser.Empty(emailMessagesAccessor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ErrorsFromCompareAttribute()
|
||||
{
|
||||
var appElement = MountTypicalValidationComponent();
|
||||
var emailContainer = appElement.FindElement(By.ClassName("email"));
|
||||
var emailInput = emailContainer.FindElement(By.TagName("input"));
|
||||
var confirmEmailContainer = appElement.FindElement(By.ClassName("confirm-email"));
|
||||
var confirmInput = confirmEmailContainer.FindElement(By.TagName("input"));
|
||||
var confirmEmailValidationMessage = CreateValidationMessagesAccessor(confirmEmailContainer);
|
||||
var modelErrors = CreateValidationMessagesAccessor(appElement.FindElement(By.ClassName("model-errors")));
|
||||
CreateValidationMessagesAccessor(emailContainer);
|
||||
var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
|
||||
|
||||
// Updates on edit
|
||||
emailInput.SendKeys("a@b.com\t");
|
||||
|
||||
submitButton.Click();
|
||||
Browser.Empty(confirmEmailValidationMessage);
|
||||
Browser.Equal(new[] { "Email and confirm email do not match." }, modelErrors);
|
||||
|
||||
confirmInput.SendKeys("not-test@example.com\t");
|
||||
Browser.Equal(new[] { "Email and confirm email do not match." }, confirmEmailValidationMessage);
|
||||
|
||||
// Can become correct
|
||||
confirmInput.Clear();
|
||||
confirmInput.SendKeys("a@b.com\t");
|
||||
|
||||
Browser.Empty(confirmEmailValidationMessage);
|
||||
|
||||
submitButton.Click();
|
||||
Browser.Empty(modelErrors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InputComponentsCauseContainerToRerenderOnChange()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<TypicalValidationComponent>();
|
||||
var appElement = MountTypicalValidationComponent();
|
||||
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select")));
|
||||
var selectedTicketClassDisplay = appElement.FindElement(By.Id("selected-ticket-class"));
|
||||
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using BasicTestApp.FormsTest;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||
{
|
||||
public class FormsTestWithExperimentalValidator : FormsTest
|
||||
{
|
||||
public FormsTestWithExperimentalValidator(
|
||||
BrowserFixture browserFixture,
|
||||
ToggleExecutionModeServerFixture<BasicTestApp.Program> serverFixture,
|
||||
ITestOutputHelper output) : base(browserFixture, serverFixture, output)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IWebElement MountSimpleValidationComponent()
|
||||
=> Browser.MountTestComponent<SimpleValidationComponentUsingExperimentalValidator>();
|
||||
|
||||
protected override IWebElement MountTypicalValidationComponent()
|
||||
=> Browser.MountTestComponent<TypicalValidationComponentUsingExperimentalValidator>();
|
||||
|
||||
[Fact]
|
||||
public void EditFormWorksWithNestedValidation()
|
||||
{
|
||||
var appElement = Browser.MountTestComponent<ExperimentalValidationComponent>();
|
||||
|
||||
var nameInput = appElement.FindElement(By.CssSelector(".name input"));
|
||||
var emailInput = appElement.FindElement(By.CssSelector(".email input"));
|
||||
var confirmEmailInput = appElement.FindElement(By.CssSelector(".confirm-email input"));
|
||||
var streetInput = appElement.FindElement(By.CssSelector(".street input"));
|
||||
var zipInput = appElement.FindElement(By.CssSelector(".zip input"));
|
||||
var countryInput = new SelectElement(appElement.FindElement(By.CssSelector(".country select")));
|
||||
var descriptionInput = appElement.FindElement(By.CssSelector(".description input"));
|
||||
var weightInput = appElement.FindElement(By.CssSelector(".weight input"));
|
||||
|
||||
var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
|
||||
|
||||
submitButton.Click();
|
||||
|
||||
Browser.Equal(4, () => appElement.FindElements(By.CssSelector(".all-errors .validation-message")).Count);
|
||||
|
||||
Browser.Equal("Enter a name", () => appElement.FindElement(By.CssSelector(".name .validation-message")).Text);
|
||||
Browser.Equal("Enter an email", () => appElement.FindElement(By.CssSelector(".email .validation-message")).Text);
|
||||
Browser.Equal("A street address is required.", () => appElement.FindElement(By.CssSelector(".street .validation-message")).Text);
|
||||
Browser.Equal("Description is required.", () => appElement.FindElement(By.CssSelector(".description .validation-message")).Text);
|
||||
|
||||
// Verify class-level validation
|
||||
nameInput.SendKeys("Some person");
|
||||
emailInput.SendKeys("test@example.com");
|
||||
countryInput.SelectByValue("Mordor");
|
||||
descriptionInput.SendKeys("Fragile staff");
|
||||
streetInput.SendKeys("Mount Doom\t");
|
||||
|
||||
submitButton.Click();
|
||||
|
||||
// Verify member validation from IValidatableObject on a model property, CustomValidationAttribute on a model attribute, and BlazorCompareAttribute.
|
||||
Browser.Equal("A ZipCode is required", () => appElement.FindElement(By.CssSelector(".zip .validation-message")).Text);
|
||||
Browser.Equal("'Confirm email address' and 'EmailAddress' do not match.", () => appElement.FindElement(By.CssSelector(".confirm-email .validation-message")).Text);
|
||||
Browser.Equal("Fragile items must be placed in secure containers", () => appElement.FindElement(By.CssSelector(".item-error .validation-message")).Text);
|
||||
Browser.Equal(3, () => appElement.FindElements(By.CssSelector(".all-errors .validation-message")).Count);
|
||||
|
||||
zipInput.SendKeys("98052");
|
||||
confirmEmailInput.SendKeys("test@example.com");
|
||||
descriptionInput.Clear();
|
||||
weightInput.SendKeys("0");
|
||||
descriptionInput.SendKeys("The One Ring\t");
|
||||
|
||||
submitButton.Click();
|
||||
// Verify validation from IValidatableObject on the model.
|
||||
Browser.Equal("Some items in your list cannot be delivered.", () => appElement.FindElement(By.CssSelector(".model-errors .validation-message")).Text);
|
||||
|
||||
Browser.Single(() => appElement.FindElements(By.CssSelector(".all-errors .validation-message")));
|
||||
|
||||
// Let's make sure the form submits
|
||||
descriptionInput.Clear();
|
||||
descriptionInput.SendKeys("A different ring\t");
|
||||
submitButton.Click();
|
||||
|
||||
Browser.Empty(() => appElement.FindElements(By.CssSelector(".all-errors .validation-message")));
|
||||
Browser.Equal("OnValidSubmit", () => appElement.FindElement(By.CssSelector(".submission-log")).Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
<Reference Include="Microsoft.AspNetCore.Blazor" />
|
||||
<Reference Include="Microsoft.AspNetCore.Blazor.HttpClient" />
|
||||
<Reference Include="Microsoft.AspNetCore.Components.Authorization" />
|
||||
<Reference Include="Microsoft.AspNetCore.Blazor.DataAnnotations.Validation" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,185 @@
|
|||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
|
||||
<p>
|
||||
This component is used to verify the use of the experimental ObjectGraphDataAnnotationsValidator type with IValidatableObject and deep validation, as well
|
||||
as the ComparePropertyAttribute.
|
||||
</p>
|
||||
|
||||
<EditForm Model="@model" OnValidSubmit="@HandleValidSubmit">
|
||||
<ObjectGraphDataAnnotationsValidator />
|
||||
|
||||
<p class="name">
|
||||
Name: <InputText @bind-Value="model.Recipient" placeholder="Enter the recipient" />
|
||||
<ValidationMessage For="@(() => model.Recipient)" />
|
||||
</p>
|
||||
|
||||
<p class="email">
|
||||
Email: <InputText @bind-Value="model.EmailAddress" />
|
||||
<ValidationMessage For="@(() => model.EmailAddress)" />
|
||||
</p>
|
||||
|
||||
<p class="confirm-email">
|
||||
Confirm Email: <InputText @bind-Value="model.ConfirmEmailAddress" />
|
||||
<ValidationMessage For="@(() => model.ConfirmEmailAddress)" />
|
||||
</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Items to deliver</legend>
|
||||
<p>
|
||||
<button id="addItem" type="button" @onclick="AddItem">Add Item</button>
|
||||
</p>
|
||||
<ul class="items">
|
||||
@foreach (var item in model.Items)
|
||||
{
|
||||
<li>
|
||||
<div style="display: inline-flex; flex-direction: row">
|
||||
<div style="flex-grow: 1" class="description">
|
||||
<InputText @bind-Value="item.Description" placeholder="Description" />
|
||||
<ValidationMessage For="@(() => item.Description)" />
|
||||
</div>
|
||||
|
||||
<div style="flex-grow: 1" class="weight">
|
||||
<InputNumber @bind-Value="item.Weight" />
|
||||
<ValidationMessage For="@(() => item.Weight)" />
|
||||
</div>
|
||||
<div style="flex-grow: 1" class="item-error">
|
||||
<ValidationSummary Model="item" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Shipping details</legend>
|
||||
<p class="street">
|
||||
Street Address: <InputText @bind-Value="model.Address.Street" />
|
||||
<ValidationMessage For="@(() => model.Address.Street)" />
|
||||
</p>
|
||||
<p class="zip">
|
||||
Zip Code: <InputText @bind-Value="model.Address.ZipCode" />
|
||||
<ValidationMessage For="@(() => model.Address.ZipCode)" />
|
||||
</p>
|
||||
<p class="country">
|
||||
Country:
|
||||
<InputSelect @bind-Value="model.Address.Country">
|
||||
<option></option>
|
||||
<option value="@Country.Gondor">@Country.Gondor</option>
|
||||
<option value="@Country.Mordor">@Country.Mordor</option>
|
||||
<option value="@Country.Rohan">@Country.Rohan</option>
|
||||
<option value="@Country.Shire">@Country.Shire</option>
|
||||
</InputSelect>
|
||||
<ValidationMessage For="@(() => model.Address.Country)" />
|
||||
</p>
|
||||
<p class="address-validation">
|
||||
<ValidationSummary Model="model.Address" />
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<div class="model-errors">
|
||||
<ValidationSummary Model="model"/>
|
||||
</div>
|
||||
<button type="submit">Submit</button>
|
||||
|
||||
<div class="all-errors">
|
||||
<ValidationSummary />
|
||||
</div>
|
||||
|
||||
</EditForm>
|
||||
|
||||
<ul class="submission-log">
|
||||
@foreach (var entry in submissionLog)
|
||||
{
|
||||
<li>@entry</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@code {
|
||||
Delivery model = new Delivery();
|
||||
|
||||
public class Delivery : IValidatableObject
|
||||
{
|
||||
[Required(ErrorMessage = "Enter a name")]
|
||||
public string Recipient { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Enter an email")]
|
||||
[EmailAddress(ErrorMessage = "Enter a valid email address")]
|
||||
public string EmailAddress { get; set; }
|
||||
|
||||
[CompareProperty(nameof(EmailAddress))]
|
||||
[Display(Name = "Confirm email address")]
|
||||
public string ConfirmEmailAddress { get; set; }
|
||||
|
||||
[ValidateComplexType]
|
||||
public Address Address { get; } = new Address();
|
||||
|
||||
[ValidateComplexType]
|
||||
public List<Item> Items { get; } = new List<Item>
|
||||
{
|
||||
new Item(),
|
||||
};
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext context)
|
||||
{
|
||||
if (Address.Street == "Mount Doom" && Items.Any(i => i.Description == "The One Ring"))
|
||||
{
|
||||
yield return new ValidationResult("Some items in your list cannot be delivered.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Address : IValidatableObject
|
||||
{
|
||||
[Required(ErrorMessage = "A street address is required.")]
|
||||
public string Street { get; set; }
|
||||
|
||||
public string ZipCode { get; set; }
|
||||
|
||||
[EnumDataType(typeof(Country))]
|
||||
public Country Country { get; set; }
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext context)
|
||||
{
|
||||
if (Country == Country.Mordor && string.IsNullOrEmpty(ZipCode))
|
||||
{
|
||||
yield return new ValidationResult("A ZipCode is required", new[] { nameof(ZipCode) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[CustomValidation(typeof(Item), nameof(Item.CustomValidate))]
|
||||
public class Item
|
||||
{
|
||||
[Required(ErrorMessage = "Description is required.")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Range(0.1, 50, ErrorMessage = "Items must weigh between 0.1 and 5")]
|
||||
public double Weight { get; set; } = 1;
|
||||
|
||||
public static ValidationResult CustomValidate(Item item, ValidationContext context)
|
||||
{
|
||||
if (item.Weight < 2.0 && item.Description.StartsWith("Fragile"))
|
||||
{
|
||||
return new ValidationResult("Fragile items must be placed in secure containers");
|
||||
}
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Country { Gondor, Mordor, Rohan, Shire }
|
||||
|
||||
List<string> submissionLog = new List<string>();
|
||||
|
||||
void HandleValidSubmit()
|
||||
{
|
||||
submissionLog.Add("OnValidSubmit");
|
||||
}
|
||||
|
||||
void AddItem()
|
||||
{
|
||||
model.Items.Add(new Item());
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,14 @@
|
|||
@using Microsoft.AspNetCore.Components.Forms
|
||||
|
||||
<EditForm Model="@this" OnValidSubmit="@HandleValidSubmit" OnInvalidSubmit="@HandleInvalidSubmit" autocomplete="off">
|
||||
<DataAnnotationsValidator />
|
||||
@if (UseExperimentalValidator)
|
||||
{
|
||||
<ObjectGraphDataAnnotationsValidator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<DataAnnotationsValidator />
|
||||
}
|
||||
|
||||
<p class="user-name">
|
||||
User name: <input @bind="UserName" class="@context.FieldCssClass(() => UserName)" />
|
||||
|
|
@ -29,6 +36,8 @@
|
|||
}
|
||||
|
||||
@code {
|
||||
protected virtual bool UseExperimentalValidator => false;
|
||||
|
||||
string lastCallback;
|
||||
|
||||
[Required(ErrorMessage = "Please choose a username")]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
namespace BasicTestApp.FormsTest
|
||||
{
|
||||
public class TypicalValidationComponentUsingExperimentalValidator : TypicalValidationComponent
|
||||
{
|
||||
protected override bool UseExperimentalValidator => true;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,14 @@
|
|||
@using Microsoft.AspNetCore.Components.Forms
|
||||
|
||||
<EditForm EditContext="@editContext" OnValidSubmit="@HandleValidSubmit">
|
||||
@if (UseExperimentalValidator)
|
||||
{
|
||||
<ObjectGraphDataAnnotationsValidator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<DataAnnotationsValidator />
|
||||
}
|
||||
|
||||
<p class="name">
|
||||
Name: <InputText @bind-Value="person.Name" placeholder="Enter your name" />
|
||||
|
|
@ -11,6 +18,10 @@
|
|||
Email: <InputText @bind-Value="person.Email" />
|
||||
<ValidationMessage For="@(() => person.Email)" />
|
||||
</p>
|
||||
<p class="confirm-email">
|
||||
Email: <InputText @bind-Value="person.ConfirmEmail" />
|
||||
<ValidationMessage For="@(() => person.ConfirmEmail)" />
|
||||
</p>
|
||||
<p class="age">
|
||||
Age (years): <InputNumber @bind-Value="person.AgeInYears" placeholder="Enter your age" />
|
||||
</p>
|
||||
|
|
@ -49,12 +60,18 @@
|
|||
|
||||
<button type="submit">Submit</button>
|
||||
|
||||
<p class="model-errors">
|
||||
<ValidationSummary Model="person" />
|
||||
</p>
|
||||
|
||||
<ValidationSummary />
|
||||
</EditForm>
|
||||
|
||||
<ul>@foreach (var entry in submissionLog) { <li>@entry</li> }</ul>
|
||||
|
||||
@code {
|
||||
protected virtual bool UseExperimentalValidator => false;
|
||||
|
||||
Person person = new Person();
|
||||
EditContext editContext;
|
||||
ValidationMessageStore customValidationMessageStore;
|
||||
|
|
@ -75,6 +92,9 @@
|
|||
[StringLength(10, ErrorMessage = "We only accept very short email addresses (max 10 chars)")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Compare(nameof(Email), ErrorMessage = "Email and confirm email do not match.")]
|
||||
public string ConfirmEmail { get; set; }
|
||||
|
||||
[Range(0, 200, ErrorMessage = "Nobody is that old")]
|
||||
public int AgeInYears { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
namespace BasicTestApp.FormsTest
|
||||
{
|
||||
public class SimpleValidationComponentUsingExperimentalValidator : SimpleValidationComponent
|
||||
{
|
||||
protected override bool UseExperimentalValidator => true;
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,10 @@
|
|||
<option value="BasicTestApp.FocusEventComponent">Focus events</option>
|
||||
<option value="BasicTestApp.FormsTest.NotifyPropertyChangedValidationComponent">INotifyPropertyChanged validation</option>
|
||||
<option value="BasicTestApp.FormsTest.SimpleValidationComponent">Simple validation</option>
|
||||
<option value="BasicTestApp.FormsTest.SimpleValidationComponentUsingExperimentalValidator">Simple validation using experimental validator</option>
|
||||
<option value="BasicTestApp.FormsTest.TypicalValidationComponent">Typical validation</option>
|
||||
<option value="BasicTestApp.FormsTest.TypicalValidationComponentUsingExperimentalValidator">Typical validation using experimental validator</option>
|
||||
<option value="BasicTestApp.FormsTest.ExperimentalValidationComponent">Experimental validation</option>
|
||||
<option value="BasicTestApp.GlobalizationBindCases">Globalization Bind Cases</option>
|
||||
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
|
||||
<option value="BasicTestApp.HtmlBlockChildContent">ChildContent HTML Block</option>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace OpenQA.Selenium
|
||||
|
|
@ -13,15 +14,31 @@ namespace OpenQA.Selenium
|
|||
// case.
|
||||
public class BrowserAssertFailedException : XunitException
|
||||
{
|
||||
public BrowserAssertFailedException(IReadOnlyList<LogEntry> logs, Exception innerException, string screenShotPath)
|
||||
: base(BuildMessage(innerException, logs, screenShotPath), innerException)
|
||||
public BrowserAssertFailedException(IReadOnlyList<LogEntry> logs, Exception innerException, string screenShotPath, string innerHTML)
|
||||
: base(BuildMessage(innerException, logs, screenShotPath, innerHTML), innerException)
|
||||
{
|
||||
}
|
||||
|
||||
private static string BuildMessage(Exception innerException, IReadOnlyList<LogEntry> logs, string screenShotPath) =>
|
||||
innerException.ToString() + Environment.NewLine +
|
||||
(File.Exists(screenShotPath) ? $"Screen shot captured at '{screenShotPath}'" + Environment.NewLine : "") +
|
||||
(logs.Count > 0 ? "Encountered browser logs" : "No browser logs found") + " while running the assertion." + Environment.NewLine +
|
||||
string.Join(Environment.NewLine, logs);
|
||||
private static string BuildMessage(Exception exception, IReadOnlyList<LogEntry> logs, string screenShotPath, string innerHTML)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine(exception.ToString());
|
||||
|
||||
if (File.Exists(screenShotPath))
|
||||
{
|
||||
builder.AppendLine($"Screen shot captured at '{screenShotPath}'");
|
||||
}
|
||||
|
||||
if (logs.Count > 0)
|
||||
{
|
||||
builder.AppendLine("Encountered browser errors")
|
||||
.AppendJoin(Environment.NewLine, logs);
|
||||
}
|
||||
|
||||
builder.AppendLine("Page content:")
|
||||
.AppendLine(innerHTML);
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ namespace Microsoft.AspNetCore.E2ETesting
|
|||
// tests running concurrently might use the DefaultTimeout in their current assertion, which is fine.
|
||||
TestRunFailed = true;
|
||||
|
||||
var innerHtml = driver.FindElement(By.CssSelector(":first-child")).GetAttribute("innerHTML");
|
||||
|
||||
var fileId = $"{Guid.NewGuid():N}.png";
|
||||
var screenShotPath = Path.Combine(Path.GetFullPath(E2ETestOptions.Instance.ScreenShotsPath), fileId);
|
||||
var errors = driver.GetBrowserLogs(LogLevel.All);
|
||||
|
|
@ -109,7 +111,7 @@ namespace Microsoft.AspNetCore.E2ETesting
|
|||
var exceptionInfo = lastException != null ? ExceptionDispatchInfo.Capture(lastException) :
|
||||
CaptureException(() => assertion());
|
||||
|
||||
throw new BrowserAssertFailedException(errors, exceptionInfo.SourceException, screenShotPath);
|
||||
throw new BrowserAssertFailedException(errors, exceptionInfo.SourceException, screenShotPath, innerHtml);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
Loading…
Reference in New Issue