File: Forms\InputBaseClientValidationTest.cs
Web Access
Project: src\src\Components\Web\test\Microsoft.AspNetCore.Components.Web.Tests.csproj (Microsoft.AspNetCore.Components.Web.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.Components.Forms.ClientValidation;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
 
namespace Microsoft.AspNetCore.Components.Forms;
 
public class InputBaseClientValidationTest
{
    private readonly TestRenderer _testRenderer = new TestRenderer();
 
    [Fact]
    public async Task InputBase_RendersDataValAttributes_WhenClientValidationServiceRegistered()
    {
        var model = new TestModel();
        var editContext = new EditContext(model);
        editContext.Properties[typeof(IClientValidationService)] = new TestClientValidationService(
            new Dictionary<string, object>
            {
                ["data-val"] = "true",
                ["data-val-required"] = "Generated required message.",
            });
 
        var rootComponent = new TestInputHostComponent<string, InputText>
        {
            EditContext = editContext,
            ValueExpression = () => model.Value,
        };
 
        var componentId = await RenderAndGetInputComponentIdAsync(rootComponent);
        var frames = _testRenderer.GetCurrentRenderTreeFrames(componentId);
 
        AssertHasAttribute(frames, "data-val", "true");
        AssertHasAttribute(frames, "data-val-required", "Generated required message.");
    }
 
    [Fact]
    public async Task InputBase_DoesNotRenderDataValAttributes_WhenServiceNotRegistered()
    {
        var model = new TestModel();
        var editContext = new EditContext(model); // No IClientValidationService registered
 
        var rootComponent = new TestInputHostComponent<string, InputText>
        {
            EditContext = editContext,
            ValueExpression = () => model.Value,
        };
 
        var componentId = await RenderAndGetInputComponentIdAsync(rootComponent);
        var frames = _testRenderer.GetCurrentRenderTreeFrames(componentId);
 
        AssertNoAttribute(frames, "data-val");
    }
 
    [Fact]
    public async Task InputBase_DeveloperAdditionalAttributes_TakePrecedenceOverGenerated()
    {
        var model = new TestModel();
        var editContext = new EditContext(model);
        editContext.Properties[typeof(IClientValidationService)] = new TestClientValidationService(
            new Dictionary<string, object>
            {
                ["data-val"] = "true",
                ["data-val-required"] = "Generated message",
            });
 
        var rootComponent = new TestInputHostComponent<string, InputText>
        {
            EditContext = editContext,
            ValueExpression = () => model.Value,
            AdditionalAttributes = new Dictionary<string, object>
            {
                ["data-val-required"] = "Custom developer message",
            },
        };
 
        var componentId = await RenderAndGetInputComponentIdAsync(rootComponent);
        var frames = _testRenderer.GetCurrentRenderTreeFrames(componentId);
 
        // Developer-specified attribute must win (first-wins merging in InputBase)
        AssertHasAttribute(frames, "data-val-required", "Custom developer message");
        // Generated data-val=true should still be present (no developer override)
        AssertHasAttribute(frames, "data-val", "true");
    }
 
    [Fact]
    public async Task InputBase_NoDataValAttributes_WhenServiceReturnsEmpty()
    {
        var model = new TestModel();
        var editContext = new EditContext(model);
        editContext.Properties[typeof(IClientValidationService)] = new TestClientValidationService(
            new Dictionary<string, object>());
 
        var rootComponent = new TestInputHostComponent<string, InputText>
        {
            EditContext = editContext,
            ValueExpression = () => model.Value,
        };
 
        var componentId = await RenderAndGetInputComponentIdAsync(rootComponent);
        var frames = _testRenderer.GetCurrentRenderTreeFrames(componentId);
 
        AssertNoAttribute(frames, "data-val");
    }
 
    // Helpers
 
    private async Task<int> RenderAndGetInputComponentIdAsync(TestInputHostComponent<string, InputText> hostComponent)
    {
        var hostComponentId = _testRenderer.AssignRootComponentId(hostComponent);
        await _testRenderer.RenderRootComponentAsync(hostComponentId);
        var batch = _testRenderer.Batches.Single();
        return batch.GetComponentFrames<InputText>().Single().ComponentId;
    }
 
    private static void AssertHasAttribute(ArrayRange<RenderTreeFrame> frames, string attributeName, object expectedValue)
    {
        var match = frames.Array.Take(frames.Count).FirstOrDefault(f =>
            f.FrameType == RenderTreeFrameType.Attribute && f.AttributeName == attributeName);
 
        Assert.True(match.FrameType == RenderTreeFrameType.Attribute,
            $"Expected attribute '{attributeName}' was not found in rendered output.");
        Assert.Equal(expectedValue, match.AttributeValue);
    }
 
    private static void AssertNoAttribute(ArrayRange<RenderTreeFrame> frames, string attributeName)
    {
        Assert.DoesNotContain(frames.Array.Take(frames.Count), f =>
            f.FrameType == RenderTreeFrameType.Attribute && f.AttributeName == attributeName);
    }
 
    /// <summary>
    /// Test implementation of IClientValidationService that returns a fixed set of attributes
    /// (or null) regardless of which field is requested. Lets us test InputBase's merge behavior
    /// without depending on the internal DefaultClientValidationService.
    /// </summary>
    private sealed class TestClientValidationService : IClientValidationService
    {
        private readonly IReadOnlyDictionary<string, object> _attributes;
 
        public TestClientValidationService(IReadOnlyDictionary<string, object> attributes)
        {
            _attributes = attributes;
        }
 
        public IReadOnlyDictionary<string, object> GetClientValidationAttributes(FieldIdentifier fieldIdentifier)
            => _attributes;
    }
 
    private class TestModel
    {
        public string Value { get; set; } = "";
    }
}