File: LayoutViewTest.cs
Web Access
Project: src\src\Components\Components\test\Microsoft.AspNetCore.Components.Tests.csproj (Microsoft.AspNetCore.Components.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.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
 
namespace Microsoft.AspNetCore.Components.Test;
 
public class LayoutViewTest
{
    private readonly TestRenderer _renderer;
    private readonly LayoutView _layoutViewComponent;
    private readonly int _layoutViewComponentId;
 
    public LayoutViewTest()
    {
        _renderer = new TestRenderer();
        _layoutViewComponent = new LayoutView();
        _layoutViewComponentId = _renderer.AssignRootComponentId(_layoutViewComponent);
    }
 
    [Fact]
    public void GivenNoParameters_RendersNothing()
    {
        // Arrange/Act
        var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.Empty));
        Assert.True(setParametersTask.IsCompletedSuccessfully);
        var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
 
        // Assert
        Assert.Single(_renderer.Batches);
        Assert.Empty(frames);
    }
 
    [Fact]
    public void GivenContentButNoLayout_RendersContent()
    {
        // Arrange/Act
        var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
                    builder.AddContent(123, "Hello");
                    builder.AddContent(456, "Goodbye");
                })}
            })));
        Assert.True(setParametersTask.IsCompletedSuccessfully);
        var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
 
        // Assert
        Assert.Single(_renderer.Batches);
        Assert.Collection(frames,
            frame => AssertFrame.Text(frame, "Hello", 123),
            frame => AssertFrame.Text(frame, "Goodbye", 456));
    }
 
    [Fact]
    public void GivenLayoutButNoContent_RendersLayoutWithEmptyBody()
    {
        // Arrange/Act
        var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(LayoutView.Layout), typeof(RootLayout) }
            })));
 
        // Assert
        Assert.True(setParametersTask.IsCompletedSuccessfully);
        var batch = _renderer.Batches.Single();
 
        var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
        Assert.Collection(layoutViewFrames,
            frame => AssertFrame.Component<RootLayout>(frame, subtreeLength: 2, sequence: 0),
            frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1));
 
        var rootLayoutComponentId = batch.GetComponentFrames<RootLayout>().Single().ComponentId;
        var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable();
        Assert.Collection(rootLayoutFrames,
            frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0),
            frame => AssertFrame.Region(frame, subtreeLength: 1), // i.e., empty region
            frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2));
    }
 
    [Fact]
    public void RendersContentInsideLayout()
    {
        // Arrange/Act
        var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(LayoutView.Layout), typeof(RootLayout) },
                { nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
                    builder.AddContent(123, "Hello");
                    builder.AddContent(456, "Goodbye");
                })}
            })));
 
        // Assert
        Assert.True(setParametersTask.IsCompletedSuccessfully);
        var batch = _renderer.Batches.Single();
 
        var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
        Assert.Collection(layoutViewFrames,
            frame => AssertFrame.Component<RootLayout>(frame, subtreeLength: 2, sequence: 0),
            frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1));
 
        var rootLayoutComponentId = batch.GetComponentFrames<RootLayout>().Single().ComponentId;
        var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable();
        Assert.Collection(rootLayoutFrames,
            frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0),
            frame => AssertFrame.Region(frame, subtreeLength: 3),
            frame => AssertFrame.Text(frame, "Hello", sequence: 123),
            frame => AssertFrame.Text(frame, "Goodbye", sequence: 456),
            frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2));
    }
 
    [Fact]
    public void RendersContentInsideNestedLayout()
    {
        // Arrange/Act
        var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(LayoutView.Layout), typeof(NestedLayout) },
                { nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
                    builder.AddContent(123, "Hello");
                    builder.AddContent(456, "Goodbye");
                })}
            })));
 
        // Assert
        Assert.True(setParametersTask.IsCompletedSuccessfully);
        var batch = _renderer.Batches.Single();
 
        var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable();
        Assert.Collection(layoutViewFrames,
            frame => AssertFrame.Component<RootLayout>(frame, subtreeLength: 2, sequence: 0),
            frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1));
 
        var rootLayoutComponentId = batch.GetComponentFrames<RootLayout>().Single().ComponentId;
        var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable();
        Assert.Collection(rootLayoutFrames,
            frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0),
            frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1),
            frame => AssertFrame.Component<NestedLayout>(frame, subtreeLength: 2, sequence: 0),
            frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1),
            frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2));
 
        var nestedLayoutComponentId = batch.GetComponentFrames<NestedLayout>().Single().ComponentId;
        var nestedLayoutFrames = _renderer.GetCurrentRenderTreeFrames(nestedLayoutComponentId).AsEnumerable();
        Assert.Collection(nestedLayoutFrames,
            frame => AssertFrame.Text(frame, "NestedLayout starts here", sequence: 0),
            frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1),
            frame => AssertFrame.Text(frame, "Hello", sequence: 123),
            frame => AssertFrame.Text(frame, "Goodbye", sequence: 456),
            frame => AssertFrame.Text(frame, "NestedLayout ends here", sequence: 2));
    }
 
    [Fact]
    public void CanChangeContentWithSameLayout()
    {
        // Arrange
        var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(LayoutView.Layout), typeof(NestedLayout) },
                { nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
                    builder.AddContent(0, "Initial content");
                })}
            })));
 
        // Act
        Assert.True(setParametersTask.IsCompletedSuccessfully);
        _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(LayoutView.Layout), typeof(NestedLayout) },
                { nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
                    builder.AddContent(0, "Changed content");
                })}
            })));
 
        // Assert
        Assert.Equal(2, _renderer.Batches.Count);
        var batch = _renderer.Batches[1];
        Assert.Empty(batch.DisposedComponentIDs);
        Assert.Collection(batch.DiffsInOrder,
            diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes
            diff => Assert.Empty(diff.Edits), // RootLayout rerendered, but with no changes
            diff =>
            {
                // NestedLayout rerendered, patching content in place
                Assert.Collection(diff.Edits, edit =>
                {
                    Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
                    Assert.Equal(1, edit.SiblingIndex);
                    AssertFrame.Text(
                        batch.ReferenceFrames[edit.ReferenceFrameIndex],
                        "Changed content",
                        sequence: 0);
                });
            });
    }
 
    [Fact]
    public void CanChangeLayout()
    {
        // Arrange
        var setParametersTask1 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(LayoutView.Layout), typeof(NestedLayout) },
                { nameof(LayoutView.ChildContent), (RenderFragment)(builder => {
                    builder.AddContent(0, "Some content");
                })}
            })));
        Assert.True(setParametersTask1.IsCompletedSuccessfully);
 
        // Act
        var setParametersTask2 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object>
            {
                { nameof(LayoutView.Layout), typeof(OtherNestedLayout) },
            })));
 
        // Assert
        Assert.True(setParametersTask2.IsCompletedSuccessfully);
        Assert.Equal(2, _renderer.Batches.Count);
        var batch = _renderer.Batches[1];
        Assert.Single(batch.DisposedComponentIDs); // Disposes NestedLayout
        Assert.Collection(batch.DiffsInOrder,
            diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes
            diff =>
            {
                // RootLayout rerendered, changing child
                Assert.Collection(diff.Edits,
                edit =>
                {
                    Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
                    Assert.Equal(1, edit.SiblingIndex);
                },
                edit =>
                {
                    Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                    Assert.Equal(1, edit.SiblingIndex);
                    AssertFrame.Component<OtherNestedLayout>(
                        batch.ReferenceFrames[edit.ReferenceFrameIndex],
                        sequence: 0);
                });
            },
            diff =>
            {
                // Inserts new OtherNestedLayout
                Assert.Collection(diff.Edits,
                edit =>
                {
                    Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                    Assert.Equal(0, edit.SiblingIndex);
                    AssertFrame.Text(
                        batch.ReferenceFrames[edit.ReferenceFrameIndex],
                        "OtherNestedLayout starts here");
                },
                edit =>
                {
                    Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                    Assert.Equal(1, edit.SiblingIndex);
                    AssertFrame.Text(
                        batch.ReferenceFrames[edit.ReferenceFrameIndex],
                        "Some content");
                },
                edit =>
                {
                    Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                    Assert.Equal(2, edit.SiblingIndex);
                    AssertFrame.Text(
                        batch.ReferenceFrames[edit.ReferenceFrameIndex],
                        "OtherNestedLayout ends here");
                });
            });
    }
 
    private class RootLayout : AutoRenderComponent
    {
        [Parameter]
        public RenderFragment Body { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            if (Body == null)
            {
                // Prove that we don't expect layouts to tolerate null values for Body
                throw new InvalidOperationException("Got a null body when not expecting it");
            }
 
            builder.AddContent(0, "RootLayout starts here");
            builder.AddContent(1, Body);
            builder.AddContent(2, "RootLayout ends here");
        }
    }
 
    [Layout(typeof(RootLayout))]
    private class NestedLayout : AutoRenderComponent
    {
        [Parameter]
        public RenderFragment Body { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.AddContent(0, "NestedLayout starts here");
            builder.AddContent(1, Body);
            builder.AddContent(2, "NestedLayout ends here");
        }
    }
 
    [Layout(typeof(RootLayout))]
    private class OtherNestedLayout : AutoRenderComponent
    {
        [Parameter]
        public RenderFragment Body { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.AddContent(0, "OtherNestedLayout starts here");
            builder.AddContent(1, Body);
            builder.AddContent(2, "OtherNestedLayout ends here");
        }
    }
}