From 4c62eb89af1a5b7890499c5afe240095b6c2db30 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 31 Jan 2017 12:17:44 -0800 Subject: [PATCH] Unit tests for VCTH and model directive --- .../ModelDirective.cs | 2 +- .../ViewComponentTagHelperPass.cs | 12 +- .../Directives/SetBaseTypeChunkMergerTest.cs | 94 ----- .../ViewComponentTagHelperChunkVisitorTest.cs | 61 ---- .../ModelDirectiveTest.cs | 223 ++++++++++++ .../StringTextBuffer.cs | 49 --- .../GeneratedViewComponentTagHelperClasses.cs | 58 --- .../ViewComponentTagHelperPassTest.cs | 336 ++++++++++++++++++ 8 files changed, 566 insertions(+), 269 deletions(-) delete mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Directives/SetBaseTypeChunkMergerTest.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/ViewComponentTagHelperChunkVisitorTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ModelDirectiveTest.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/StringTextBuffer.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/GeneratedViewComponentTagHelperClasses.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ViewComponentTagHelperPassTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs index ad54ed4afb..6d21a6b306 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ModelDirective.cs @@ -49,7 +49,7 @@ private static string GetModelType(Visitor visitor) return "dynamic"; } - private class Pass : IRazorIRPass + internal class Pass : IRazorIRPass { public RazorEngine Engine { get; set; } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ViewComponentTagHelperPass.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ViewComponentTagHelperPass.cs index 5458ac086b..99d1269fd9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/ViewComponentTagHelperPass.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/ViewComponentTagHelperPass.cs @@ -147,7 +147,7 @@ private void BuildAttributeDeclarations(CSharpCodeWriter writer, TagHelperDescri writer.WriteAutoPropertyDeclaration( "public", $"global::Microsoft.AspNetCore.Mvc.Rendering.ViewContext", - "_context"); + "ViewContext"); var indexerAttributes = descriptor.Attributes.Where(a => a.IsIndexer); @@ -187,19 +187,19 @@ private void BuildProcessMethodString(CSharpCodeWriter writer, TagHelperDescript writer.WriteInstanceMethodInvocation( $"(_helper as global::Microsoft.AspNetCore.Mvc.ViewFeatures.IViewContextAware)?", "Contextualize", - new[] { "_context" }); + new[] { "ViewContext" }); var methodParameters = GetMethodParameters(descriptor); - var viewContentVariable = "viewContent"; + var contentVariable = "content"; writer.Write("var ") - .WriteStartAssignment(viewContentVariable) + .WriteStartAssignment(contentVariable) .WriteInstanceMethodInvocation($"await _helper", "InvokeAsync", methodParameters); writer.WriteStartAssignment($"{outputVariable}.TagName") .WriteLine("null;"); writer.WriteInstanceMethodInvocation( $"{outputVariable}.Content", "SetHtmlContent", - new[] { viewContentVariable }); + new[] { contentVariable }); } } @@ -207,7 +207,7 @@ private string[] GetMethodParameters(TagHelperDescriptor descriptor) { var propertyNames = descriptor.Attributes.Where(a => !a.IsIndexer).Select(attribute => attribute.PropertyName); var joinedPropertyNames = string.Join(", ", propertyNames); - var parametersString = $" new {{ { joinedPropertyNames } }}"; + var parametersString = $"new {{ { joinedPropertyNames } }}"; var viewComponentName = descriptor.PropertyBag[ ViewComponentTagHelperDescriptorConventions.ViewComponentNameKey]; diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Directives/SetBaseTypeChunkMergerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Directives/SetBaseTypeChunkMergerTest.cs deleted file mode 100644 index b0feff71f5..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Directives/SetBaseTypeChunkMergerTest.cs +++ /dev/null @@ -1,94 +0,0 @@ -// 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.AspNetCore.Razor.Chunks; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Razor.Directives -{ - public class SetBaseTypeChunkMergerTest - { - #if OLD_RAZOR - [Theory] - [InlineData("MyApp.BaseType", "MyApp.BaseType")] - [InlineData("TestBaseType", "TestBaseType")] - public void Visit_UpdatesTModelTokenToMatchModelType(string typeName, string expectedValue) - { - // Arrange - var chunk = new SetBaseTypeChunk - { - TypeName = typeName, - }; - var merger = new SetBaseTypeChunkMerger("Person"); - - // Act - merger.VisitChunk(chunk); - - // Assert - Assert.Equal(expectedValue, chunk.TypeName); - } - - [Fact] - public void Merge_SetsBaseTypeIfItHasNotBeenSetInChunkTree() - { - // Arrange - var expected = "MyApp.Razor.MyBaseType"; - var merger = new SetBaseTypeChunkMerger("dynamic"); - var chunkTree = new ChunkTree(); - var inheritedChunks = new[] - { - new SetBaseTypeChunk { TypeName = expected } - }; - - // Act - merger.MergeInheritedChunks(chunkTree, inheritedChunks); - - // Assert - var chunk = Assert.Single(chunkTree.Children); - var setBaseTypeChunk = Assert.IsType(chunk); - Assert.Equal(expected, setBaseTypeChunk.TypeName); - } - - [Fact] - public void Merge_IgnoresSetBaseTypeChunksIfChunkTreeContainsOne() - { - // Arrange - var merger = new SetBaseTypeChunkMerger("dynamic"); - var chunkTree = new ChunkTree(); - var inheritedChunks = new[] - { - new SetBaseTypeChunk { TypeName = "MyBaseType2" } - }; - - // Act - merger.VisitChunk(new SetBaseTypeChunk { TypeName = "MyBaseType1" }); - merger.MergeInheritedChunks(chunkTree, inheritedChunks); - - // Assert - Assert.Empty(chunkTree.Children); - } - - [Fact] - public void Merge_PicksLastBaseTypeChunkFromChunkTree() - { - // Arrange - var merger = new SetBaseTypeChunkMerger("dynamic"); - var chunkTree = new ChunkTree(); - var inheritedChunks = new Chunk[] - { - new SetBaseTypeChunk { TypeName = "MyBase2" }, - new LiteralChunk(), - new SetBaseTypeChunk { TypeName = "MyBase1" }, - }; - - // Act - merger.MergeInheritedChunks(chunkTree, inheritedChunks); - - // Assert - var chunk = Assert.Single(chunkTree.Children); - var setBaseTypeChunk = Assert.IsType(chunk); - Assert.Equal("MyBase1", setBaseTypeChunk.TypeName); - } -#endif - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/ViewComponentTagHelperChunkVisitorTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/ViewComponentTagHelperChunkVisitorTest.cs deleted file mode 100644 index f926e1e8b9..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Internal/ViewComponentTagHelperChunkVisitorTest.cs +++ /dev/null @@ -1,61 +0,0 @@ -// 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 System.Collections.Generic; -using System.Reflection; -using Microsoft.AspNetCore.Razor.Chunks; -using Microsoft.AspNetCore.Razor.CodeGenerators; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Razor.Host.Test.Internal -{ - public class ViewComponentTagHelperChunkVisitorTest - { - #if OLD_RAZOR - public static TheoryData CodeGenerationData - { - get - { - var oneInstanceChunks = ChunkVisitorTestFactory.GetTestChunks(visitedTagHelperChunks: true); - var twoInstanceChunks = ChunkVisitorTestFactory.GetTestChunks(visitedTagHelperChunks: true); - twoInstanceChunks.Add(twoInstanceChunks[twoInstanceChunks.Count - 1]); - - return new TheoryData> - { - oneInstanceChunks, - twoInstanceChunks - }; - } - } - - [Theory] - [MemberData(nameof(CodeGenerationData))] - public void Accept_CorrectlyGeneratesCode(IList chunks) - { - // Arrange - var writer = new CSharpCodeWriter(); - var context = ChunkVisitorTestFactory.CreateCodeGeneratorContext(); - var chunkVisitor = new ViewComponentTagHelperChunkVisitor(writer, context); - - var assembly = typeof(ViewComponentTagHelperChunkVisitorTest).GetTypeInfo().Assembly; - var path = "TestFiles/Output/Runtime/GeneratedViewComponentTagHelperClasses.cs"; - var expectedOutput = ResourceFile.ReadResource(assembly, path, sourceFile: true); - - // Act - chunkVisitor.Accept(chunks); - var resultOutput = writer.GenerateCode(); - -#if GENERATE_BASELINES - if (!string.Equals(expectedOutput, resultOutput, System.StringComparison.Ordinal)) - { - ResourceFile.UpdateFile(assembly, path, expectedOutput, resultOutput); - } -#else - // Assert - Assert.Equal(expectedOutput, resultOutput, ignoreLineEndingDifferences: true); -#endif - - } -#endif - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ModelDirectiveTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ModelDirectiveTest.cs new file mode 100644 index 0000000000..35b8d4d769 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ModelDirectiveTest.cs @@ -0,0 +1,223 @@ +// 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 System.IO; +using System.Text; +using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Host +{ + public class ModelDirectiveTest + { + [Fact] + public void ModelDirective_GetModelType_GetsTypeFromLastWellFormedDirective() + { + // Arrange + var codeDocument = CreateDocument(@" +@model Type1 +@model Type2 +@model +"); + + var engine = CreateEngine(); + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + var result = ModelDirective.GetModelType(irDocument); + + // Assert + Assert.Equal("Type2", result); + } + + [Fact] + public void ModelDirective_GetModelType_DefaultsToDynamic() + { + // Arrange + var codeDocument = CreateDocument(@" "); + + var engine = CreateEngine(); + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + var result = ModelDirective.GetModelType(irDocument); + + // Assert + Assert.Equal("dynamic", result); + } + + [Fact] + public void ModelDirectivePass_Execute_ReplacesTModelInBaseType() + { + // Arrange + var codeDocument = CreateDocument(@" +@inherits BaseType +@model Type1 +"); + + var engine = CreateEngine(); + var pass = new ModelDirective.Pass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var @class = FindClassNode(irDocument); + Assert.NotNull(@class); + Assert.Equal("BaseType", @class.BaseType); + } + + [Fact] + public void ModelDirectivePass_Execute_ReplacesTModelInBaseType_DifferentOrdering() + { + // Arrange + var codeDocument = CreateDocument(@" +@model Type1 +@inherits BaseType +@model Type2 +"); + + var engine = CreateEngine(); + var pass = new ModelDirective.Pass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var @class = FindClassNode(irDocument); + Assert.NotNull(@class); + Assert.Equal("BaseType", @class.BaseType); + } + + [Fact] + public void ModelDirectivePass_Execute_NoOpWithoutTModel() + { + // Arrange + var codeDocument = CreateDocument(@" +@inherits BaseType +@model Type1 +"); + + var engine = CreateEngine(); + var pass = new ModelDirective.Pass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var @class = FindClassNode(irDocument); + Assert.NotNull(@class); + Assert.Equal("BaseType", @class.BaseType); + } + + [Fact] + public void ModelDirectivePass_Execute_ReplacesTModelInBaseType_DefaultDynamic() + { + // Arrange + var codeDocument = CreateDocument(@" +@inherits BaseType +"); + + var engine = CreateEngine(); + var pass = new ModelDirective.Pass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var @class = FindClassNode(irDocument); + Assert.NotNull(@class); + Assert.Equal("BaseType", @class.BaseType); + } + + private RazorCodeDocument CreateDocument(string content) + { + using (var stream = new MemoryStream()) + { + var bytes = Encoding.UTF8.GetBytes(content); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0L, SeekOrigin.Begin); + + var source = RazorSourceDocument.ReadFrom(stream, "test.cshtml"); + return RazorCodeDocument.Create(source); + } + } + + private ClassDeclarationIRNode FindClassNode(RazorIRNode node) + { + var visitor = new ClassNodeVisitor(); + visitor.Visit(node); + return visitor.Node; + } + + private RazorEngine CreateEngine() + { + return RazorEngine.Create(b => + { + // Notice we're not registering the ModelDirective.Pass here so we can run it on demand. + b.AddDirective(ModelDirective.Directive); + }); + } + + private DocumentIRNode CreateIRDocument(RazorEngine engine, RazorCodeDocument codeDocument) + { + for (var i = 0; i < engine.Phases.Count; i++) + { + var phase = engine.Phases[i]; + phase.Execute(codeDocument); + + if (phase is IRazorIRPhase) + { + break; + } + } + + return codeDocument.GetIRDocument(); + } + + private string GetCSharpContent(RazorIRNode node) + { + var builder = new StringBuilder(); + for (var i = 0; i < node.Children.Count; i++) + { + var child = node.Children[i] as CSharpTokenIRNode; + builder.Append(child.Content); + } + + return builder.ToString(); + } + + private class ClassNodeVisitor : RazorIRNodeWalker + { + public ClassDeclarationIRNode Node { get; set; } + + public override void VisitClass(ClassDeclarationIRNode node) + { + Node = node; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/StringTextBuffer.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/StringTextBuffer.cs deleted file mode 100644 index 5ddbc83bb6..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/StringTextBuffer.cs +++ /dev/null @@ -1,49 +0,0 @@ -// 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 System; -using Microsoft.AspNetCore.Razor.Text; - -namespace Microsoft.AspNetCore.Mvc.Razor -{ - public class StringTextBuffer : ITextBuffer, IDisposable - { - private string _buffer; - public bool Disposed { get; set; } - - public StringTextBuffer(string buffer) - { - _buffer = buffer; - } - - public int Length - { - get { return _buffer.Length; } - } - - public int Position { get; set; } - - public int Read() - { - if (Position >= _buffer.Length) - { - return -1; - } - return _buffer[Position++]; - } - - public int Peek() - { - if (Position >= _buffer.Length) - { - return -1; - } - return _buffer[Position]; - } - - public void Dispose() - { - Disposed = true; - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/GeneratedViewComponentTagHelperClasses.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/GeneratedViewComponentTagHelperClasses.cs deleted file mode 100644 index c83d7a93df..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/GeneratedViewComponentTagHelperClasses.cs +++ /dev/null @@ -1,58 +0,0 @@ -[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("foo")] -public class __Generated__FooViewComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper -{ - private readonly global::Microsoft.AspNetCore.Mvc.IViewComponentHelper _viewComponentHelper = null; - public __Generated__FooViewComponentTagHelper(global::Microsoft.AspNetCore.Mvc.IViewComponentHelper viewComponentHelper) - { - _viewComponentHelper = viewComponentHelper; - } - [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute, global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute] - public global::Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { get; set; } - public System.String Attribute { get; set; } - public override async global::System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) - { - (_viewComponentHelper as global::Microsoft.AspNetCore.Mvc.ViewFeatures.IViewContextAware)?.Contextualize(ViewContext); - var viewContent = await _viewComponentHelper.InvokeAsync("Foo", new { Attribute }); - output.TagName = null; - output.Content.SetHtmlContent(viewContent); - } -} -[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("bar")] -public class __Generated__BarViewComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper -{ - private readonly global::Microsoft.AspNetCore.Mvc.IViewComponentHelper _viewComponentHelper = null; - public __Generated__BarViewComponentTagHelper(global::Microsoft.AspNetCore.Mvc.IViewComponentHelper viewComponentHelper) - { - _viewComponentHelper = viewComponentHelper; - } - [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute, global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute] - public global::Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { get; set; } - public System.String Attribute { get; set; } - public override async global::System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) - { - (_viewComponentHelper as global::Microsoft.AspNetCore.Mvc.ViewFeatures.IViewContextAware)?.Contextualize(ViewContext); - var viewContent = await _viewComponentHelper.InvokeAsync("Bar", new { Attribute }); - output.TagName = null; - output.Content.SetHtmlContent(viewContent); - } -} -[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("bee")] -public class __Generated__BeeViewComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper -{ - private readonly global::Microsoft.AspNetCore.Mvc.IViewComponentHelper _viewComponentHelper = null; - public __Generated__BeeViewComponentTagHelper(global::Microsoft.AspNetCore.Mvc.IViewComponentHelper viewComponentHelper) - { - _viewComponentHelper = viewComponentHelper; - } - [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute, global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute] - public global::Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { get; set; } - public System.Collections.Generic.Dictionary> Attribute { get; set; } - = new System.Collections.Generic.Dictionary>(); - public override async global::System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) - { - (_viewComponentHelper as global::Microsoft.AspNetCore.Mvc.ViewFeatures.IViewContextAware)?.Contextualize(ViewContext); - var viewContent = await _viewComponentHelper.InvokeAsync("Bee", new { Attribute }); - output.TagName = null; - output.Content.SetHtmlContent(viewContent); - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ViewComponentTagHelperPassTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ViewComponentTagHelperPassTest.cs new file mode 100644 index 0000000000..fe69548cb2 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/ViewComponentTagHelperPassTest.cs @@ -0,0 +1,336 @@ +// 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 System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Xunit; +using ErrorSink = Microsoft.AspNetCore.Razor.Evolution.Legacy.ErrorSink; + +namespace Microsoft.AspNetCore.Mvc.Razor.Host +{ + public class ViewComponentTagHelperPassTest + { + [Fact] + public void ViewComponentTagHelperPass_Execute_IgnoresRegularTagHelper() + { + // Arrange + var codeDocument = CreateDocument(@" +@addTagHelper TestTagHelper, TestAssembly +

"); + + var tagHelpers = new[] + { + new TagHelperDescriptor() + { + AssemblyName = "TestAssembly", + TypeName = "TestTagHelper", + TagName = "p", + Attributes = new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor() + { + TypeName = "System.Int32", + Name = "Foo", + } + + } + } + }; + + var engine = CreateEngine(tagHelpers); + var pass = new ViewComponentTagHelperPass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var @class = FindClassNode(irDocument); + Assert.Equal(2, @class.Children.Count); // No class node created for a VCTH + for (var i = 0; i < @class.Children.Count; i++) + { + Assert.IsNotType(@class.Children[i]); + } + } + + [Fact] + public void ViewComponentTagHelperPass_Execute_CreatesViewComponentTagHelper() + { + // Arrange + var codeDocument = CreateDocument(@" +@addTagHelper TestTagHelper, TestAssembly +"); + + var tagHelpers = new[] + { + new TagHelperDescriptor() + { + AssemblyName = "TestAssembly", + TypeName = "TestTagHelper", + TagName = "tagcloud", + Attributes = new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor() + { + TypeName = "System.Int32", + Name = "Foo", + PropertyName = "Foo", + } + + } + } + }; + + tagHelpers[0].PropertyBag.Add(ViewComponentTagHelperDescriptorConventions.ViewComponentNameKey, "TagCloud"); + + var engine = CreateEngine(tagHelpers); + var pass = new ViewComponentTagHelperPass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + var expectedVCTHName = "AspNetCore.Generated_test.__Generated__TagCloudViewComponentTagHelper"; + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var tagHelper = FindTagHelperNode(irDocument); + Assert.Equal(expectedVCTHName, Assert.IsType(tagHelper.Children[1]).TagHelperTypeName); + Assert.Equal(expectedVCTHName, Assert.IsType(tagHelper.Children[2]).TagHelperTypeName); + + + var @class = FindClassNode(irDocument); + Assert.Equal(3, @class.Children.Count); + + var vcthClass = Assert.IsType(@class.Children[2]); + Assert.Equal(@"[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute(""tagcloud"")] +public class __Generated__TagCloudViewComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper +{ + private readonly global::Microsoft.AspNetCore.Mvc.IViewComponentHelper _helper = null; + public __Generated__TagCloudViewComponentTagHelper(global::Microsoft.AspNetCore.Mvc.IViewComponentHelper helper) + { + _helper = helper; + } + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute, global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { get; set; } + public System.Int32 Foo { get; set; } + public override async global::System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) + { + (_helper as global::Microsoft.AspNetCore.Mvc.ViewFeatures.IViewContextAware)?.Contextualize(ViewContext); + var content = await _helper.InvokeAsync(""TagCloud"", new { Foo }); + output.TagName = null; + output.Content.SetHtmlContent(content); + } +} +", vcthClass.Content); + } + + [Fact] + public void ViewComponentTagHelperPass_Execute_CreatesViewComponentTagHelper_WithIndexer() + { + // Arrange + var codeDocument = CreateDocument(@" +@addTagHelper TestTagHelper, TestAssembly +"); + + var tagHelpers = new[] + { + new TagHelperDescriptor() + { + AssemblyName = "TestAssembly", + TypeName = "TestTagHelper", + TagName = "tagcloud", + Attributes = new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor() + { + TypeName = "System.Collections.Generic.Dictionary", + Name = "foo", + PropertyName = "Tags", + IsIndexer = false, + }, + new TagHelperAttributeDescriptor() + { + TypeName = "System.Collections.Generic.Dictionary", + Name = "foo-", + PropertyName = "Tags", + IsIndexer = true, + } + + } + } + }; + + tagHelpers[0].PropertyBag.Add(ViewComponentTagHelperDescriptorConventions.ViewComponentNameKey, "TagCloud"); + + var engine = CreateEngine(tagHelpers); + var pass = new ViewComponentTagHelperPass() + { + Engine = engine, + }; + + var irDocument = CreateIRDocument(engine, codeDocument); + + var expectedVCTHName = "AspNetCore.Generated_test.__Generated__TagCloudViewComponentTagHelper"; + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + var tagHelper = FindTagHelperNode(irDocument); + Assert.Equal(expectedVCTHName, Assert.IsType(tagHelper.Children[1]).TagHelperTypeName); + Assert.IsType(tagHelper.Children[2]); + + var @class = FindClassNode(irDocument); + Assert.Equal(4, @class.Children.Count); + + var vcthClass = Assert.IsType(@class.Children[3]); + Assert.Equal(@"[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute(""tagcloud"")] +public class __Generated__TagCloudViewComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper +{ + private readonly global::Microsoft.AspNetCore.Mvc.IViewComponentHelper _helper = null; + public __Generated__TagCloudViewComponentTagHelper(global::Microsoft.AspNetCore.Mvc.IViewComponentHelper helper) + { + _helper = helper; + } + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute, global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { get; set; } + public System.Collections.Generic.Dictionary Tags { get; set; } + = new System.Collections.Generic.Dictionary(); + public override async global::System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) + { + (_helper as global::Microsoft.AspNetCore.Mvc.ViewFeatures.IViewContextAware)?.Contextualize(ViewContext); + var content = await _helper.InvokeAsync(""TagCloud"", new { Tags }); + output.TagName = null; + output.Content.SetHtmlContent(content); + } +} +", vcthClass.Content); + } + + private RazorCodeDocument CreateDocument(string content) + { + using (var stream = new MemoryStream()) + { + var bytes = Encoding.UTF8.GetBytes(content); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0L, SeekOrigin.Begin); + + var source = RazorSourceDocument.ReadFrom(stream, "test.cshtml"); + return RazorCodeDocument.Create(source); + } + } + + private RazorEngine CreateEngine(params TagHelperDescriptor[] tagHelpers) + { + return RazorEngine.Create(b => + { + b.Features.Add(new MvcViewDocumentClassifierPass()); + + b.Features.Add(new TagHelperFeature(tagHelpers)); + }); + } + + private DocumentIRNode CreateIRDocument(RazorEngine engine, RazorCodeDocument codeDocument) + { + for (var i = 0; i < engine.Phases.Count; i++) + { + var phase = engine.Phases[i]; + phase.Execute(codeDocument); + + if (phase is IRazorIRPhase) + { + break; + } + } + + return codeDocument.GetIRDocument(); + } + + private ClassDeclarationIRNode FindClassNode(RazorIRNode node) + { + var visitor = new ClassDeclarationNodeVisitor(); + visitor.Visit(node); + return visitor.Node; + } + + private TagHelperIRNode FindTagHelperNode(RazorIRNode node) + { + var visitor = new TagHelperNodeVisitor(); + visitor.Visit(node); + return visitor.Node; + } + + private string GetCSharpContent(RazorIRNode node) + { + var builder = new StringBuilder(); + for (var i = 0; i < node.Children.Count; i++) + { + var child = node.Children[i] as CSharpTokenIRNode; + builder.Append(child.Content); + } + + return builder.ToString(); + } + + private class ClassDeclarationNodeVisitor : RazorIRNodeWalker + { + public ClassDeclarationIRNode Node { get; set; } + + public override void VisitClass(ClassDeclarationIRNode node) + { + Node = node; + } + } + + private class TagHelperNodeVisitor : RazorIRNodeWalker + { + public TagHelperIRNode Node { get; set; } + + public override void VisitTagHelper(TagHelperIRNode node) + { + Node = node; + } + } + + private class TagHelperFeature : ITagHelperFeature + { + public TagHelperFeature(TagHelperDescriptor[] tagHelpers) + { + Resolver = new TagHelperDescriptorResolver(tagHelpers); + } + + public RazorEngine Engine { get; set; } + + public ITagHelperDescriptorResolver Resolver { get; } + } + + private class TagHelperDescriptorResolver : ITagHelperDescriptorResolver + { + public TagHelperDescriptorResolver(TagHelperDescriptor[] tagHelpers) + { + TagHelpers = tagHelpers; + } + + public TagHelperDescriptor[] TagHelpers { get; } + + public IEnumerable Resolve(ErrorSink errorSink) + { + return TagHelpers; + } + } + } +}