File: Forms\EditFormTest.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.Mapping;
using Microsoft.AspNetCore.Components.Infrastructure;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.Extensions.DependencyInjection;
 
namespace Microsoft.AspNetCore.Components.Forms;
 
public class EditFormTest
{
    private TestRenderer _testRenderer = new();
 
    public EditFormTest()
    {
        var services = new ServiceCollection();
        services.AddSingleton<IFormValueMapper, TestFormValueModelBinder>();
        services.AddAntiforgery();
        services.AddLogging();
        services.AddSingleton<ComponentStatePersistenceManager>();
        services.AddSingleton(services => services.GetRequiredService<ComponentStatePersistenceManager>().State);
        services.AddSingleton<AntiforgeryStateProvider, DefaultAntiforgeryStateProvider>();
        _testRenderer = new(services.BuildServiceProvider());
    }
 
    [Fact]
    public async Task ThrowsIfBothEditContextAndModelAreSupplied()
    {
        // Arrange
        var editForm = new EditForm
        {
            EditContext = new EditContext(new TestModel()),
            Model = new TestModel()
        };
        var testRenderer = new TestRenderer();
        var componentId = testRenderer.AssignRootComponentId(editForm);
 
        // Act/Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => testRenderer.RenderRootComponentAsync(componentId));
        Assert.StartsWith($"{nameof(EditForm)} requires a {nameof(EditForm.Model)} parameter, or an {nameof(EditContext)} parameter, but not both.", ex.Message);
    }
 
    [Fact]
    public async Task ThrowsIfBothEditContextAndModelAreNull()
    {
        // Arrange
        var editForm = new EditForm();
        var testRenderer = new TestRenderer();
        var componentId = testRenderer.AssignRootComponentId(editForm);
 
        // Act/Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => testRenderer.RenderRootComponentAsync(componentId));
        Assert.StartsWith($"{nameof(EditForm)} requires either a {nameof(EditForm.Model)} parameter, or an {nameof(EditContext)} parameter, please provide one of these.", ex.Message);
    }
 
