diff --git a/Blazor.sln b/Blazor.sln
index 92870f00f2..bdc9df769c 100644
--- a/Blazor.sln
+++ b/Blazor.sln
@@ -52,7 +52,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Blazor.Build", "s
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Blazor.Browser.JS", "src\Microsoft.Blazor.Browser.JS\Microsoft.Blazor.Browser.JS.csproj", "{8A19B1CE-9B62-4440-93B3-152DDBB39D0A}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Blazor.Browser", "src\Microsoft.Blazor.Browser\Microsoft.Blazor.Browser.csproj", "{4874AFF4-335D-4037-8858-BBBB987C124B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Blazor.Browser", "src\Microsoft.Blazor.Browser\Microsoft.Blazor.Browser.csproj", "{4874AFF4-335D-4037-8858-BBBB987C124B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Blazor.Test", "test\Microsoft.Blazor.Test\Microsoft.Blazor.Test.csproj", "{8FD8636E-AFA5-434D-8857-06D02686741A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -132,6 +134,10 @@ Global
{4874AFF4-335D-4037-8858-BBBB987C124B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4874AFF4-335D-4037-8858-BBBB987C124B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4874AFF4-335D-4037-8858-BBBB987C124B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8FD8636E-AFA5-434D-8857-06D02686741A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8FD8636E-AFA5-434D-8857-06D02686741A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8FD8636E-AFA5-434D-8857-06D02686741A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8FD8636E-AFA5-434D-8857-06D02686741A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -158,6 +164,7 @@ Global
{8B3D0F1C-0E38-4E6D-BFF1-C4FDA0CD9815} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
{8A19B1CE-9B62-4440-93B3-152DDBB39D0A} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
{4874AFF4-335D-4037-8858-BBBB987C124B} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
+ {8FD8636E-AFA5-434D-8857-06D02686741A} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}
diff --git a/test/Microsoft.Blazor.Test/Microsoft.Blazor.Test.csproj b/test/Microsoft.Blazor.Test/Microsoft.Blazor.Test.csproj
new file mode 100644
index 0000000000..1227b792bf
--- /dev/null
+++ b/test/Microsoft.Blazor.Test/Microsoft.Blazor.Test.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netcoreapp2.0
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.Blazor.Test/UITreeBuilderTest.cs b/test/Microsoft.Blazor.Test/UITreeBuilderTest.cs
new file mode 100644
index 0000000000..e75d8c43ef
--- /dev/null
+++ b/test/Microsoft.Blazor.Test/UITreeBuilderTest.cs
@@ -0,0 +1,164 @@
+// 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.UITree;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Blazor.Test
+{
+ public class UITreeBuilderTest
+ {
+ [Fact]
+ public void StartsEmpty()
+ {
+ // Arrange
+ var builder = new UITreeBuilder();
+
+ // Assert
+ var nodes = builder.GetNodes();
+ Assert.NotNull(nodes.Array);
+ Assert.Equal(0, nodes.Offset);
+ Assert.Empty(nodes);
+ }
+
+ [Fact]
+ public void CanAddText()
+ {
+ // Arrange
+ var builder = new UITreeBuilder();
+
+ // Act
+ builder.AddText("First item");
+ builder.AddText("Second item");
+
+ // Assert
+ var nodes = builder.GetNodes();
+ Assert.Equal(0, nodes.Offset);
+ Assert.Collection(nodes,
+ node => AssertText(node, "First item"),
+ node => AssertText(node, "Second item"));
+ }
+
+ [Fact]
+ public void UnclosedElementsHaveNoEndDescendantIndex()
+ {
+ // Arrange
+ var builder = new UITreeBuilder();
+
+ // Act
+ builder.OpenElement("my element");
+
+ // Assert
+ var node = builder.GetNodes().Single();
+ AssertElement(node, "my element", 0);
+ }
+
+ [Fact]
+ public void ClosedEmptyElementsHaveSelfAsEndDescendantIndex()
+ {
+ // Arrange
+ var builder = new UITreeBuilder();
+
+ // Act
+ builder.AddText("some node so that the element isn't at position zero");
+ builder.OpenElement("my element");
+ builder.CloseElement();
+
+ // Assert
+ var nodes = builder.GetNodes();
+ Assert.Equal(2, nodes.Count);
+ AssertElement(nodes[1], "my element", 1);
+ }
+
+ [Fact]
+ public void ClosedElementsHaveCorrectEndDescendantIndex()
+ {
+ // Arrange
+ var builder = new UITreeBuilder();
+
+ // Act
+ builder.OpenElement("my element");
+ builder.AddText("child 1");
+ builder.AddText("child 2");
+ builder.CloseElement();
+ builder.AddText("unrelated item");
+
+ // Assert
+ var nodes = builder.GetNodes();
+ Assert.Equal(4, nodes.Count);
+ AssertElement(nodes[0], "my element", 2);
+ }
+
+ [Fact]
+ public void CanNestElements()
+ {
+ // Arrange
+ var builder = new UITreeBuilder();
+
+ // Act
+ builder.AddText("standalone text 1"); // 0: standalone text 1
+ builder.OpenElement("root"); // 1:
+ builder.AddText("root text 1"); // 2: root text 1
+ builder.AddText("root text 2"); // 3: root text 2
+ builder.OpenElement("child"); // 4:
+ builder.AddText("child text"); // 5: child text
+ builder.OpenElement("grandchild"); // 6:
+ builder.AddText("grandchild text 1"); // 7: grandchild text 1
+ builder.AddText("grandchild text 2"); // 8: grandchild text 2
+ builder.CloseElement(); //
+ builder.CloseElement(); //
+ builder.AddText("root text 3"); // 9: root text 3
+ builder.OpenElement("child 2"); // 10:
+ builder.CloseElement(); //
+ builder.CloseElement(); //
+ builder.AddText("standalone text 2"); // 11: standalone text 2
+
+ // Assert
+ Assert.Collection(builder.GetNodes(),
+ node => AssertText(node, "standalone text 1"),
+ node => AssertElement(node, "root", 10),
+ node => AssertText(node, "root text 1"),
+ node => AssertText(node, "root text 2"),
+ node => AssertElement(node, "child", 8),
+ node => AssertText(node, "child text"),
+ node => AssertElement(node, "grandchild", 8),
+ node => AssertText(node, "grandchild text 1"),
+ node => AssertText(node, "grandchild text 2"),
+ node => AssertText(node, "root text 3"),
+ node => AssertElement(node, "child 2", 10),
+ node => AssertText(node, "standalone text 2"));
+ }
+
+ [Fact]
+ public void CanClear()
+ {
+ // Arrange
+ var builder = new UITreeBuilder();
+
+ // Act
+ builder.AddText("some text");
+ builder.OpenElement("elem");
+ builder.AddText("more text");
+ builder.CloseElement();
+ builder.Clear();
+
+ // Assert
+ Assert.Empty(builder.GetNodes());
+ }
+
+ void AssertText(UITreeNode node, string textContent)
+ {
+ Assert.Equal(UITreeNodeType.Text, node.NodeType);
+ Assert.Equal(textContent, node.TextContent);
+ Assert.Equal(0, node.ElementDescendantsEndIndex);
+ }
+
+ void AssertElement(UITreeNode node, string elementName, int descendantsEndIndex)
+ {
+ Assert.Equal(UITreeNodeType.Element, node.NodeType);
+ Assert.Equal(elementName, node.ElementName);
+ Assert.Equal(descendantsEndIndex, node.ElementDescendantsEndIndex);
+ }
+ }
+}