From 7ab25918e0cf17e90b78e3fec9dfa4246fa84edf Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Tue, 21 Oct 2014 12:14:31 -0700 Subject: [PATCH] Add tests to validate @removetaghelper functionality. - Added utility methods to construct valid SyntaxTreeNodes that represent the @removetaghelper directive. - Added parse level unit tests to validate the @removetaghelper generates an accurate SyntaxTreeNode. - Added parse level unit tests to validate the @removetaghelper throws with bad formats. - Added TagHelperRegistration unit tests to validate the AddOrRemoveTagHelperCodeGenerators are understood and affect the descriptors found. - Added Designtime mapping tests to validate correct source mappings are made to ensure proper coloring and lack-of C# intellisense. - Added end-to-end tests to validate @removetaghelper can essentially disable TagHelpers on a page. #112 --- .../Parser/CSharpCodeParser.cs | 1 + .../Framework/TestSpanBuilder.cs | 9 +- .../Generator/CSharpTagHelperRenderingTest.cs | 18 ++ .../Generator/CodeTree/CodeTreeBuilderTest.cs | 24 ++ .../Parser/CSharp/CSharpDirectivesTest.cs | 88 +++++++ .../AddOrRemoveTagHelperSpanVisitorTest.cs | 219 ++++++++++++++++++ .../Output/BasicTagHelpers.RemoveTagHelper.cs | 25 ++ .../CS/Output/RemoveTagHelperDirective.cs | 33 +++ .../BasicTagHelpers.RemoveTagHelper.cshtml | 10 + .../CS/Source/RemoveTagHelperDirective.cshtml | 1 + 10 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.AspNet.Razor.Test/TagHelpers/AddOrRemoveTagHelperSpanVisitorTest.cs create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.RemoveTagHelper.cs create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/RemoveTagHelperDirective.cs create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/BasicTagHelpers.RemoveTagHelper.cshtml create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/RemoveTagHelperDirective.cshtml diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs index 018d11683e..51ca91bb3c 100644 --- a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs @@ -20,6 +20,7 @@ namespace Microsoft.AspNet.Razor.Parser internal static ISet DefaultKeywords = new HashSet() { SyntaxConstants.CSharp.AddTagHelperKeyword, + SyntaxConstants.CSharp.RemoveTagHelperKeyword, "if", "do", "try", diff --git a/test/Microsoft.AspNet.Razor.Test/Framework/TestSpanBuilder.cs b/test/Microsoft.AspNet.Razor.Test/Framework/TestSpanBuilder.cs index ea4b8b7997..79351c251e 100644 --- a/test/Microsoft.AspNet.Razor.Test/Framework/TestSpanBuilder.cs +++ b/test/Microsoft.AspNet.Razor.Test/Framework/TestSpanBuilder.cs @@ -302,7 +302,14 @@ namespace Microsoft.AspNet.Razor.Test.Framework public SpanConstructor AsAddTagHelper(string lookupText) { - return _self.With(new AddTagHelperCodeGenerator(lookupText)); + return _self.With( + new AddOrRemoveTagHelperCodeGenerator(removeTagHelperDescriptors: false, lookupText: lookupText)); + } + + public SpanConstructor AsRemoveTagHelper(string lookupText) + { + return _self.With( + new AddOrRemoveTagHelperCodeGenerator(removeTagHelperDescriptors: true, lookupText: lookupText)); } public SpanConstructor As(ISpanCodeGenerator codeGenerator) diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs index be9929ab5e..f2acd0b425 100644 --- a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs @@ -11,6 +11,23 @@ namespace Microsoft.AspNet.Razor.Test.Generator { public class CSharpTagHelperRenderingTest : TagHelperTestBase { + [Fact] + public void CSharpCodeGenerator_CorrectlyGeneratesMappings_ForRemoveTagHelperDirective() + { + // Act & Assert + RunTagHelperTest("RemoveTagHelperDirective", + designTimeMode: true, + expectedDesignTimePragmas: new List() + { + BuildLineMapping(documentAbsoluteIndex: 17, + documentLineIndex: 0, + generatedAbsoluteIndex: 442, + generatedLineIndex: 14, + characterOffsetIndex: 17, + contentLength: 11) + }); + } + [Fact] public void CSharpCodeGenerator_CorrectlyGeneratesMappings_ForAddTagHelperDirective() { @@ -65,6 +82,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator [Theory] [InlineData("SingleTagHelper")] [InlineData("BasicTagHelpers")] + [InlineData("BasicTagHelpers.RemoveTagHelper")] [InlineData("ComplexTagHelpers")] public void TagHelpers_GenerateExpectedOutput(string testType) { diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/CodeTree/CodeTreeBuilderTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/CodeTree/CodeTreeBuilderTest.cs index f11ef92626..905ae015b0 100644 --- a/test/Microsoft.AspNet.Razor.Test/Generator/CodeTree/CodeTreeBuilderTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Generator/CodeTree/CodeTreeBuilderTest.cs @@ -36,6 +36,30 @@ namespace Microsoft.AspNet.Razor Assert.Equal(addTagHelperChunk.LookupText, "some text"); } + [Fact] + public void AddRemoveTagHelperChunk_AddsChunkToTopLevelCodeTree() + { + // Arrange + var spanFactory = SpanFactory.CreateCsHtml(); + var builder = new CodeTreeBuilder(); + var block = new ExpressionBlock(); + var removeTagHelperDirective = spanFactory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " "); + + // Act + builder.StartChunkBlock(block); + builder.AddRemoveTagHelperChunk("some text", removeTagHelperDirective); + builder.EndChunkBlock(); + + // Assert + Assert.Equal(2, builder.CodeTree.Chunks.Count); + + var chunkBlock = Assert.IsType(builder.CodeTree.Chunks.First()); + Assert.Empty(chunkBlock.Children); + + var removeTagHelperChunk = Assert.IsType(builder.CodeTree.Chunks.Last()); + Assert.Equal(removeTagHelperChunk.LookupText, "some text"); + } + [Fact] public void AddLiteralChunk_AddsChunkToCodeTree() { diff --git a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpDirectivesTest.cs b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpDirectivesTest.cs index 07e1234ad6..5e79d22274 100644 --- a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpDirectivesTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpDirectivesTest.cs @@ -12,6 +12,94 @@ namespace Microsoft.AspNet.Razor.Test.Parser.CSharp { public class CSharpDirectivesTest : CsHtmlCodeParserTestBase { + [Fact] + public void RemoveTagHelperDirective_Succeeds() + { + ParseBlockTest("@removetaghelper \"Foo\"", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"Foo\"").AsRemoveTagHelper("Foo"))); + } + + [Fact] + public void RemoveTagHelperDirective_SupportsSpaces() + { + ParseBlockTest("@removetaghelper \" Foo, Bar \" ", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\" Foo, Bar \" ").AsRemoveTagHelper(" Foo, Bar "))); + } + + [Fact] + public void RemoveTagHelperDirective_RequiresValue() + { + ParseBlockTest("@removetaghelper ", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp().AsRemoveTagHelper(string.Empty)), + new RazorError( + RazorResources.FormatParseError_DirectiveMustHaveValue( + SyntaxConstants.CSharp.RemoveTagHelperKeyword), + absoluteIndex: 17, lineIndex: 0, columnIndex: 17)); + } + + [Fact] + public void RemoveTagHelperDirective_StartQuoteRequiresDoubleQuotesAroundValue() + { + ParseBlockTest("@removetaghelper \"Foo", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"Foo").AsRemoveTagHelper("Foo")), + new RazorError( + RazorResources.ParseError_Unterminated_String_Literal, + absoluteIndex: 17, lineIndex: 0, columnIndex: 17), + new RazorError( + RazorResources.FormatParseError_DirectiveMustBeSurroundedByQuotes( + SyntaxConstants.CSharp.RemoveTagHelperKeyword), + absoluteIndex: 17, lineIndex: 0, columnIndex: 17)); + } + + [Fact] + public void RemoveTagHelperDirective_EndQuoteRequiresDoubleQuotesAroundValue() + { + ParseBlockTest("@removetaghelper Foo\"", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("Foo\"").AsRemoveTagHelper("Foo")), + new RazorError( + RazorResources.ParseError_Unterminated_String_Literal, + absoluteIndex: 20, lineIndex: 0, columnIndex: 20), + new RazorError( + RazorResources.FormatParseError_DirectiveMustBeSurroundedByQuotes( + SyntaxConstants.CSharp.RemoveTagHelperKeyword), + absoluteIndex: 17, lineIndex: 0, columnIndex: 17)); + } + + [Fact] + public void RemoveTagHelperDirective_RequiresDoubleQuotesAroundValue() + { + ParseBlockTest("@removetaghelper Foo", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("Foo").AsRemoveTagHelper("Foo")), + new RazorError( + RazorResources.FormatParseError_DirectiveMustBeSurroundedByQuotes( + SyntaxConstants.CSharp.RemoveTagHelperKeyword), + absoluteIndex: 17, lineIndex: 0, columnIndex: 17)); + } + [Fact] public void AddTagHelperDirective_Succeeds() { diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/AddOrRemoveTagHelperSpanVisitorTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/AddOrRemoveTagHelperSpanVisitorTest.cs new file mode 100644 index 0000000000..2d6fd620d3 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/AddOrRemoveTagHelperSpanVisitorTest.cs @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Parser.TagHelpers; +using Microsoft.AspNet.Razor.Test.Framework; +#if !ASPNETCORE50 +using Moq; +#endif +using Xunit; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + public class AddOrRemoveTagHelperSpanVisitorTest + { + private static readonly SpanFactory Factory = SpanFactory.CreateCsHtml(); + + private static TagHelperDescriptor PTagHelperDescriptor + { + get + { + return new TagHelperDescriptor("p", "PTagHelper", ContentBehavior.None); + } + } + + private static TagHelperDescriptor DivTagHelperDescriptor + { + get + { + return new TagHelperDescriptor("div", "DivTagHelper", ContentBehavior.None); + } + } + +#if !ASPNETCORE50 + [Fact] + public void GetDescriptors_InvokesResolveForEachLookup() + { + // Arrange + var resolver = new Mock(); + resolver.Setup(mock => mock.Resolve(It.IsAny())).Returns(Enumerable.Empty()); + var addOrRemoveTagHelperSpanVisitor = new AddOrRemoveTagHelperSpanVisitor(resolver.Object); + var document = new MarkupBlock( + Factory.Code("\"one\"").AsAddTagHelper("one"), + Factory.Code("\"two\"").AsRemoveTagHelper("two"), + Factory.Code("\"three\"").AsRemoveTagHelper("three")); + + // Act + var descriptors = addOrRemoveTagHelperSpanVisitor.GetDescriptors(document); + + // Assert + Assert.Empty(descriptors); + resolver.Verify(mock => mock.Resolve(It.IsAny()), Times.Exactly(3)); + } +#endif + + [Fact] + public void GetDescriptors_LocatesNestedRemoveTagHelperCodeGenerator() + { + // Arrange + var resolver = new TestTagHelperDescriptorResolver( + new Dictionary> + { + { "something", new[] { PTagHelperDescriptor } } + }); + var addOrRemoveTagHelperSpanVisitor = new AddOrRemoveTagHelperSpanVisitor(resolver); + var document = new MarkupBlock( + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"something\"").AsRemoveTagHelper("something")) + ); + + // Act + var descriptors = addOrRemoveTagHelperSpanVisitor.GetDescriptors(document); + + // Assert + Assert.Empty(descriptors); + var lookup = Assert.Single(resolver.Lookups); + Assert.Equal("something", lookup); + } + + [Fact] + public void GetDescriptors_RemovesSpecifiedTagHelper() + { + // Arrange + var resolver = new TestTagHelperDescriptorResolver( + new Dictionary> + { + { "twoTagHelpers", new[] { PTagHelperDescriptor, DivTagHelperDescriptor } }, + { "singleTagHelper", new [] { PTagHelperDescriptor } } + }); + var addOrRemoveTagHelperSpanVisitor = new AddOrRemoveTagHelperSpanVisitor(resolver); + var document = new MarkupBlock( + Factory.Code("\"twoTagHelpers\"").AsAddTagHelper("twoTagHelpers"), + Factory.Code("\"singleTagHelper\"").AsRemoveTagHelper("singleTagHelper")); + + // Act + var descriptors = addOrRemoveTagHelperSpanVisitor.GetDescriptors(document); + + // Assert + var descriptor = Assert.Single(descriptors); + Assert.Equal(DivTagHelperDescriptor, descriptor, TagHelperDescriptorComparer.Default); + Assert.Equal(2, resolver.Lookups.Count); + Assert.Contains("twoTagHelpers", resolver.Lookups); + Assert.Contains("singleTagHelper", resolver.Lookups); + } + + [Fact] + public void GetDescriptors_RemovesAddedTagHelpers() + { + // Arrange + var resolver = new TestTagHelperDescriptorResolver( + new Dictionary> + { + { "twoTagHelpers", new[] { PTagHelperDescriptor, DivTagHelperDescriptor } } + }); + var addOrRemoveTagHelperSpanVisitor = new AddOrRemoveTagHelperSpanVisitor(resolver); + var document = new MarkupBlock( + Factory.Code("\"twoTagHelpers\"").AsAddTagHelper("twoTagHelpers"), + Factory.Code("\"twoTagHelpers\"").AsRemoveTagHelper("twoTagHelpers")); + + // Act + var descriptors = addOrRemoveTagHelperSpanVisitor.GetDescriptors(document); + + // Assert + Assert.Empty(descriptors); + Assert.Equal(Enumerable.Repeat("twoTagHelpers", 2), resolver.Lookups); + } + + [Fact] + public void GetDescriptors_RemoveTagHelper_OrderMatters() + { + // Arrange + var expectedDescriptors = new[] { PTagHelperDescriptor, DivTagHelperDescriptor }; + var resolver = new TestTagHelperDescriptorResolver( + new Dictionary> + { + { "twoTagHelpers", expectedDescriptors } + }); + var addOrRemoveTagHelperSpanVisitor = new AddOrRemoveTagHelperSpanVisitor(resolver); + var document = new MarkupBlock( + Factory.Code("\"twoTagHelpers\"").AsRemoveTagHelper("twoTagHelpers"), + Factory.Code("\"twoTagHelpers\"").AsAddTagHelper("twoTagHelpers")); + + // Act + var descriptors = addOrRemoveTagHelperSpanVisitor.GetDescriptors(document); + + // Assert + Assert.Equal(expectedDescriptors, descriptors, TagHelperDescriptorComparer.Default); + Assert.Equal(Enumerable.Repeat("twoTagHelpers", 2), resolver.Lookups); + } + + [Fact] + public void GetDescriptors_RemoveTagHelperInDocument_ThrowsIfNullResolver() + { + // Arrange + var addOrRemoveTagHelperSpanVisitor = new AddOrRemoveTagHelperSpanVisitor(descriptorResolver: null); + var document = new MarkupBlock( + Factory.Code("\"something\"").AsRemoveTagHelper("something")); + var expectedMessage = "Cannot use directive 'removetaghelper' when a Microsoft.AspNet.Razor.TagHelpers." + + "ITagHelperDescriptorResolver has not been provided to the Microsoft.AspNet.Razor." + + "Parser.RazorParser."; + + // Act & Assert + var ex = Assert.Throws(() => + { + addOrRemoveTagHelperSpanVisitor.GetDescriptors(document); + }); + + Assert.Equal(expectedMessage, ex.Message); + } + + [Fact] + public void GetDescriptors_RemoveTagHelperNotInDocument_DoesNotThrow() + { + // Arrange + var addOrRemoveTagHelperSpanVisitor = new AddOrRemoveTagHelperSpanVisitor(descriptorResolver: null); + var document = new MarkupBlock(Factory.Markup("Hello World")); + + // Act & Assert + Assert.DoesNotThrow(() => addOrRemoveTagHelperSpanVisitor.GetDescriptors(document)); + } + + // TODO: Add @addtaghelper directive unit tests. Tracked by https://github.com/aspnet/Razor/issues/202. + + private class TestTagHelperDescriptorResolver : ITagHelperDescriptorResolver + { + private readonly IReadOnlyDictionary> _lookupTable; + + public TestTagHelperDescriptorResolver( + IReadOnlyDictionary> lookupTable) + { + _lookupTable = lookupTable; + + Lookups = new List(); + } + + public List Lookups { get; private set; } + + public IEnumerable Resolve(string lookupText) + { + Lookups.Add(lookupText); + + IEnumerable descriptors; + if (_lookupTable.TryGetValue(lookupText, out descriptors)) + { + return descriptors; + } + + return Enumerable.Empty(); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.RemoveTagHelper.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.RemoveTagHelper.cs new file mode 100644 index 0000000000..06b68da503 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/BasicTagHelpers.RemoveTagHelper.cs @@ -0,0 +1,25 @@ +#pragma checksum "BasicTagHelpers.RemoveTagHelper.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "631e4af360bd37f540b637596d5f059c7abc0ac2" +namespace TestOutput +{ + using System; + using System.Threading.Tasks; + + public class BasicTagHelpers.RemoveTagHelper + { + #line hidden + public BasicTagHelpers.RemoveTagHelper() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + Instrumentation.BeginContext(60, 187, true); + WriteLiteral("\r\n
\r\n

