File: Forms\DisplayNameTest.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 Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Test.Helpers;
 
namespace Microsoft.AspNetCore.Components.Forms;
 
public class DisplayNameTest
{
    [Fact]
    public async Task ThrowsIfNoForParameterProvided()
    {
        // Arrange
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<DisplayName<string>>(0);
                builder.CloseComponent();
            }
        };
 
        var testRenderer = new TestRenderer();
        var componentId = testRenderer.AssignRootComponentId(rootComponent);
 
        // Act & Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            async () => await testRenderer.RenderRootComponentAsync(componentId));
        Assert.Contains("For", ex.Message);
        Assert.Contains("parameter", ex.Message);
    }
 
    [Fact]
    public async Task DisplaysPropertyNameWhenNoAttributePresent()
    {
        // Arrange
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<DisplayName<string>>(0);
                builder.AddComponentParameter(1, "For", (System.Linq.Expressions.Expression<Func<string>>)(() => model.PlainProperty));
                builder.CloseComponent();
            }
        };
 
        // Act
        var output = await RenderAndGetOutput(rootComponent);
 
        // Assert
        Assert.Equal("PlainProperty", output);
    }
 
    [Fact]
    public async Task DisplaysDisplayAttributeName()
    {
        // Arrange
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<DisplayName<string>>(0);
                builder.AddComponentParameter(1, "For", (System.Linq.Expressions.Expression<Func<string>>)(() => model.PropertyWithDisplayAttribute));
                builder.CloseComponent();
            }
        };
 
        // Act
        var output = await RenderAndGetOutput(rootComponent);
 
        // Assert
        Assert.Equal("Custom Display Name", output);
    }
 
    [Fact]
    public async Task DisplaysDisplayNameAttributeName()
    {
        // Arrange
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<DisplayName<string>>(0);
                builder.AddComponentParameter(1, "For", (System.Linq.Expressions.Expression<Func<string>>)(() => model.PropertyWithDisplayNameAttribute));
                builder.CloseComponent();
            }
        };
 
        // Act
        var output = await RenderAndGetOutput(rootComponent);
 
        // Assert
        Assert.Equal("Custom DisplayName", output);
    }
 
    [Fact]
    public async Task DisplayAttributeTakesPrecedenceOverDisplayNameAttribute()
    {
        // Arrange
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<DisplayName<string>>(0);
                builder.AddComponentParameter(1, "For", (System.Linq.Expressions.Expression<Func<string>>)(() => model.PropertyWithBothAttributes));
                builder.CloseComponent();
            }
        };
 
        // Act
        var output = await RenderAndGetOutput(rootComponent);
 
        // Assert
        // DisplayAttribute should take precedence per MVC conventions
        Assert.Equal("Display Takes Precedence", output);
    }
 
    [Fact]
    public async Task WorksWithDifferentPropertyTypes()
    {
        // Arrange
        var model = new TestModel();
        var intComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<DisplayName<int>>(0);
                builder.AddComponentParameter(1, "For", (System.Linq.Expressions.Expression<Func<int>>)(() => model.IntProperty));
                builder.CloseComponent();
            }
        };
        var dateComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<DisplayName<DateTime>>(0);
                builder.AddComponentParameter(1, "For", (System.Linq.Expressions.Expression<Func<DateTime>>)(() => model.DateProperty));
                builder.CloseComponent();
            }
        };
 
        // Act
        var intOutput = await RenderAndGetOutput(intComponent);
        var dateOutput = await RenderAndGetOutput(dateComponent);
 
        // Assert
        Assert.Equal("Integer Value", intOutput);
        Assert.Equal("Date Value", dateOutput);
    }
 
    [Fact]
    public async Task SupportsLocalizationWithResourceType()
    {
        var model = new TestModel();
        var rootComponent = new TestHostComponent
        {
            InnerContent = builder =>
            {
                builder.OpenComponent<DisplayName<string>>(0);
                builder.AddComponentParameter(1, "For", (System.Linq.Expressions.Expression<Func<string>>)(() => model.PropertyWithResourceBasedDisplay));
                builder.CloseComponent();
            }
        };
 
        var output = await RenderAndGetOutput(rootComponent);
        Assert.Equal("Localized Display Name", output);
    }
 
    private static async Task<string> RenderAndGetOutput(TestHostComponent rootComponent)
    {
        var testRenderer = new TestRenderer();
        var componentId = testRenderer.AssignRootComponentId(rootComponent);
        await testRenderer.RenderRootComponentAsync(componentId);
 
        var batch = testRenderer.Batches.Single();
        var displayLabelComponentFrame = batch.ReferenceFrames
            .First(f => f.FrameType == RenderTree.RenderTreeFrameType.Component &&
                       f.Component is DisplayName<string> or DisplayName<int> or DisplayName<DateTime>);
 
        // Find the text content frame within the component
        var textFrame = batch.ReferenceFrames
            .First(f => f.FrameType == RenderTree.RenderTreeFrameType.Text);
 
        return textFrame.TextContent;
    }
 
    private class TestHostComponent : ComponentBase
    {
        public RenderFragment InnerContent { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            InnerContent(builder);
        }
    }
 
    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;
    }
 
    public static class TestResources
    {
        public static string LocalizedDisplayName => "Localized Display Name";
    }
}