    [Fact]
    public async Task ReturnsEditContextWhenModelParameterUsed()
    {
        // Arrange
        var model = new TestModel();
        var rootComponent = new TestEditFormHostComponent
        {
            Model = model
        };
        var editFormComponent = await RenderAndGetTestEditFormComponentAsync(rootComponent);
 
        // Act
        var returnedEditContext = editFormComponent.EditContext;
 
        // Assert
        Assert.NotNull(returnedEditContext);
        Assert.Same(model, returnedEditContext.Model);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task ReturnsEditContextWhenEditContextParameterUsed(bool createFieldPath)
    {
        // Arrange
        var editContext = new EditContext(new TestModel()) { ShouldUseFieldIdentifiers = createFieldPath };
        var rootComponent = new TestEditFormHostComponent
        {
            EditContext = editContext
        };
        var editFormComponent = await RenderAndGetTestEditFormComponentAsync(rootComponent);
 
        // Act
        var returnedEditContext = editFormComponent.EditContext;
 
        // Assert
        Assert.Same(editContext, returnedEditContext);
    }
 
    [Fact]
    public async Task DoesNotAddSSRContentWhenNoMappingContextPresent()
    {
        // Arrange
        var model = new TestModel();
        var rootComponent = new TestEditFormHostComponent
        {
            Model = model,
            FormName = "my-form",
        };
 
        // Act
        await RenderAndGetTestEditFormComponentAsync(rootComponent);
        var editFormComponentId = _testRenderer.Batches.Single()
            .GetComponentFrames<EditForm>().Single().ComponentId;
        var editFormFrames = _testRenderer.GetCurrentRenderTreeFrames(editFormComponentId);
 
        // Assert:
        //  - Does not set any "method" attribute
        //  - Does not assign any name to the submit event
        Assert.Collection(editFormFrames.AsEnumerable(),
            frame => AssertFrame.Region(frame, 7),
            frame => AssertFrame.Element(frame, "form", 6),
            frame => AssertFrame.Attribute(frame, "onsubmit"),
            frame => AssertFrame.Component<CascadingValue<EditContext>>(frame, 4),
            frame => AssertFrame.Attribute(frame, "IsFixed", true),
            frame => AssertFrame.Attribute(frame, "Value"),
            frame => AssertFrame.Attribute(frame, "ChildContent"));
    }
 
    [Fact]
    public async Task AddSSRContentWhenMappingContextPresent()
    {
        // Arrange
        var editContext = new EditContext(new object());
        var rootComponent = new TestEditFormHostComponent
        {
            FormName = "my-form",
            MappingContextName = "mapping-context-name",
            EditContext = editContext,
        };
 
        // Act
        await RenderAndGetTestEditFormComponentAsync(rootComponent);
        var editFormComponentId = _testRenderer.Batches.Single()
            .GetComponentFrames<EditForm>().Single().ComponentId;
        var editFormFrames = _testRenderer.GetCurrentRenderTreeFrames(editFormComponentId);
 
        // Assert
        Assert.Collection(editFormFrames.AsEnumerable(),
            frame => AssertFrame.Region(frame, 13),
            frame => AssertFrame.Element(frame, "form", 12),
 
            // Sets "method" to "post" by default
            frame => AssertFrame.Attribute(frame, "method", "post"),
 
            // Assigns name to the submit event
            frame => AssertFrame.Attribute(frame, "onsubmit"),
            frame => AssertFrame.NamedEvent(frame, "onsubmit", "my-form"),
 
            frame => AssertFrame.Region(frame, 4),
 
            // Adds FormMappingValidator child
            frame => AssertFrame.Component<FormMappingValidator>(frame, 2),
            frame => AssertFrame.Attribute(frame, nameof(FormMappingValidator.CurrentEditContext), editContext),
 
            // Adds AntiforgeryToken child
            frame => AssertFrame.Component<AntiforgeryToken>(frame, 1),
 
            frame => AssertFrame.Component<CascadingValue<EditContext>>(frame, 4),
            frame => AssertFrame.Attribute(frame, "IsFixed", true),
            frame => AssertFrame.Attribute(frame, "Value"),
            frame => AssertFrame.Attribute(frame, "ChildContent"));
    }
 
    [Fact]
    public async Task CanOverrideMethodWhenMappingContextPresent()
    {
        // Arrange
        var editContext = new EditContext(new object());
        var rootComponent = new TestEditFormHostComponent
        {
            FormName = "my-form",
            MappingContextName = "mapping-context-name",
            EditContext = editContext,
            AdditionalFormAttributes = new Dictionary<string, object>
            {
                { "method", "my method" },
                { "custom attribute", "some value" },
            },
        };
 
        // Act
        await RenderAndGetTestEditFormComponentAsync(rootComponent);
        var editFormComponentId = _testRenderer.Batches.Single()
            .GetComponentFrames<EditForm>().Single().ComponentId;
        var editFormFrames = _testRenderer.GetCurrentRenderTreeFrames(editFormComponentId);
        var editFormAttributes = editFormFrames.AsEnumerable()
            .SkipWhile(f => f.FrameType != RenderTreeFrameType.Attribute)
            .TakeWhile(f => f.FrameType == RenderTreeFrameType.Attribute)
            .ToDictionary(f => f.AttributeName, f => f.AttributeValue);
 
        // Assert
        Assert.Equal("my method", editFormAttributes["method"]);
        Assert.Equal("some value", editFormAttributes["custom attribute"]);
    }
 
    private static EditForm FindEditFormComponent(CapturedBatch batch)
        => batch.ReferenceFrames
                .Where(f => f.FrameType == RenderTreeFrameType.Component)
                .Select(f => f.Component)
                .OfType<EditForm>()
                .Single();
 
    private async Task<EditForm> RenderAndGetTestEditFormComponentAsync(TestEditFormHostComponent hostComponent)
    {
        var componentId = _testRenderer.AssignRootComponentId(hostComponent);
        await _testRenderer.RenderRootComponentAsync(componentId);
        return FindEditFormComponent(_testRenderer.Batches.Single());
    }
 
    class TestModel
    {
        public string StringProperty { get; set; }
    }
 
    class TestEditFormHostComponent : AutoRenderComponent
    {
        public EditContext EditContext { get; set; }
 
        public TestModel Model { get; set; }
 
        public string MappingContextName { get; set; }
 
        public Action<EditContext> SubmitHandler { get; set; }
 
        public string FormName { get; set; }
 
        public Dictionary<string, object> AdditionalFormAttributes { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            if (MappingContextName is not null)
            {
                builder.OpenComponent<FormMappingScope>(0);
                builder.AddComponentParameter(1, nameof(FormMappingScope.Name), MappingContextName);
                builder.AddComponentParameter(3, nameof(FormMappingScope.ChildContent), (RenderFragment<FormMappingContext>)(_ => RenderForm));
                builder.CloseComponent();
            }
            else
            {
                RenderForm(builder);
            }
 
            void RenderForm(RenderTreeBuilder builder)
            {
                builder.OpenComponent<EditForm>(0);
                // Order here is intentional to make sure that the test fails if we
                // accidentally capture a parameter in a named property.
                builder.AddMultipleAttributes(1, AdditionalFormAttributes);
 
                builder.AddComponentParameter(2, "Model", Model);
                builder.AddComponentParameter(3, "EditContext", EditContext);
                if (SubmitHandler != null)
                {
                    builder.AddComponentParameter(4, "OnValidSubmit", new EventCallback<EditContext>(null, SubmitHandler));
                }
                builder.AddComponentParameter(5, "FormName", FormName);
 
                builder.CloseComponent();
            }
        }
    }
 
    private class TestFormValueModelBinder : IFormValueMapper
    {
        public bool CanMap(Type valueType, string mappingScopeName, string formName) => false;
        public void Map(FormValueMappingContext context) { }
    }
}