\r\n " + +"

\r\n \r\n \r\n

\r\n
"); + Instrumentation.EndContext(); + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/RemoveTagHelperDirective.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/RemoveTagHelperDirective.cs new file mode 100644 index 0000000000..00669fada2 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/RemoveTagHelperDirective.cs @@ -0,0 +1,33 @@ +namespace TestOutput +{ + using System; + using System.Threading.Tasks; + + public class RemoveTagHelperDirective + { + private static object @__o; + private void @__RazorDesignTimeHelpers__() + { + #pragma warning disable 219 + string __tagHelperDirectiveSyntaxHelper = null; + __tagHelperDirectiveSyntaxHelper = +#line 1 "RemoveTagHelperDirective.cshtml" + "something" + +#line default +#line hidden + ; + #pragma warning restore 219 + } + #line hidden + public RemoveTagHelperDirective() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/BasicTagHelpers.RemoveTagHelper.cshtml b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/BasicTagHelpers.RemoveTagHelper.cshtml new file mode 100644 index 0000000000..f0096f010b --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/BasicTagHelpers.RemoveTagHelper.cshtml @@ -0,0 +1,10 @@ +@addtaghelper "something" +@removetaghelper "doesntmatter" + +
+

+

+ + +

+
\ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/RemoveTagHelperDirective.cshtml b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/RemoveTagHelperDirective.cshtml new file mode 100644 index 0000000000..24b869d835 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/RemoveTagHelperDirective.cshtml @@ -0,0 +1 @@ +@removetaghelper "something" \ No newline at end of file