// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.Blazor.Components; using Microsoft.Blazor.Rendering; using Microsoft.Blazor.RenderTree; using System; using System.Collections.Generic; using System.Linq; using Xunit; namespace Microsoft.Blazor.Test { public class RenderTreeDiffTest { [Theory] [MemberData(nameof(RecognizesEquivalentNodesAsSameCases))] public void RecognizesEquivalentNodesAsSame(Action appendAction) { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); appendAction(oldTree); appendAction(newTree); // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); } public static IEnumerable RecognizesEquivalentNodesAsSameCases() => new Action[] { builder => builder.AddText(0, "Hello"), builder => builder.OpenElement(0, "Some Element"), builder => { builder.OpenElement(0, "Some Element"); builder.AddAttribute("My attribute", "My value"); builder.CloseElement(); }, builder => builder.AddComponent(0) }.Select(x => new object[] { x }); [Fact] public void RecognizesNewItemsBeingInserted() { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.AddText(0, "text0"); oldTree.AddText(2, "text2"); newTree.AddText(0, "text0"); newTree.AddText(1, "text1"); newTree.AddText(2, "text2"); // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(1, entry.NewTreeIndex); }, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); } [Fact] public void RecognizesOldItemsBeingRemoved() { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.AddText(0, "text0"); oldTree.AddText(1, "text1"); oldTree.AddText(2, "text2"); newTree.AddText(0, "text0"); newTree.AddText(2, "text2"); // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); } [Fact] public void RecognizesTrailingSequenceWithinLoopBlockBeingRemoved() { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.AddText(0, "x"); // Loop start oldTree.AddText(1, "x"); // Will be removed oldTree.AddText(2, "x"); // Will be removed oldTree.AddText(0, "x"); // Loop start newTree.AddText(0, "x"); // Loop start newTree.AddText(0, "x"); // Loop start // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); } [Fact] public void RecognizesTrailingSequenceWithinLoopBlockBeingAppended() { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.AddText(0, "x"); // Loop start oldTree.AddText(0, "x"); // Loop start newTree.AddText(0, "x"); // Loop start newTree.AddText(1, "x"); // Will be added newTree.AddText(2, "x"); // Will be added newTree.AddText(0, "x"); // Loop start // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(1, entry.NewTreeIndex); }, entry => { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(2, entry.NewTreeIndex); }, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); } [Fact] public void RecognizesTrailingLoopBlockBeingRemoved() { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.AddText(0, "x"); oldTree.AddText(1, "x"); oldTree.AddText(0, "x"); // Will be removed oldTree.AddText(1, "x"); // Will be removed newTree.AddText(0, "x"); newTree.AddText(1, "x"); // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type)); } [Fact] public void RecognizesTrailingLoopBlockBeingAdded() { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.AddText(0, "x"); oldTree.AddText(1, "x"); newTree.AddText(0, "x"); newTree.AddText(1, "x"); newTree.AddText(0, "x"); // Will be added newTree.AddText(1, "x"); // Will be added // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(2, entry.NewTreeIndex); }, entry => { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(3, entry.NewTreeIndex); }); } [Fact] public void RecognizesLeadingLoopBlockItemsBeingAdded() { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.AddText(2, "x"); oldTree.AddText(2, "x"); // Note that the '0' and '1' items are not present on this iteration newTree.AddText(2, "x"); newTree.AddText(0, "x"); newTree.AddText(1, "x"); newTree.AddText(2, "x"); // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(1, entry.NewTreeIndex); }, entry => { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(2, entry.NewTreeIndex); }, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); } [Fact] public void RecognizesLeadingLoopBlockItemsBeingRemoved() { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.AddText(2, "x"); oldTree.AddText(0, "x"); oldTree.AddText(1, "x"); oldTree.AddText(2, "x"); newTree.AddText(2, "x"); newTree.AddText(2, "x"); // Note that the '0' and '1' items are not present on this iteration // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type)); } [Fact] public void HandlesAdjacentItemsBeingRemovedAndInsertedAtOnce() { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.AddText(0, "text"); newTree.AddText(1, "text"); // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type), entry => Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type)); } [Fact] public void RecognizesTextUpdates() { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.AddText(123, "old text"); newTree.AddText(123, "new text"); // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => { Assert.Equal(RenderTreeDiffEntryType.UpdateText, entry.Type); Assert.Equal(0, entry.NewTreeIndex); }); } [Fact] public void RecognizesElementNameChangesAtSameSequenceNumber() { // Note: It's not possible to trigger this scenario from a Razor component, because // a given source sequence can only have a single fixed element name. We might later // decide just to throw in this scenario, since it's unnecessary to support it. // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.OpenElement(123, "old element"); oldTree.CloseElement(); newTree.OpenElement(123, "new element"); newTree.CloseElement(); // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(0, entry.NewTreeIndex); }, entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type)); } [Fact] public void RecognizesComponentTypeChangesAtSameSequenceNumber() { // Arrange var oldTree = new RenderTreeBuilder(new FakeRenderer()); var newTree = new RenderTreeBuilder(new FakeRenderer()); var diff = new RenderTreeDiff(); oldTree.AddComponent(123); newTree.AddComponent(123); // Act var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes()); // Assert Assert.Collection(result, entry => { Assert.Equal(RenderTreeDiffEntryType.PrependNode, entry.Type); Assert.Equal(0, entry.NewTreeIndex); }, entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type)); } private class FakeRenderer : Renderer { internal protected override void UpdateDisplay(int componentId, ArraySegment renderTree) => throw new NotImplementedException(); } private class FakeComponent : IComponent { public void BuildRenderTree(RenderTreeBuilder builder) => throw new NotImplementedException(); } private class FakeComponent2 : IComponent { public void BuildRenderTree(RenderTreeBuilder builder) => throw new NotImplementedException(); } } }