File: Forms\LabelTest.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 System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
 
namespace Microsoft.AspNetCore.Components.Forms;
 
public class LabelTest
{
    [Fact]
    public async Task RendersLabelElement()
    {
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PlainProperty));
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var labelElement = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Element && f.ElementName == "label");
        Assert.Equal("label", labelElement.ElementName);
    }
 
    [Fact]
    public async Task DisplaysDisplayAttributeNameAsContent()
    {
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PropertyWithDisplayAttribute));
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var textFrame = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Text);
        Assert.Equal("Custom Display Name", textFrame.TextContent);
    }
 
    [Fact]
    public async Task DisplaysPropertyNameWhenNoAttributePresent()
    {
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PlainProperty));
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var textFrame = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Text);
        Assert.Equal("PlainProperty", textFrame.TextContent);
    }
 
    [Fact]
    public async Task DisplaysDisplayNameAttributeName()
    {
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PropertyWithDisplayNameAttribute));
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var textFrame = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Text);
        Assert.Equal("Custom DisplayName", textFrame.TextContent);
    }
 
    [Fact]
    public async Task DisplayAttributeTakesPrecedenceOverDisplayNameAttribute()
    {
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PropertyWithBothAttributes));
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var textFrame = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Text);
        Assert.Equal("Display Takes Precedence", textFrame.TextContent);
    }
 
    [Fact]
    public async Task AppliesAdditionalAttributes()
    {
        var model = new TestModel();
        var additionalAttributes = new Dictionary<string, object>
        {
            { "class", "form-label" },
            { "data-testid", "my-label" }
        };
 
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PlainProperty));
                builder.AddComponentParameter(2, "AdditionalAttributes", additionalAttributes);
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var classAttribute = frames.FirstOrDefault(f => f.FrameType == RenderTree.RenderTreeFrameType.Attribute && f.AttributeName == "class");
        Assert.NotNull(classAttribute.AttributeName);
        Assert.Equal("form-label", classAttribute.AttributeValue);
 
        var dataAttribute = frames.FirstOrDefault(f => f.FrameType == RenderTree.RenderTreeFrameType.Attribute && f.AttributeName == "data-testid");
        Assert.NotNull(dataAttribute.AttributeName);
        Assert.Equal("my-label", dataAttribute.AttributeValue);
    }
 
    [Fact]
    public async Task RendersChildContent()
    {
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PlainProperty));
                builder.AddComponentParameter(2, "ChildContent", (RenderFragment)(childBuilder =>
                {
                    childBuilder.OpenElement(0, "input");
                    childBuilder.AddAttribute(1, "type", "text");
                    childBuilder.CloseElement();
                }));
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var labelElement = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Element && f.ElementName == "label");
        Assert.Equal("label", labelElement.ElementName);
 
        var inputElement = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Element && f.ElementName == "input");
        Assert.Equal("input", inputElement.ElementName);
 
        var textFrame = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Text);
        Assert.Equal("PlainProperty", textFrame.TextContent);
    }
 
    [Fact]
    public async Task WorksWithDifferentPropertyTypes()
    {
        var model = new TestModel();
        var intComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<int>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<int>>)(() => model.IntProperty));
                builder.CloseComponent();
            }
        };
        var dateComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<DateTime>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<DateTime>>)(() => model.DateProperty));
                builder.CloseComponent();
            }
        };
 
        var intFrames = await RenderAndGetFrames(intComponent);
        var dateFrames = await RenderAndGetFrames(dateComponent);
 
        var intText = intFrames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Text);
        var dateText = dateFrames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Text);
        Assert.Equal("Integer Value", intText.TextContent);
        Assert.Equal("Date Value", dateText.TextContent);
    }
 
    [Fact]
    public async Task ThrowsWhenForIsNull()
    {
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                // Not setting For parameter
                builder.CloseComponent();
            }
        };
 
        var testRenderer = new TestRenderer();
        var componentId = testRenderer.AssignRootComponentId(rootComponent);
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(
            () => testRenderer.RenderRootComponentAsync(componentId));
 
        Assert.Contains("For", exception.Message);
    }
 
    [Fact]
    public async Task AllowsForAttributeInAdditionalAttributes()
    {
        var model = new TestModel();
        var additionalAttributes = new Dictionary<string, object>
        {
            { "for", "some-id" }
        };
 
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PlainProperty));
                builder.AddComponentParameter(2, "AdditionalAttributes", additionalAttributes);
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var labelElement = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Element && f.ElementName == "label");
        Assert.Equal("label", labelElement.ElementName);
 
        var forAttribute = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Attribute && f.AttributeName == "for");
        Assert.Equal("some-id", forAttribute.AttributeValue);
    }
 
    [Fact]
    public async Task RendersWithoutChildContent()
    {
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PlainProperty));
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var labelElement = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Element && f.ElementName == "label");
        Assert.Equal("label", labelElement.ElementName);
 
        var textFrame = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Text);
        Assert.Equal("PlainProperty", textFrame.TextContent);
    }
 
    [Fact]
    public async Task RendersForAttributeWhenNoChildContent()
    {
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PlainProperty));
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var forAttribute = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Attribute && f.AttributeName == "for");
        Assert.Equal("model_PlainProperty", forAttribute.AttributeValue);
    }
 
    [Fact]
    public async Task DoesNotRenderForAttributeWhenChildContentProvided()
    {
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PlainProperty));
                builder.AddComponentParameter(2, "ChildContent", (RenderFragment)(childBuilder =>
                {
                    childBuilder.AddContent(0, "Input goes here");
                }));
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var forAttributes = frames.Where(f => f.FrameType == RenderTree.RenderTreeFrameType.Attribute && f.AttributeName == "for");
        Assert.Empty(forAttributes);
    }
 
    [Fact]
    public async Task NonNestedLabel_ExplicitForOverridesGenerated()
    {
        var model = new TestModel();
        var additionalAttributes = new Dictionary<string, object>
        {
            { "for", "custom-input-id" }
        };
 
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PlainProperty));
                builder.AddComponentParameter(2, "AdditionalAttributes", additionalAttributes);
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var forAttribute = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Attribute && f.AttributeName == "for");
        Assert.Equal("custom-input-id", forAttribute.AttributeValue);
    }
 
    [Fact]
    public async Task WorksWithNestedProperties()
    {
        var model = new TestModelWithNestedProperty();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.Address.Street));
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var textFrame = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Text);
        Assert.Equal("Street Address", textFrame.TextContent);
    }
 
    [Fact]
    public async Task SupportsLocalizationWithResourceType()
    {
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", (Expression<Func<string>>)(() => model.PropertyWithResourceBasedDisplay));
                builder.CloseComponent();
            }
        };
 
        var frames = await RenderAndGetFrames(rootComponent);
 
        var textFrame = frames.First(f => f.FrameType == RenderTree.RenderTreeFrameType.Text);
        Assert.Equal("Localized Display Name", textFrame.TextContent);
    }
 
    [Fact]
    public async Task ReRendersWhenForChangesWithSameDisplayNameButAttributesChange()
    {
        var model = new TestModel();
        Expression<Func<string>> forExpression1 = () => model.PlainProperty;
        Expression<Func<string>> forExpression2 = () => model.PlainProperty;
 
        var attributes1 = new Dictionary<string, object> { { "class", "label-1" } };
        var attributes2 = new Dictionary<string, object> { { "class", "label-2" } };
 
        var currentFor = forExpression1;
        var currentAttributes = attributes1;
 
        var testRenderer = new TestRenderer();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<Label<string>>(0);
                builder.AddComponentParameter(1, "For", currentFor);
                builder.AddComponentParameter(2, "AdditionalAttributes", currentAttributes);
                builder.CloseComponent();
            }
        };
 
        var componentId = testRenderer.AssignRootComponentId(rootComponent);
        await testRenderer.RenderRootComponentAsync(componentId);
 
        // Verify initial render has class="label-1"
        var initialFrames = testRenderer.Batches.Last().ReferenceFrames;
        var initialClassAttr = initialFrames.First(f =>
            f.FrameType == RenderTree.RenderTreeFrameType.Attribute && f.AttributeName == "class");
        Assert.Equal("label-1", initialClassAttr.AttributeValue);
 
        // Change both For (different object, same display name) and AdditionalAttributes
        currentFor = forExpression2;
        currentAttributes = attributes2;
        rootComponent.InnerContent = builder =>
        {
            builder.OpenComponent<Label<string>>(0);
            builder.AddComponentParameter(1, "For", currentFor);
            builder.AddComponentParameter(2, "AdditionalAttributes", currentAttributes);
            builder.CloseComponent();
        };
 
        await testRenderer.Dispatcher.InvokeAsync(() => rootComponent.TriggerRender());
 
        // Should have re-rendered with the new attributes
        var updatedFrames = testRenderer.Batches.Last().ReferenceFrames;
        var updatedClassAttr = updatedFrames.First(f =>
            f.FrameType == RenderTree.RenderTreeFrameType.Attribute && f.AttributeName == "class");
        Assert.Equal("label-2", updatedClassAttr.AttributeValue);
    }
 
    private static async Task<RenderTreeFrame[]> RenderAndGetFrames(TestHostComponent rootComponent)
    {
        var testRenderer = new TestRenderer();
        var componentId = testRenderer.AssignRootComponentId(rootComponent);
        await testRenderer.RenderRootComponentAsync(componentId);
 
        var batch = testRenderer.Batches.Single();
        return batch.ReferenceFrames;
    }
 
    private class TestHostComponent : ComponentBase
    {
        public RenderFragment InnerContent { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            InnerContent(builder);
        }
 
        public void TriggerRender()
        {
            StateHasChanged();
        }
    }
 
    private class TestModel
    {
        public string PlainProperty { get; set; } = string.Empty;
 
        [Display(Name = "Custom Display Name")]
        public string PropertyWithDisplayAttribute { get; set; } = string.Empty;
 
        [DisplayName("Custom DisplayName")]
        public string PropertyWithDisplayNameAttribute { get; set; } = string.Empty;
 
        [Display(Name = "Display Takes Precedence")]
        [DisplayName("This Should Not Be Used")]
        public string PropertyWithBothAttributes { get; set; } = string.Empty;
 
        [Display(Name = "Integer Value")]
        public int IntProperty { get; set; }
 
        [Display(Name = "Date Value")]
        public DateTime DateProperty { get; set; }
 
        [Display(Name = nameof(TestResources.LocalizedDisplayName), ResourceType = typeof(TestResources))]
        public string PropertyWithResourceBasedDisplay { get; set; } = string.Empty;
    }
 
    private class TestModelWithNestedProperty
    {
        public AddressModel Address { get; set; } = new();
    }
 
    private class AddressModel
    {
        [Display(Name = "Street Address")]
        public string Street { get; set; } = string.Empty;
    }
 
    public static class TestResources
    {
        public static string LocalizedDisplayName => "Localized Display Name";
    }
}