From 4345b06e88fb0df16c24b531a429dbb15289b1f8 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Tue, 17 Mar 2015 21:37:40 -0700 Subject: [PATCH] Add tests to validate TagHelper attribute targeting. - Added tests to validate TargetElementAttribute, TagHelperDescriptorFactory, CSharpTagHelperCodeRenderer and TagHelperParseTreeRewriterTests. - Renamed HtmlElementNameAttributeTest to TargetElementAttributeTest. #311 --- ...aseSensitiveTagHelperDescriptorComparer.cs | 89 ++ .../CompleteTagHelperDescriptorComparer.cs | 64 -- .../HtmlElementNameAttributeTest.cs | 71 -- .../TagHelperDescriptorFactoryTest.cs | 678 ++++++++++++-- .../TagHelperDescriptorResolverTest.cs | 7 +- .../Generator/CSharpTagHelperRenderingTest.cs | 94 +- .../TagHelperDescriptorProviderTest.cs | 185 +++- .../TagHelperParseTreeRewriterTest.cs | 886 +++++++++++++++++- ...AttributeTargetingTagHelpers.DesignTime.cs | 60 ++ .../CS/Output/AttributeTargetingTagHelpers.cs | 100 ++ .../AttributeTargetingTagHelpers.cshtml | 8 + 11 files changed, 1991 insertions(+), 251 deletions(-) create mode 100644 test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperDescriptorComparer.cs delete mode 100644 test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CompleteTagHelperDescriptorComparer.cs delete mode 100644 test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/HtmlElementNameAttributeTest.cs create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/AttributeTargetingTagHelpers.DesignTime.cs create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/AttributeTargetingTagHelpers.cs create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/AttributeTargetingTagHelpers.cshtml diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperDescriptorComparer.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperDescriptorComparer.cs new file mode 100644 index 0000000000..6bc5acdbab --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperDescriptorComparer.cs @@ -0,0 +1,89 @@ +// 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.TagHelpers; +using Microsoft.Internal.Web.Utils; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + public class CaseSensitiveTagHelperDescriptorComparer : TagHelperDescriptorComparer, IEqualityComparer + { + public new static readonly CaseSensitiveTagHelperDescriptorComparer Default = + new CaseSensitiveTagHelperDescriptorComparer(); + + private CaseSensitiveTagHelperDescriptorComparer() + { + } + + bool IEqualityComparer.Equals( + TagHelperDescriptor descriptorX, + TagHelperDescriptor descriptorY) + { + return base.Equals(descriptorX, descriptorY) && + // Normal comparer doesn't care about the case, required attribute order, attributes or prefixes. + // In tests we do. + string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.Ordinal) && + string.Equals(descriptorX.Prefix, descriptorY.Prefix, StringComparison.Ordinal) && + Enumerable.SequenceEqual( + descriptorX.RequiredAttributes, + descriptorY.RequiredAttributes, + StringComparer.Ordinal) && + descriptorX.Attributes.SequenceEqual( + descriptorY.Attributes, + CaseSensitiveAttributeDescriptorComparer.Default); + } + + int IEqualityComparer.GetHashCode(TagHelperDescriptor descriptor) + { + var hashCodeCombiner = HashCodeCombiner + .Start() + .Add(base.GetHashCode()) + .Add(descriptor.TagName, StringComparer.Ordinal) + .Add(descriptor.Prefix); + + foreach (var requiredAttribute in descriptor.RequiredAttributes) + { + hashCodeCombiner.Add(requiredAttribute, StringComparer.Ordinal); + } + + foreach (var attribute in descriptor.Attributes) + { + hashCodeCombiner.Add(CaseSensitiveAttributeDescriptorComparer.Default.GetHashCode(attribute)); + } + + return hashCodeCombiner.CombinedHash; + } + + private class CaseSensitiveAttributeDescriptorComparer : IEqualityComparer + { + public static readonly CaseSensitiveAttributeDescriptorComparer Default = + new CaseSensitiveAttributeDescriptorComparer(); + + private CaseSensitiveAttributeDescriptorComparer() + { + } + + public bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY) + { + return + // Normal comparer doesn't care about case, in tests we do. + string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal) && + string.Equals(descriptorX.PropertyName, descriptorY.PropertyName, StringComparison.Ordinal) && + string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal); + } + + public int GetHashCode(TagHelperAttributeDescriptor descriptor) + { + return HashCodeCombiner + .Start() + .Add(descriptor.Name, StringComparer.Ordinal) + .Add(descriptor.PropertyName, StringComparer.Ordinal) + .Add(descriptor.TypeName, StringComparer.Ordinal) + .CombinedHash; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CompleteTagHelperDescriptorComparer.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CompleteTagHelperDescriptorComparer.cs deleted file mode 100644 index 2897de31cb..0000000000 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CompleteTagHelperDescriptorComparer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// 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.TagHelpers; -using Microsoft.Internal.Web.Utils; - -namespace Microsoft.AspNet.Razor.Runtime.TagHelpers -{ - public class CompleteTagHelperDescriptorComparer : TagHelperDescriptorComparer, IEqualityComparer - { - public new static readonly CompleteTagHelperDescriptorComparer Default = - new CompleteTagHelperDescriptorComparer(); - - private CompleteTagHelperDescriptorComparer() - { - } - - bool IEqualityComparer.Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY) - { - return base.Equals(descriptorX, descriptorY) && - // Tests should be exact casing - string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.Ordinal) && - descriptorX.Attributes.SequenceEqual(descriptorY.Attributes, - CompleteTagHelperAttributeDescriptorComparer.Default); - } - - int IEqualityComparer.GetHashCode(TagHelperDescriptor descriptor) - { - return HashCodeCombiner.Start() - .Add(base.GetHashCode()) - .Add(descriptor.Attributes) - .CombinedHash; - } - - private class CompleteTagHelperAttributeDescriptorComparer : IEqualityComparer - { - public static readonly CompleteTagHelperAttributeDescriptorComparer Default = - new CompleteTagHelperAttributeDescriptorComparer(); - - private CompleteTagHelperAttributeDescriptorComparer() - { - } - - public bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY) - { - return string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal) && - string.Equals(descriptorX.PropertyName, descriptorY.PropertyName, StringComparison.Ordinal) && - string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal); - } - - public int GetHashCode(TagHelperAttributeDescriptor descriptor) - { - return HashCodeCombiner.Start() - .Add(descriptor.Name, StringComparer.Ordinal) - .Add(descriptor.PropertyName, StringComparer.Ordinal) - .Add(descriptor.TypeName, StringComparer.Ordinal) - .CombinedHash; - } - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/HtmlElementNameAttributeTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/HtmlElementNameAttributeTest.cs deleted file mode 100644 index 43839a6cae..0000000000 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/HtmlElementNameAttributeTest.cs +++ /dev/null @@ -1,71 +0,0 @@ -// 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 Xunit; - -namespace Microsoft.AspNet.Razor.Runtime.TagHelpers -{ - public class HtmlElementNameAttributeTest - { - public static TheoryData InvalidTagNameData - { - get - { - var invalidTagNameError = - "Tag helpers cannot target element name '{0}' because it contains a '{1}' character."; - var nullOrWhitespaceTagNameError = - "Tag name cannot be null or whitespace."; - - // tagName, expectedExceptionMessage - return new TheoryData - { - { "!", string.Format(invalidTagNameError, "!", "!") }, - { "hello!", string.Format(invalidTagNameError, "hello!", "!") }, - { "!hello", string.Format(invalidTagNameError, "!hello", "!") }, - { "he!lo", string.Format(invalidTagNameError, "he!lo", "!") }, - { "!he!lo!", string.Format(invalidTagNameError, "!he!lo!", "!") }, - { string.Empty, nullOrWhitespaceTagNameError }, - { Environment.NewLine, nullOrWhitespaceTagNameError }, - { "\t", nullOrWhitespaceTagNameError }, - { " \t ", nullOrWhitespaceTagNameError }, - { " ", nullOrWhitespaceTagNameError }, - { Environment.NewLine + " ", nullOrWhitespaceTagNameError }, - { null, nullOrWhitespaceTagNameError }, - }; - } - } - - [Theory] - [MemberData(nameof(InvalidTagNameData))] - public void SingleArgumentConstructor_ThrowsOnInvalidTagNames( - string tagName, - string expectedExceptionMessage) - { - // Arrange - expectedExceptionMessage += Environment.NewLine + "Parameter name: tag"; - - // Act & Assert - var exception = Assert.Throws( - "tag", - () => new HtmlElementNameAttribute(tagName)); - Assert.Equal(exception.Message, expectedExceptionMessage); - } - - [Theory] - [MemberData(nameof(InvalidTagNameData))] - public void MultipleArgumentConstructor_ThrowsOnInvalidTagNames( - string tagName, - string expectedExceptionMessage) - { - // Arrange - expectedExceptionMessage += Environment.NewLine + "Parameter name: additionalTags"; - - // Act & Assert - var exception = Assert.Throws( - "additionalTags", - () => new HtmlElementNameAttribute("p", "div", "span", tagName)); - Assert.Equal(exception.Message, expectedExceptionMessage); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs index 4be74b02fa..6fd9e95275 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs @@ -2,8 +2,12 @@ // 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 System.Reflection; +using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.AspNet.Razor.Text; using Xunit; namespace Microsoft.AspNet.Razor.Runtime.TagHelpers @@ -13,6 +17,183 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers private static readonly string AssemblyName = typeof(TagHelperDescriptorFactoryTest).GetTypeInfo().Assembly.GetName().Name; + public static TheoryData AttributeTargetData + { + get + { + var attributes = Enumerable.Empty(); + + // tagHelperType, expectedDescriptors + return new TheoryData> + { + { + typeof(AttributeTargetingTagHelper), + new[] + { + new TagHelperDescriptor( + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + typeof(AttributeTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "class" }) + } + }, + { + typeof(MultiAttributeTargetingTagHelper), + new[] + { + new TagHelperDescriptor( + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + typeof(MultiAttributeTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "class", "style" }) + } + }, + { + typeof(MultiAttributeAttributeTargetingTagHelper), + new[] + { + new TagHelperDescriptor( + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + typeof(MultiAttributeAttributeTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "custom" }), + new TagHelperDescriptor( + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + typeof(MultiAttributeAttributeTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "class", "style" }) + } + }, + { + typeof(InheritedAttributeTargetingTagHelper), + new[] + { + new TagHelperDescriptor( + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + typeof(InheritedAttributeTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "style" }) + } + }, + { + typeof(RequiredAttributeTagHelper), + new[] + { + new TagHelperDescriptor( + "input", + typeof(RequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "class" }) + } + }, + { + typeof(InheritedRequiredAttributeTagHelper), + new[] + { + new TagHelperDescriptor( + "div", + typeof(InheritedRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "class" }) + } + }, + { + typeof(MultiAttributeRequiredAttributeTagHelper), + new[] + { + new TagHelperDescriptor( + "div", + typeof(MultiAttributeRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "class" }), + new TagHelperDescriptor( + "input", + typeof(MultiAttributeRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "class" }) + } + }, + { + typeof(MultiAttributeSameTagRequiredAttributeTagHelper), + new[] + { + new TagHelperDescriptor( + "input", + typeof(MultiAttributeSameTagRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "style" }), + new TagHelperDescriptor( + "input", + typeof(MultiAttributeSameTagRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "class" }) + } + }, + { + typeof(MultiRequiredAttributeTagHelper), + new[] + { + new TagHelperDescriptor( + "input", + typeof(MultiRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "class", "style" }) + } + }, + { + typeof(MultiTagMultiRequiredAttributeTagHelper), + new[] + { + new TagHelperDescriptor( + "div", + typeof(MultiTagMultiRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "class", "style" }), + new TagHelperDescriptor( + "input", + typeof(MultiTagMultiRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { "class", "style" }), + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(AttributeTargetData))] + public void CreateDescriptors_ReturnsExpectedDescriptors( + Type tagHelperType, + IEnumerable expectedDescriptors) + { + // Arrange + var errorSink = new ParserErrorSink(); + + // Act + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + tagHelperType, + errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + public static TheoryData HtmlCaseData { get @@ -39,10 +220,17 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers string expectedTagName, string expectedAttributeName) { - // Arrange & Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, tagHelperType); + // Arrange + var errorSink = new ParserErrorSink(); + + // Act + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + tagHelperType, + errorSink); // Assert + Assert.Empty(errorSink.Errors); var descriptor = Assert.Single(descriptors); Assert.Equal(expectedTagName, descriptor.TagName, StringComparer.Ordinal); var attributeDescriptor = Assert.Single(descriptor.Attributes); @@ -53,6 +241,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers public void CreateDescriptor_OverridesAttributeNameFromAttribute() { // Arrange + var errorSink = new ParserErrorSink(); var validProperty1 = typeof(OverriddenAttributeTagHelper).GetProperty( nameof(OverriddenAttributeTagHelper.ValidAttribute1)); var validProperty2 = typeof(OverriddenAttributeTagHelper).GetProperty( @@ -69,17 +258,21 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }; // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, - typeof(OverriddenAttributeTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + typeof(OverriddenAttributeTagHelper), + errorSink); // Assert - Assert.Equal(expectedDescriptors, descriptors, CompleteTagHelperDescriptorComparer.Default); + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] public void CreateDescriptor_DoesNotInheritOverridenAttributeName() { // Arrange + var errorSink = new ParserErrorSink(); var validProperty1 = typeof(InheritedOverriddenAttributeTagHelper).GetProperty( nameof(InheritedOverriddenAttributeTagHelper.ValidAttribute1)); var validProperty2 = typeof(InheritedOverriddenAttributeTagHelper).GetProperty( @@ -97,17 +290,21 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }; // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, - typeof(InheritedOverriddenAttributeTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + typeof(InheritedOverriddenAttributeTagHelper), + errorSink); // Assert - Assert.Equal(expectedDescriptors, descriptors, CompleteTagHelperDescriptorComparer.Default); + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] public void CreateDescriptor_AllowsOverridenAttributeNameOnUnimplementedVirtual() { // Arrange + var errorSink = new ParserErrorSink(); var validProperty1 = typeof(InheritedNotOverriddenAttributeTagHelper).GetProperty( nameof(InheritedNotOverriddenAttributeTagHelper.ValidAttribute1)); var validProperty2 = typeof(InheritedNotOverriddenAttributeTagHelper).GetProperty( @@ -124,33 +321,42 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }; // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, - typeof(InheritedNotOverriddenAttributeTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + typeof(InheritedNotOverriddenAttributeTagHelper), + errorSink); // Assert - Assert.Equal(expectedDescriptors, descriptors, CompleteTagHelperDescriptorComparer.Default); + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] public void CreateDescriptor_BuildsDescriptorsFromSimpleTypes() { // Arrange + var errorSink = new ParserErrorSink(); var objectAssemblyName = typeof(object).GetTypeInfo().Assembly.GetName().Name; var expectedDescriptor = new TagHelperDescriptor("object", "System.Object", objectAssemblyName); // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(objectAssemblyName, typeof(object)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + objectAssemblyName, + typeof(object), + errorSink); // Assert + Assert.Empty(errorSink.Errors); var descriptor = Assert.Single(descriptors); - Assert.Equal(expectedDescriptor, descriptor, CompleteTagHelperDescriptorComparer.Default); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] public void CreateDescriptor_BuildsDescriptorsWithInheritedProperties() { // Arrange + var errorSink = new ParserErrorSink(); var intProperty = typeof(InheritedSingleAttributeTagHelper).GetProperty( nameof(InheritedSingleAttributeTagHelper.IntAttribute)); var expectedDescriptor = new TagHelperDescriptor( @@ -162,18 +368,22 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }); // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, - typeof(InheritedSingleAttributeTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + typeof(InheritedSingleAttributeTagHelper), + errorSink); // Assert + Assert.Empty(errorSink.Errors); var descriptor = Assert.Single(descriptors); - Assert.Equal(expectedDescriptor, descriptor, CompleteTagHelperDescriptorComparer.Default); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] public void CreateDescriptor_BuildsDescriptorsWithConventionNames() { // Arrange + var errorSink = new ParserErrorSink(); var intProperty = typeof(SingleAttributeTagHelper).GetProperty(nameof(SingleAttributeTagHelper.IntAttribute)); var expectedDescriptor = new TagHelperDescriptor( "single-attribute", @@ -184,18 +394,22 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }); // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, - typeof(SingleAttributeTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + typeof(SingleAttributeTagHelper), + new ParserErrorSink()); // Assert + Assert.Empty(errorSink.Errors); var descriptor = Assert.Single(descriptors); - Assert.Equal(expectedDescriptor, descriptor, CompleteTagHelperDescriptorComparer.Default); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] public void CreateDescriptor_OnlyAcceptsPropertiesWithGetAndSet() { // Arrange + var errorSink = new ParserErrorSink(); var validProperty = typeof(MissingAccessorTagHelper).GetProperty( nameof(MissingAccessorTagHelper.ValidAttribute)); var expectedDescriptor = new TagHelperDescriptor( @@ -207,18 +421,22 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }); // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, - typeof(MissingAccessorTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + typeof(MissingAccessorTagHelper), + errorSink); // Assert + Assert.Empty(errorSink.Errors); var descriptor = Assert.Single(descriptors); - Assert.Equal(expectedDescriptor, descriptor, CompleteTagHelperDescriptorComparer.Default); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] public void CreateDescriptor_OnlyAcceptsPropertiesWithPublicGetAndSet() { // Arrange + var errorSink = new ParserErrorSink(); var validProperty = typeof(PrivateAccessorTagHelper).GetProperty( nameof(PrivateAccessorTagHelper.ValidAttribute)); var expectedDescriptor = new TagHelperDescriptor( @@ -231,29 +449,33 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }); // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, - typeof(PrivateAccessorTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + typeof(PrivateAccessorTagHelper), + errorSink); // Assert + Assert.Empty(errorSink.Errors); var descriptor = Assert.Single(descriptors); - Assert.Equal(expectedDescriptor, descriptor, CompleteTagHelperDescriptorComparer.Default); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] public void CreateDescriptor_ResolvesMultipleTagHelperDescriptorsFromSingleType() { // Arrange + var errorSink = new ParserErrorSink(); var validProp = typeof(MultiTagTagHelper).GetProperty(nameof(MultiTagTagHelper.ValidAttribute)); var expectedDescriptors = new[] { new TagHelperDescriptor( - "div", + "p", typeof(MultiTagTagHelper).FullName, AssemblyName, new[] { new TagHelperAttributeDescriptor("valid-attribute", validProp) }), new TagHelperDescriptor( - "p", + "div", typeof(MultiTagTagHelper).FullName, AssemblyName, new[] { @@ -262,16 +484,21 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }; // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, typeof(MultiTagTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + typeof(MultiTagTagHelper), + errorSink); // Assert - Assert.Equal(expectedDescriptors, descriptors, CompleteTagHelperDescriptorComparer.Default); + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] public void CreateDescriptor_DoesntResolveInheritedTagNames() { // Arrange + var errorSink = new ParserErrorSink(); var validProp = typeof(InheritedMultiTagTagHelper).GetProperty(nameof(InheritedMultiTagTagHelper.ValidAttribute)); var expectedDescriptor = new TagHelperDescriptor( "inherited-multi-tag", @@ -282,18 +509,22 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }); // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, - typeof(InheritedMultiTagTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + typeof(InheritedMultiTagTagHelper), + errorSink); // Assert + Assert.Empty(errorSink.Errors); var descriptor = Assert.Single(descriptors); - Assert.Equal(expectedDescriptor, descriptor, CompleteTagHelperDescriptorComparer.Default); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] public void CreateDescriptor_IgnoresDuplicateTagNamesFromAttribute() { // Arrange + var errorSink = new ParserErrorSink(); var expectedDescriptors = new[] { new TagHelperDescriptor( "p", @@ -306,17 +537,21 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }; // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, - typeof(DuplicateTagNameTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + typeof(DuplicateTagNameTagHelper), + errorSink); // Assert - Assert.Equal(expectedDescriptors, descriptors, CompleteTagHelperDescriptorComparer.Default); + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] public void CreateDescriptor_OverridesTagNameFromAttribute() { // Arrange + var errorSink = new ParserErrorSink(); var expectedDescriptors = new[] { new TagHelperDescriptor("data-condition", typeof(OverrideNameTagHelper).FullName, @@ -324,41 +559,353 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers }; // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, - typeof(OverrideNameTagHelper)); + var descriptors = TagHelperDescriptorFactory.CreateDescriptors( + AssemblyName, + typeof(OverrideNameTagHelper), + errorSink); // Assert - Assert.Equal(expectedDescriptors, descriptors, CompleteTagHelperDescriptorComparer.Default); + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + public static TheoryData InvalidNameData + { + get + { + var invalidNameError = + "Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character."; + var nullOrWhitespaceNameError = + "{0} name cannot be null or whitespace."; + Func onNameError = (invalidText, invalidCharacter) => + string.Format(invalidNameError, "tag", invalidText, invalidCharacter); + + // name, expectedErrorMessages + return new TheoryData + { + { "!", new[] { onNameError("!", "!") } }, + { "hello!", new[] { onNameError("hello!", "!") } }, + { "!hello", new[] { onNameError("!hello", "!") } }, + { "he!lo", new[] { onNameError("he!lo", "!") } }, + { + "!he!lo!", + new[] + { + onNameError("!he!lo!", "!"), + onNameError("!he!lo!", "!"), + onNameError("!he!lo!", "!") + } + }, + { "@", new[] { onNameError("@", "@") } }, + { "hello@", new[] { onNameError("hello@", "@") } }, + { "@hello", new[] { onNameError("@hello", "@") } }, + { "he@lo", new[] { onNameError("he@lo", "@") } }, + { + "@he@lo@", + new[] + { + onNameError("@he@lo@", "@"), + onNameError("@he@lo@", "@"), + onNameError("@he@lo@", "@") + } + }, + { "/", new[] { onNameError("/", "/") } }, + { "hello/", new[] { onNameError("hello/", "/") } }, + { "/hello", new[] { onNameError("/hello", "/") } }, + { "he/lo", new[] { onNameError("he/lo", "/") } }, + { + "/he/lo/", + new[] { + onNameError("/he/lo/", "/"), + onNameError("/he/lo/", "/"), + onNameError("/he/lo/", "/") + } + }, + { "<", new[] { onNameError("<", "<") } }, + { "hello<", new[] { onNameError("hello<", "<") } }, + { "", new[] { onNameError(">", ">") } }, + { "hello>", new[] { onNameError("hello>", ">") } }, + { ">hello", new[] { onNameError(">hello", ">") } }, + { "he>lo", new[] { onNameError("he>lo", ">") } }, + { + ">he>lo>", + new[] + { + onNameError(">he>lo>", ">"), + onNameError(">he>lo>", ">"), + onNameError(">he>lo>", ">") + } + }, + { "]", new[] { onNameError("]", "]") } }, + { "hello]", new[] { onNameError("hello]", "]") } }, + { "]hello", new[] { onNameError("]hello", "]") } }, + { "he]lo", new[] { onNameError("he]lo", "]") } }, + { + "]he]lo]", + new[] + { + onNameError("]he]lo]", "]"), + onNameError("]he]lo]", "]"), + onNameError("]he]lo]", "]") + } + }, + { "=", new[] { onNameError("=", "=") } }, + { "hello=", new[] { onNameError("hello=", "=") } }, + { "=hello", new[] { onNameError("=hello", "=") } }, + { "he=lo", new[] { onNameError("he=lo", "=") } }, + { + "=he=lo=", + new[] + { + onNameError("=he=lo=", "="), + onNameError("=he=lo=", "="), + onNameError("=he=lo=", "=") + } + }, + { "\"", new[] { onNameError("\"", "\"") } }, + { "hello\"", new[] { onNameError("hello\"", "\"") } }, + { "\"hello", new[] { onNameError("\"hello", "\"") } }, + { "he\"lo", new[] { onNameError("he\"lo", "\"") } }, + { + "\"he\"lo\"", + new[] + { + onNameError("\"he\"lo\"", "\""), + onNameError("\"he\"lo\"", "\""), + onNameError("\"he\"lo\"", "\"") + } + }, + { "'", new[] { onNameError("'", "'") } }, + { "hello'", new[] { onNameError("hello'", "'") } }, + { "'hello", new[] { onNameError("'hello", "'") } }, + { "he'lo", new[] { onNameError("he'lo", "'") } }, + { + "'he'lo'", + new[] + { + onNameError("'he'lo'", "'"), + onNameError("'he'lo'", "'"), + onNameError("'he'lo'", "'") + } + }, + { string.Empty, new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, + { Environment.NewLine, new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, + { "\t", new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, + { " \t ", new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, + { " ", new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, + { Environment.NewLine + " ", new[] { string.Format(nullOrWhitespaceNameError, "Tag") } }, + { + "! \t\r\n@/<>?[]=\"'", + new[] + { + onNameError("! \t\r\n@/<>?[]=\"'", "!"), + onNameError("! \t\r\n@/<>?[]=\"'", " "), + onNameError("! \t\r\n@/<>?[]=\"'", "\t"), + onNameError("! \t\r\n@/<>?[]=\"'", "\r"), + onNameError("! \t\r\n@/<>?[]=\"'", "\n"), + onNameError("! \t\r\n@/<>?[]=\"'", "@"), + onNameError("! \t\r\n@/<>?[]=\"'", "/"), + onNameError("! \t\r\n@/<>?[]=\"'", "<"), + onNameError("! \t\r\n@/<>?[]=\"'", ">"), + onNameError("! \t\r\n@/<>?[]=\"'", "?"), + onNameError("! \t\r\n@/<>?[]=\"'", "["), + onNameError("! \t\r\n@/<>?[]=\"'", "]"), + onNameError("! \t\r\n@/<>?[]=\"'", "="), + onNameError("! \t\r\n@/<>?[]=\"'", "\""), + onNameError("! \t\r\n@/<>?[]=\"'", "'"), + } + }, + { + "! \tv\ra\nl@i/d<>?[]=\"'", + new[] + { + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "!"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", " "), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\t"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\r"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\n"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "@"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "/"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "<"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", ">"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "?"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "["), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "]"), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "="), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "\""), + onNameError("! \tv\ra\nl@i/d<>?[]=\"'", "'"), + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(InvalidNameData))] + public void ValidTargetElementAttributeNames_CreatesErrorOnInvalidNames( + string name, string[] expectedErrorMessages) + { + // Arrange + var errorSink = new ParserErrorSink(); + var attribute = new TargetElementAttribute(name); + + // Act + TagHelperDescriptorFactory.ValidTargetElementAttributeNames(attribute, errorSink); + + // Assert + var errors = errorSink.Errors.ToArray(); + for (var i = 0; i < errors.Length; i++) + { + Assert.Equal(expectedErrorMessages[i], errors[i].Message); + Assert.Equal(SourceLocation.Zero, errors[i].Location); + } + } + + public static TheoryData ValidNameData + { + get + { + // name, expectedNames + return new TheoryData> + { + { "p", new[] { "p" } }, + { " p", new[] { "p" } }, + { "p ", new[] { "p" } }, + { " p ", new[] { "p" } }, + { "p,div", new[] { "p", "div" } }, + { " p,div", new[] { "p", "div" } }, + { "p ,div", new[] { "p", "div" } }, + { " p ,div", new[] { "p", "div" } }, + { "p, div", new[] { "p", "div" } }, + { "p,div ", new[] { "p", "div" } }, + { "p, div ", new[] { "p", "div" } }, + { " p, div ", new[] { "p", "div" } }, + { " p , div ", new[] { "p", "div" } }, + }; + } + } + + [Theory] + [MemberData(nameof(ValidNameData))] + public void GetCommaSeparatedValues_OutputsCommaSeparatedListOfNames( + string name, + IEnumerable expectedNames) + { + // Act + var result = TagHelperDescriptorFactory.GetCommaSeparatedValues(name); + + // Assert + Assert.Equal(expectedNames, result); } [Fact] - public void CreateDescriptor_GetsTagNamesFromMultipleAttributes() + public void GetCommaSeparatedValues_OutputsEmptyArrayForNullValue() { - // Arrange - var expectedDescriptors = new[] { - new TagHelperDescriptor( - "span", - typeof(MultipleAttributeTagHelper).FullName, - AssemblyName), - new TagHelperDescriptor( - "p", - typeof(MultipleAttributeTagHelper).FullName, - AssemblyName), - new TagHelperDescriptor( - "div", - typeof(MultipleAttributeTagHelper).FullName, - AssemblyName) - }; - // Act - var descriptors = TagHelperDescriptorFactory.CreateDescriptors(AssemblyName, - typeof(MultipleAttributeTagHelper)); + var result = TagHelperDescriptorFactory.GetCommaSeparatedValues(text: null); // Assert - Assert.Equal(expectedDescriptors, descriptors, CompleteTagHelperDescriptorComparer.Default); + Assert.Empty(result); } - [HtmlElementName("p", "div")] + [TargetElement(Attributes = "class")] + private class AttributeTargetingTagHelper : TagHelper + { + } + + [TargetElement(Attributes = "class,style")] + private class MultiAttributeTargetingTagHelper : TagHelper + { + } + + [TargetElement(Attributes = "custom")] + [TargetElement(Attributes = "class,style")] + private class MultiAttributeAttributeTargetingTagHelper : TagHelper + { + } + + [TargetElement(Attributes = "style")] + private class InheritedAttributeTargetingTagHelper : AttributeTargetingTagHelper + { + } + + [TargetElement("input", Attributes = "class")] + private class RequiredAttributeTagHelper : TagHelper + { + } + + [TargetElement("div", Attributes = "class")] + private class InheritedRequiredAttributeTagHelper : RequiredAttributeTagHelper + { + } + + [TargetElement("div", Attributes = "class")] + [TargetElement("input", Attributes = "class")] + private class MultiAttributeRequiredAttributeTagHelper : TagHelper + { + } + + [TargetElement("input", Attributes = "style")] + [TargetElement("input", Attributes = "class")] + private class MultiAttributeSameTagRequiredAttributeTagHelper : TagHelper + { + } + + [TargetElement("input", Attributes = "class,style")] + private class MultiRequiredAttributeTagHelper : TagHelper + { + } + + [TargetElement("div", Attributes = "style")] + private class InheritedMultiRequiredAttributeTagHelper : MultiRequiredAttributeTagHelper + { + } + + [TargetElement("div", Attributes = "class,style")] + [TargetElement("input", Attributes = "class,style")] + private class MultiTagMultiRequiredAttributeTagHelper : TagHelper + { + } + + [TargetElement("p")] + [TargetElement("div")] private class MultiTagTagHelper { public string ValidAttribute { get; set; } @@ -368,22 +915,19 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { } - [HtmlElementName("p", "p", "div", "div")] + [TargetElement("p")] + [TargetElement("p")] + [TargetElement("div")] + [TargetElement("div")] private class DuplicateTagNameTagHelper { } - [HtmlElementName("data-condition")] + [TargetElement("data-condition")] private class OverrideNameTagHelper { } - [HtmlElementName("span")] - [HtmlElementName("div", "p")] - private class MultipleAttributeTagHelper - { - } - private class InheritedSingleAttributeTagHelper : SingleAttributeTagHelper { } diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs index f8a3ab8f81..ae5d791fc3 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs @@ -1241,7 +1241,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers // Assert var descriptor = Assert.Single(descriptors); - Assert.Equal(Valid_PlainTagHelperDescriptor, descriptor, CompleteTagHelperDescriptorComparer.Default); + Assert.Equal(Valid_PlainTagHelperDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] @@ -1265,7 +1265,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers // Assert Assert.Equal(descriptors.Length, 2); - Assert.Equal(expectedDescriptors, descriptors, CompleteTagHelperDescriptorComparer.Default); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); } [Fact] @@ -1369,7 +1369,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers tagName, typeName, assemblyName, - attributes: Enumerable.Empty()); + attributes: Enumerable.Empty(), + requiredAttributes: Enumerable.Empty()); } private static TagHelperDescriptor CreatePrefixedValidPlainDescriptor(string prefix) diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs index 31c51b2ac1..90626b2740 100644 --- a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs @@ -19,6 +19,49 @@ namespace Microsoft.AspNet.Razor.Test.Generator private static IEnumerable PrefixedPAndInputTagHelperDescriptors => BuildPAndInputTagHelperDescriptors("THS"); + private static IEnumerable AttributeTargetingTagHelperDescriptors + { + get + { + var inputTypePropertyInfo = typeof(TestType).GetProperty("Type"); + var inputCheckedPropertyInfo = typeof(TestType).GetProperty("Checked"); + return new[] + { + new TagHelperDescriptor( + tagName: "p", + typeName: "PTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: new[] { "class" }), + new TagHelperDescriptor( + tagName: "input", + typeName: "InputTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor("type", inputTypePropertyInfo) + }, + requiredAttributes: new[] { "type" }), + new TagHelperDescriptor( + tagName: "input", + typeName: "InputTagHelper2", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[] + { + new TagHelperAttributeDescriptor("type", inputTypePropertyInfo), + new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo) + }, + requiredAttributes: new[] { "type", "checked" }), + new TagHelperDescriptor( + tagName: "*", + typeName: "CatchAllTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: new[] { "catchAll" }) + }; + } + } + public static TheoryData TagHelperDescriptorFlowTestData { get @@ -91,6 +134,20 @@ namespace Microsoft.AspNet.Razor.Test.Generator DefaultPAndInputTagHelperDescriptors, DefaultPAndInputTagHelperDescriptors, true + }, + { + "AttributeTargetingTagHelpers", + "AttributeTargetingTagHelpers", + AttributeTargetingTagHelperDescriptors, + AttributeTargetingTagHelperDescriptors, + false + }, + { + "AttributeTargetingTagHelpers", + "AttributeTargetingTagHelpers.DesignTime", + AttributeTargetingTagHelperDescriptors, + AttributeTargetingTagHelperDescriptors, + true } }; } @@ -294,6 +351,33 @@ namespace Microsoft.AspNet.Razor.Test.Generator contentLength: 4) } }, + { + "AttributeTargetingTagHelpers", + "AttributeTargetingTagHelpers.DesignTime", + AttributeTargetingTagHelperDescriptors, + new List + { + BuildLineMapping(documentAbsoluteIndex: 14, + documentLineIndex: 0, + generatedAbsoluteIndex: 501, + generatedLineIndex: 15, + characterOffsetIndex: 14, + contentLength: 14), + BuildLineMapping(documentAbsoluteIndex: 186, + documentLineIndex: 5, + generatedAbsoluteIndex: 1460, + generatedLineIndex: 41, + characterOffsetIndex: 36, + contentLength: 4), + BuildLineMapping(documentAbsoluteIndex: 232, + documentLineIndex: 6, + documentCharacterOffsetIndex: 36, + generatedAbsoluteIndex: 1827, + generatedLineIndex: 50, + generatedCharacterOffsetIndex: 36, + contentLength: 4) + } + }, }; } } @@ -328,6 +412,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator { "ComplexTagHelpers", DefaultPAndInputTagHelperDescriptors }, { "EmptyAttributeTagHelpers", DefaultPAndInputTagHelperDescriptors }, { "EscapedTagHelpers", DefaultPAndInputTagHelperDescriptors }, + { "AttributeTargetingTagHelpers", AttributeTargetingTagHelperDescriptors }, }; } } @@ -421,7 +506,8 @@ namespace Microsoft.AspNet.Razor.Test.Generator assemblyName: "SomeAssembly", attributes: new [] { new TagHelperAttributeDescriptor("age", pAgePropertyInfo) - }), + }, + requiredAttributes: Enumerable.Empty()), new TagHelperDescriptor( prefix, tagName: "input", @@ -429,7 +515,8 @@ namespace Microsoft.AspNet.Razor.Test.Generator assemblyName: "SomeAssembly", attributes: new TagHelperAttributeDescriptor[] { new TagHelperAttributeDescriptor("type", inputTypePropertyInfo) - }), + }, + requiredAttributes: Enumerable.Empty()), new TagHelperDescriptor( prefix, tagName: "input", @@ -438,7 +525,8 @@ namespace Microsoft.AspNet.Razor.Test.Generator attributes: new TagHelperAttributeDescriptor[] { new TagHelperAttributeDescriptor("type", inputTypePropertyInfo), new TagHelperAttributeDescriptor("checked", checkedPropertyInfo) - }) + }, + requiredAttributes: Enumerable.Empty()) }; } diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs index 978653e099..8d50fb1dfd 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs @@ -1,31 +1,143 @@ // 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.Collections.Generic; using System.Linq; -using Microsoft.AspNet.Razor.TagHelpers; using Xunit; -namespace Microsoft.AspNet.Razor.Test.TagHelpers +namespace Microsoft.AspNet.Razor.TagHelpers { public class TagHelperDescriptorProviderTest { - [Fact] - public void GetTagHelpers_ReturnsEmptyDescriptorsWithPrefixAsTagName() + public static TheoryData RequiredAttributeData + { + get + { + var divDescriptor = new TagHelperDescriptor( + tagName: "div", + typeName: "DivTagHelper", + assemblyName: "SomeAssembly", + attributes: Enumerable.Empty(), + requiredAttributes: new[] { "style" }); + var inputDescriptor = new TagHelperDescriptor( + tagName: "input", + typeName: "InputTagHelper", + assemblyName: "SomeAssembly", + attributes: Enumerable.Empty(), + requiredAttributes: new[] { "class", "style" }); + var catchAllDescriptor = new TagHelperDescriptor( + tagName: TagHelperDescriptorProvider.CatchAllDescriptorTarget, + typeName: "CatchAllTagHelper", + assemblyName: "SomeAssembly", + attributes: Enumerable.Empty(), + requiredAttributes: new[] { "class" }); + var catchAllDescriptor2 = new TagHelperDescriptor( + tagName: TagHelperDescriptorProvider.CatchAllDescriptorTarget, + typeName: "CatchAllTagHelper", + assemblyName: "SomeAssembly", + attributes: Enumerable.Empty(), + requiredAttributes: new[] { "custom", "class" }); + var defaultAvailableDescriptors = + new[] { divDescriptor, inputDescriptor, catchAllDescriptor, catchAllDescriptor2 }; + + return new TheoryData< + string, // tagName + IEnumerable, // providedAttributes + IEnumerable, // availableDescriptors + IEnumerable> // expectedDescriptors + { + { + "div", + new[] { "custom" }, + defaultAvailableDescriptors, + Enumerable.Empty() + }, + { "div", new[] { "style" }, defaultAvailableDescriptors, new[] { divDescriptor } }, + { "div", new[] { "class" }, defaultAvailableDescriptors, new[] { catchAllDescriptor } }, + { + "div", + new[] { "class", "style" }, + defaultAvailableDescriptors, + new[] { divDescriptor, catchAllDescriptor } + }, + { + "div", + new[] { "class", "style", "custom" }, + defaultAvailableDescriptors, + new[] { divDescriptor, catchAllDescriptor, catchAllDescriptor2 } + }, + { + "input", + new[] { "class", "style" }, + defaultAvailableDescriptors, + new[] { inputDescriptor, catchAllDescriptor } + }, + { + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + new[] { "custom" }, + defaultAvailableDescriptors, + Enumerable.Empty() + }, + { + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + new[] { "class" }, + defaultAvailableDescriptors, + new[] { catchAllDescriptor } + }, + { + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + new[] { "class", "style" }, + defaultAvailableDescriptors, + new[] { catchAllDescriptor } + }, + { + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + new[] { "class", "custom" }, + defaultAvailableDescriptors, + new[] { catchAllDescriptor, catchAllDescriptor2 } + }, + }; + } + } + + [Theory] + [MemberData(nameof(RequiredAttributeData))] + public void GetDescriptors_ReturnsDescriptorsWithRequiredAttributes( + string tagName, + IEnumerable providedAttributes, + IEnumerable availableDescriptors, + IEnumerable expectedDescriptors) { // Arrange - var catchAllDescriptor = CreatePrefixedDescriptor("th", "*", "foo1"); + var provider = new TagHelperDescriptorProvider(availableDescriptors); + + // Act + var resolvedDescriptors = provider.GetDescriptors(tagName, providedAttributes); + + // Assert + Assert.Equal(expectedDescriptors, resolvedDescriptors, TagHelperDescriptorComparer.Default); + } + + [Fact] + public void GetDescriptors_ReturnsEmptyDescriptorsWithPrefixAsTagName() + { + // Arrange + var catchAllDescriptor = CreatePrefixedDescriptor( + "th", + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + "foo1"); var descriptors = new[] { catchAllDescriptor }; var provider = new TagHelperDescriptorProvider(descriptors); // Act - var resolvedDescriptors = provider.GetTagHelpers("th"); + var resolvedDescriptors = provider.GetDescriptors("th", attributeNames: Enumerable.Empty()); // Assert Assert.Empty(resolvedDescriptors); } [Fact] - public void GetTagHelpers_OnlyUnderstandsSinglePrefix() + public void GetDescriptors_OnlyUnderstandsSinglePrefix() { // Arrange var divDescriptor = CreatePrefixedDescriptor("th:", "div", "foo1"); @@ -34,8 +146,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers var provider = new TagHelperDescriptorProvider(descriptors); // Act - var retrievedDescriptorsDiv = provider.GetTagHelpers("th:div"); - var retrievedDescriptorsSpan = provider.GetTagHelpers("th2:span"); + var retrievedDescriptorsDiv = provider.GetDescriptors("th:div", attributeNames: Enumerable.Empty()); + var retrievedDescriptorsSpan = provider.GetDescriptors("th2:span", attributeNames: Enumerable.Empty()); // Assert var descriptor = Assert.Single(retrievedDescriptorsDiv); @@ -44,16 +156,16 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers } [Fact] - public void GetTagHelpers_ReturnsCatchAllDescriptorsForPrefixedTags() + public void GetDescriptors_ReturnsCatchAllDescriptorsForPrefixedTags() { // Arrange - var catchAllDescriptor = CreatePrefixedDescriptor("th:", "*", "foo1"); + var catchAllDescriptor = CreatePrefixedDescriptor("th:", TagHelperDescriptorProvider.CatchAllDescriptorTarget, "foo1"); var descriptors = new[] { catchAllDescriptor }; var provider = new TagHelperDescriptorProvider(descriptors); // Act - var retrievedDescriptorsDiv = provider.GetTagHelpers("th:div"); - var retrievedDescriptorsSpan = provider.GetTagHelpers("th:span"); + var retrievedDescriptorsDiv = provider.GetDescriptors("th:div", attributeNames: Enumerable.Empty()); + var retrievedDescriptorsSpan = provider.GetDescriptors("th:span", attributeNames: Enumerable.Empty()); // Assert var descriptor = Assert.Single(retrievedDescriptorsDiv); @@ -63,7 +175,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers } [Fact] - public void GetTagHelpers_ReturnsDescriptorsForPrefixedTags() + public void GetDescriptors_ReturnsDescriptorsForPrefixedTags() { // Arrange var divDescriptor = CreatePrefixedDescriptor("th:", "div", "foo1"); @@ -71,7 +183,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers var provider = new TagHelperDescriptorProvider(descriptors); // Act - var retrievedDescriptors = provider.GetTagHelpers("th:div"); + var retrievedDescriptors = provider.GetDescriptors("th:div", attributeNames: Enumerable.Empty()); // Assert var descriptor = Assert.Single(retrievedDescriptors); @@ -81,7 +193,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers [Theory] [InlineData("*")] [InlineData("div")] - public void GetTagHelpers_ReturnsNothingForUnprefixedTags(string tagName) + public void GetDescriptors_ReturnsNothingForUnprefixedTags(string tagName) { // Arrange var divDescriptor = CreatePrefixedDescriptor("th:", tagName, "foo1"); @@ -89,14 +201,14 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers var provider = new TagHelperDescriptorProvider(descriptors); // Act - var retrievedDescriptorsDiv = provider.GetTagHelpers("div"); + var retrievedDescriptorsDiv = provider.GetDescriptors("div", attributeNames: Enumerable.Empty()); // Assert Assert.Empty(retrievedDescriptorsDiv); } [Fact] - public void GetTagHelpers_ReturnsNothingForUnregisteredTags() + public void GetDescriptors_ReturnsNothingForUnregisteredTags() { // Arrange var divDescriptor = new TagHelperDescriptor("div", "foo1", "SomeAssembly"); @@ -105,24 +217,27 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers var provider = new TagHelperDescriptorProvider(descriptors); // Act - var retrievedDescriptors = provider.GetTagHelpers("foo"); + var retrievedDescriptors = provider.GetDescriptors("foo", attributeNames: Enumerable.Empty()); // Assert Assert.Empty(retrievedDescriptors); } [Fact] - public void GetTagHelpers_DoesNotReturnNonCatchAllTagsForCatchAll() + public void GetDescriptors_DoesNotReturnNonCatchAllTagsForCatchAll() { // Arrange var divDescriptor = new TagHelperDescriptor("div", "foo1", "SomeAssembly"); var spanDescriptor = new TagHelperDescriptor("span", "foo2", "SomeAssembly"); - var catchAllDescriptor = new TagHelperDescriptor("*", "foo3", "SomeAssembly"); + var catchAllDescriptor = new TagHelperDescriptor( + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + "foo3", + "SomeAssembly"); var descriptors = new TagHelperDescriptor[] { divDescriptor, spanDescriptor, catchAllDescriptor }; var provider = new TagHelperDescriptorProvider(descriptors); // Act - var retrievedDescriptors = provider.GetTagHelpers("*"); + var retrievedDescriptors = provider.GetDescriptors(TagHelperDescriptorProvider.CatchAllDescriptorTarget, attributeNames: Enumerable.Empty()); // Assert var descriptor = Assert.Single(retrievedDescriptors); @@ -130,18 +245,21 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers } [Fact] - public void GetTagHelpers_ReturnsCatchAllsWithEveryTagName() + public void GetDescriptors_ReturnsCatchAllsWithEveryTagName() { // Arrange var divDescriptor = new TagHelperDescriptor("div", "foo1", "SomeAssembly"); var spanDescriptor = new TagHelperDescriptor("span", "foo2", "SomeAssembly"); - var catchAllDescriptor = new TagHelperDescriptor("*", "foo3", "SomeAssembly"); + var catchAllDescriptor = new TagHelperDescriptor( + TagHelperDescriptorProvider.CatchAllDescriptorTarget, + "foo3", + "SomeAssembly"); var descriptors = new TagHelperDescriptor[] { divDescriptor, spanDescriptor, catchAllDescriptor }; var provider = new TagHelperDescriptorProvider(descriptors); // Act - var divDescriptors = provider.GetTagHelpers("div"); - var spanDescriptors = provider.GetTagHelpers("span"); + var divDescriptors = provider.GetDescriptors("div", attributeNames: Enumerable.Empty()); + var spanDescriptors = provider.GetDescriptors("span", attributeNames: Enumerable.Empty()); // Assert // For divs @@ -156,7 +274,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers } [Fact] - public void GetTagHelpers_DuplicateDescriptorsAreNotPartOfTagHelperDescriptorPool() + public void GetDescriptors_DuplicateDescriptorsAreNotPartOfTagHelperDescriptorPool() { // Arrange var divDescriptor = new TagHelperDescriptor("div", "foo1", "SomeAssembly"); @@ -164,7 +282,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers var provider = new TagHelperDescriptorProvider(descriptors); // Act - var retrievedDescriptors = provider.GetTagHelpers("div"); + var retrievedDescriptors = provider.GetDescriptors("div", attributeNames: Enumerable.Empty()); // Assert var descriptor = Assert.Single(retrievedDescriptors); @@ -174,11 +292,12 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers private static TagHelperDescriptor CreatePrefixedDescriptor(string prefix, string tagName, string typeName) { return new TagHelperDescriptor( - prefix, - tagName, - typeName, - assemblyName: "SomeAssembly", - attributes: Enumerable.Empty()); + prefix, + tagName, + typeName, + assemblyName: "SomeAssembly", + attributes: Enumerable.Empty(), + requiredAttributes: Enumerable.Empty()); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs index 4bd3966446..c2f5760308 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs @@ -19,6 +19,867 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers { public class TagHelperParseTreeRewriterTest : CsHtmlMarkupParserTestBase { + public static TheoryData RequiredAttributeData + { + get + { + var factory = CreateDefaultSpanFactory(); + var blockFactory = new BlockFactory(factory); + var dateTimeNow = new MarkupBlock( + new MarkupBlock( + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)))); + + // documentContent, expectedOutput + return new TheoryData + { + { + "

", + new MarkupBlock(blockFactory.MarkupTagBlock("

")) + }, + { + "

", + new MarkupBlock( + blockFactory.MarkupTagBlock("

"), + blockFactory.MarkupTagBlock("

")) + }, + { + "
", + new MarkupBlock(blockFactory.MarkupTagBlock("
")) + }, + { + "
", + new MarkupBlock( + blockFactory.MarkupTagBlock("
"), + blockFactory.MarkupTagBlock("
")) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: true, + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + })) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: true, + attributes: new Dictionary + { + ["class"] = dateTimeNow + })) + }, + { + "

words and spaces

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + }, + children: factory.Markup("words and spaces"))) + }, + { + "

words and spaces

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = dateTimeNow + }, + children: factory.Markup("words and spaces"))) + }, + { + "

wordsandspaces

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + }, + children: new SyntaxTreeNode[] + { + factory.Markup("words"), + blockFactory.MarkupTagBlock(""), + factory.Markup("and"), + blockFactory.MarkupTagBlock(""), + factory.Markup("spaces") + })) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "strong", + selfClosing: true, + attributes: new Dictionary + { + ["catchAll"] = factory.Markup("hi") + })) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "strong", + selfClosing: true, + attributes: new Dictionary + { + ["catchAll"] = dateTimeNow + })) + }, + { + "words and spaces", + new MarkupBlock( + new MarkupTagHelperBlock( + "strong", + attributes: new Dictionary + { + ["catchAll"] = factory.Markup("hi") + }, + children: factory.Markup("words and spaces"))) + }, + { + "words and spaces", + new MarkupBlock( + new MarkupTagHelperBlock( + "strong", + attributes: new Dictionary + { + ["catchAll"] = dateTimeNow + }, + children: factory.Markup("words and spaces"))) + }, + { + "
", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" class=\"", 4, 0, 4), + suffix: new LocationTagged("\"", 15, 0, 15)), + factory.Markup(" class=\"").With(SpanCodeGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeCodeGenerator( + prefix: new LocationTagged(string.Empty, 12, 0, 12), + value: new LocationTagged("btn", 12, 0, 12))), + factory.Markup("\"").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + "
", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" class=\"", 4, 0, 4), + suffix: new LocationTagged("\"", 15, 0, 15)), + factory.Markup(" class=\"").With(SpanCodeGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeCodeGenerator( + prefix: new LocationTagged(string.Empty, 12, 0, 12), + value: new LocationTagged("btn", 12, 0, 12))), + factory.Markup("\"").With(SpanCodeGenerator.Null)), + factory.Markup(">")), + blockFactory.MarkupTagBlock("
")) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: true, + attributes: new Dictionary + { + ["notRequired"] = factory.Markup("a"), + ["class"] = factory.Markup("btn") + })) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: true, + attributes: new Dictionary + { + ["notRequired"] = dateTimeNow, + ["class"] = factory.Markup("btn") + })) + }, + { + "

words and spaces

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["notRequired"] = factory.Markup("a"), + ["class"] = factory.Markup("btn") + }, + children: factory.Markup("words and spaces"))) + }, + { + "
", + new MarkupBlock( + new MarkupTagHelperBlock( + "div", + selfClosing: true, + attributes: new Dictionary + { + ["style"] = new MarkupBlock(), + ["class"] = factory.Markup("btn") + })) + }, + { + "
", + new MarkupBlock( + new MarkupTagHelperBlock( + "div", + selfClosing: true, + attributes: new Dictionary + { + ["style"] = dateTimeNow, + ["class"] = factory.Markup("btn") + })) + }, + { + "
words and spaces
", + new MarkupBlock( + new MarkupTagHelperBlock( + "div", + attributes: new Dictionary + { + ["style"] = new MarkupBlock(), + ["class"] = factory.Markup("btn") + }, + children: factory.Markup("words and spaces"))) + }, + { + "
words and spaces
", + new MarkupBlock( + new MarkupTagHelperBlock( + "div", + attributes: new Dictionary + { + ["style"] = dateTimeNow, + ["class"] = dateTimeNow + }, + children: factory.Markup("words and spaces"))) + }, + { + "
wordsandspaces
", + new MarkupBlock( + new MarkupTagHelperBlock( + "div", + attributes: new Dictionary + { + ["style"] = new MarkupBlock(), + ["class"] = factory.Markup("btn") + }, + children: new SyntaxTreeNode[] + { + factory.Markup("words"), + blockFactory.MarkupTagBlock(""), + factory.Markup("and"), + blockFactory.MarkupTagBlock(""), + factory.Markup("spaces") + })) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + selfClosing: true, + attributes: new Dictionary + { + ["class"] = factory.Markup("btn"), + ["catchAll"] = factory.Markup("hi") + })) + }, + { + "

words and spaces

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn"), + ["catchAll"] = factory.Markup("hi") + }, + children: factory.Markup("words and spaces"))) + }, + { + "
", + new MarkupBlock( + new MarkupTagHelperBlock( + "div", + selfClosing: true, + attributes: new Dictionary + { + ["style"] = new MarkupBlock(), + ["class"] = factory.Markup("btn"), + ["catchAll"] = factory.Markup("hi") + })) + }, + { + "
words and spaces
", + new MarkupBlock( + new MarkupTagHelperBlock( + "div", + attributes: new Dictionary + { + ["style"] = new MarkupBlock(), + ["class"] = factory.Markup("btn"), + ["catchAll"] = factory.Markup("hi") + }, + children: factory.Markup("words and spaces"))) + }, + { + "
words and " + + "spaces
", + new MarkupBlock( + new MarkupTagHelperBlock( + "div", + attributes: new Dictionary + { + ["style"] = dateTimeNow, + ["class"] = dateTimeNow, + ["catchAll"] = dateTimeNow + }, + children: factory.Markup("words and spaces"))) + }, + { + "
wordsandspaces
", + new MarkupBlock( + new MarkupTagHelperBlock( + "div", + attributes: new Dictionary + { + ["style"] = new MarkupBlock(), + ["class"] = factory.Markup("btn"), + ["catchAll"] = factory.Markup("hi") + }, + children: new SyntaxTreeNode[] + { + factory.Markup("words"), + blockFactory.MarkupTagBlock(""), + factory.Markup("and"), + blockFactory.MarkupTagBlock(""), + factory.Markup("spaces") + })) + }, + }; + } + } + + [Theory] + [MemberData(nameof(RequiredAttributeData))] + public void Rewrite_RequiredAttributeDescriptorsCreateTagHelperBlocksCorrectly( + string documentContent, + MarkupBlock expectedOutput) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor( + tagName: "p", + typeName: "pTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: new[] { "class" }), + new TagHelperDescriptor( + tagName: "div", + typeName: "divTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: new[] { "class", "style" }), + new TagHelperDescriptor( + tagName: "*", + typeName: "catchAllTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: new[] { "catchAll" }) + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]); + } + + public static TheoryData NestedRequiredAttributeData + { + get + { + var factory = CreateDefaultSpanFactory(); + var blockFactory = new BlockFactory(factory); + var dateTimeNow = new MarkupBlock( + new MarkupBlock( + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)))); + + // documentContent, expectedOutput + return new TheoryData + { + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + }, + children: new[] + { + blockFactory.MarkupTagBlock("

"), + blockFactory.MarkupTagBlock("

") + })) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "strong", + attributes: new Dictionary + { + ["catchAll"] = factory.Markup("hi") + }, + children: new SyntaxTreeNode[] + { + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + })) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + }, + children: new[] + { + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("

"), + blockFactory.MarkupTagBlock("

"), + blockFactory.MarkupTagBlock("
"), + })) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "strong", + attributes: new Dictionary + { + ["catchAll"] = factory.Markup("hi") + }, + children: new SyntaxTreeNode[] + { + blockFactory.MarkupTagBlock("

"), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock("

"), + })) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + }, + children: new MarkupTagHelperBlock( + "strong", + attributes: new Dictionary + { + ["catchAll"] = factory.Markup("hi") + }, + children: new[] + { + blockFactory.MarkupTagBlock("

"), + blockFactory.MarkupTagBlock("

") + }))) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "strong", + attributes: new Dictionary + { + ["catchAll"] = factory.Markup("hi") + }, + children: new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + }, + children: new[] + { + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + }))) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + }, + children: new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + }, + children: new[] + { + blockFactory.MarkupTagBlock("

"), + blockFactory.MarkupTagBlock("

") + }))) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "strong", + attributes: new Dictionary + { + ["catchAll"] = factory.Markup("hi") + }, + children: new MarkupTagHelperBlock( + "strong", + attributes: new Dictionary + { + ["catchAll"] = factory.Markup("hi") + }, + children: new[] + { + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + }))) + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + }, + children: new[] + { + blockFactory.MarkupTagBlock("

"), + blockFactory.MarkupTagBlock("

"), + new MarkupTagHelperBlock( + "p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + }, + children: new[] + { + blockFactory.MarkupTagBlock("

"), + blockFactory.MarkupTagBlock("

") + }), + blockFactory.MarkupTagBlock("

"), + blockFactory.MarkupTagBlock("

"), + })) + }, + { + "" + + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "strong", + attributes: new Dictionary + { + ["catchAll"] = factory.Markup("hi") + }, + children: new[] + { + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + new MarkupTagHelperBlock( + "strong", + attributes: new Dictionary + { + ["catchAll"] = factory.Markup("hi") + }, + children: new[] + { + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + }), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + })) + }, + }; + } + } + + [Theory] + [MemberData(nameof(NestedRequiredAttributeData))] + public void Rewrite_NestedRequiredAttributeDescriptorsCreateTagHelperBlocksCorrectly( + string documentContent, + MarkupBlock expectedOutput) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor( + tagName: "p", + typeName: "pTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: new[] { "class" }), + new TagHelperDescriptor( + tagName: "*", + typeName: "catchAllTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: new[] { "catchAll" }) + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]); + } + + public static TheoryData MalformedRequiredAttributeData + { + get + { + var factory = CreateDefaultSpanFactory(); + var blockFactory = new BlockFactory(factory); + var errorFormatUnclosed = "Found a malformed '{0}' tag helper. Tag helpers must have a start and " + + "end tag or be self closing."; + var errorFormatNoCloseAngle = "Missing close angle for tag helper '{0}'."; + + // documentContent, expectedOutput, expectedErrors + return new TheoryData + { + { + " + { + ["class"] = factory.Markup("btn") + })), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + SourceLocation.Zero), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + SourceLocation.Zero) + } + }, + { + "

+ { + ["notRequired"] = factory.Markup("hi"), + ["class"] = factory.Markup("btn") + })), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + SourceLocation.Zero), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + SourceLocation.Zero) + } + }, + { + "

"), + blockFactory.MarkupTagBlock(" + { + ["class"] = factory.Markup("btn") + })), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + absoluteIndex: 15, lineIndex: 0, columnIndex: 15) + } + }, + { + "

+ { + ["notRequired"] = factory.Markup("hi"), + ["class"] = factory.Markup("btn") + })), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + absoluteIndex: 32, lineIndex: 0, columnIndex: 32) + } + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + attributes: new Dictionary + { + ["class"] = factory.Markup("btn") + }, + children: blockFactory.MarkupTagBlock("

"))), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + SourceLocation.Zero), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + SourceLocation.Zero), + } + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + attributes: new Dictionary + { + ["notRequired"] = factory.Markup("hi"), + ["class"] = factory.Markup("btn") + }, + children: blockFactory.MarkupTagBlock("

"))), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + SourceLocation.Zero), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + SourceLocation.Zero), + } + }, + { + "

+ { + ["class"] = factory.Markup("btn") + })), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + SourceLocation.Zero), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + absoluteIndex: 15, lineIndex: 0, columnIndex: 15) + } + }, + { + "

+ { + ["notRequired"] = factory.Markup("hi"), + ["class"] = factory.Markup("btn") + })), + new[] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + SourceLocation.Zero), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + absoluteIndex: 32, lineIndex: 0, columnIndex: 32) + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(MalformedRequiredAttributeData))] + public void Rewrite_RequiredAttributeDescriptorsCreateMalformedTagHelperBlocksCorrectly( + string documentContent, + MarkupBlock expectedOutput, + RazorError[] expectedErrors) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor( + tagName: "p", + typeName: "pTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: new[] { "class" }) + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors); + } + public static TheoryData PrefixedTagHelperBoundData { get @@ -32,7 +893,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers tagName: "myth", typeName: "mythTagHelper", assemblyName: "SomeAssembly", - attributes: Enumerable.Empty()), + attributes: Enumerable.Empty(), + requiredAttributes: Enumerable.Empty()), new TagHelperDescriptor( prefix: "th:", tagName: "myth2", @@ -44,7 +906,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers name: "bound", propertyName: "Bound", typeName: typeof(bool).FullName), - }) + }, + requiredAttributes: Enumerable.Empty()) }; var availableDescriptorsText = new TagHelperDescriptor[] { @@ -53,7 +916,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers tagName: "myth", typeName: "mythTagHelper", assemblyName: "SomeAssembly", - attributes: Enumerable.Empty()), + attributes: Enumerable.Empty(), + requiredAttributes: Enumerable.Empty()), new TagHelperDescriptor( prefix: "PREFIX", tagName: "myth2", @@ -65,7 +929,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers name: "bound", propertyName: "Bound", typeName: typeof(bool).FullName), - }) + }, + requiredAttributes: Enumerable.Empty()) }; var availableDescriptorsCatchAll = new TagHelperDescriptor[] { @@ -74,7 +939,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers tagName: "*", typeName: "mythTagHelper", assemblyName: "SomeAssembly", - attributes: Enumerable.Empty()), + attributes: Enumerable.Empty(), + requiredAttributes: Enumerable.Empty()), }; // documentContent, expectedOutput, availableDescriptors @@ -280,8 +1146,8 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers [Theory] [MemberData(nameof(PrefixedTagHelperBoundData))] public void Rewrite_AllowsPrefixedTagHelpers( - string documentContent, - MarkupBlock expectedOutput, + string documentContent, + MarkupBlock expectedOutput, IEnumerable availableDescriptors) { // Arrange @@ -289,9 +1155,9 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers // Act & Assert EvaluateData( - descriptorProvider, - documentContent, - expectedOutput, + descriptorProvider, + documentContent, + expectedOutput, expectedErrors: Enumerable.Empty()); } diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/AttributeTargetingTagHelpers.DesignTime.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/AttributeTargetingTagHelpers.DesignTime.cs new file mode 100644 index 0000000000..8895517051 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/AttributeTargetingTagHelpers.DesignTime.cs @@ -0,0 +1,60 @@ +namespace TestOutput +{ + using Microsoft.AspNet.Razor.Runtime.TagHelpers; + using System; + using System.Threading.Tasks; + + public class AttributeTargetingTagHelpers + { + private static object @__o; + private void @__RazorDesignTimeHelpers__() + { + #pragma warning disable 219 + string __tagHelperDirectiveSyntaxHelper = null; + __tagHelperDirectiveSyntaxHelper = +#line 1 "AttributeTargetingTagHelpers.cshtml" + "*, something" + +#line default +#line hidden + ; + #pragma warning restore 219 + } + #line hidden + private PTagHelper __PTagHelper = null; + private CatchAllTagHelper __CatchAllTagHelper = null; + private InputTagHelper __InputTagHelper = null; + private InputTagHelper2 __InputTagHelper2 = null; + #line hidden + public AttributeTargetingTagHelpers() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + __CatchAllTagHelper = CreateTagHelper(); + __InputTagHelper = CreateTagHelper(); + __InputTagHelper.Type = "checkbox"; + __InputTagHelper2 = CreateTagHelper(); + __InputTagHelper2.Type = __InputTagHelper.Type; +#line 6 "AttributeTargetingTagHelpers.cshtml" + __InputTagHelper2.Checked = true; + +#line default +#line hidden + __InputTagHelper = CreateTagHelper(); + __InputTagHelper.Type = "checkbox"; + __InputTagHelper2 = CreateTagHelper(); + __InputTagHelper2.Type = __InputTagHelper.Type; +#line 7 "AttributeTargetingTagHelpers.cshtml" + __InputTagHelper2.Checked = true; + +#line default +#line hidden + __CatchAllTagHelper = CreateTagHelper(); + __PTagHelper = CreateTagHelper(); + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/AttributeTargetingTagHelpers.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/AttributeTargetingTagHelpers.cs new file mode 100644 index 0000000000..5cf2e808d4 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/AttributeTargetingTagHelpers.cs @@ -0,0 +1,100 @@ +#pragma checksum "AttributeTargetingTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "e5aa16869aaf5543b30289e98ee5733b08bfe423" +namespace TestOutput +{ + using Microsoft.AspNet.Razor.Runtime.TagHelpers; + using System; + using System.Threading.Tasks; + + public class AttributeTargetingTagHelpers + { + #line hidden + #pragma warning disable 0414 + private TagHelperContent __tagHelperStringValueBuffer = null; + #pragma warning restore 0414 + private TagHelperExecutionContext __tagHelperExecutionContext = null; + private TagHelperRunner __tagHelperRunner = null; + private TagHelperScopeManager __tagHelperScopeManager = new TagHelperScopeManager(); + private PTagHelper __PTagHelper = null; + private CatchAllTagHelper __CatchAllTagHelper = null; + private InputTagHelper __InputTagHelper = null; + private InputTagHelper2 __InputTagHelper2 = null; + #line hidden + public AttributeTargetingTagHelpers() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + __tagHelperRunner = __tagHelperRunner ?? new TagHelperRunner(); + Instrumentation.BeginContext(30, 2, true); + WriteLiteral("\r\n"); + Instrumentation.EndContext(); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("p", false, "test", async() => { + WriteLiteral("\r\n

"); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("strong", false, "test", async() => { + WriteLiteral("Hello"); + } + , StartTagHelperWritingScope, EndTagHelperWritingScope); + __CatchAllTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__CatchAllTagHelper); + __tagHelperExecutionContext.AddHtmlAttribute("catchAll", "hi"); + __tagHelperExecutionContext.Output = __tagHelperRunner.RunAsync(__tagHelperExecutionContext).Result; + WriteTagHelperAsync(__tagHelperExecutionContext).Wait(); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + WriteLiteral("World

\r\n \r\n "); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => { + } + , StartTagHelperWritingScope, EndTagHelperWritingScope); + __InputTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__InputTagHelper); + __InputTagHelper.Type = "checkbox"; + __tagHelperExecutionContext.AddTagHelperAttribute("type", __InputTagHelper.Type); + __InputTagHelper2 = CreateTagHelper(); + __tagHelperExecutionContext.Add(__InputTagHelper2); + __InputTagHelper2.Type = __InputTagHelper.Type; +#line 6 "AttributeTargetingTagHelpers.cshtml" + __InputTagHelper2.Checked = true; + +#line default +#line hidden + __tagHelperExecutionContext.AddTagHelperAttribute("checked", __InputTagHelper2.Checked); + __tagHelperExecutionContext.Output = __tagHelperRunner.RunAsync(__tagHelperExecutionContext).Result; + WriteTagHelperAsync(__tagHelperExecutionContext).Wait(); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + WriteLiteral("\r\n "); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => { + } + , StartTagHelperWritingScope, EndTagHelperWritingScope); + __InputTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__InputTagHelper); + __InputTagHelper.Type = "checkbox"; + __tagHelperExecutionContext.AddTagHelperAttribute("type", __InputTagHelper.Type); + __InputTagHelper2 = CreateTagHelper(); + __tagHelperExecutionContext.Add(__InputTagHelper2); + __InputTagHelper2.Type = __InputTagHelper.Type; +#line 7 "AttributeTargetingTagHelpers.cshtml" + __InputTagHelper2.Checked = true; + +#line default +#line hidden + __tagHelperExecutionContext.AddTagHelperAttribute("checked", __InputTagHelper2.Checked); + __CatchAllTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__CatchAllTagHelper); + __tagHelperExecutionContext.AddHtmlAttribute("catchAll", "hi"); + __tagHelperExecutionContext.Output = __tagHelperRunner.RunAsync(__tagHelperExecutionContext).Result; + WriteTagHelperAsync(__tagHelperExecutionContext).Wait(); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + WriteLiteral("\r\n"); + } + , StartTagHelperWritingScope, EndTagHelperWritingScope); + __PTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__PTagHelper); + __tagHelperExecutionContext.AddHtmlAttribute("class", "btn"); + __tagHelperExecutionContext.Output = __tagHelperRunner.RunAsync(__tagHelperExecutionContext).Result; + WriteTagHelperAsync(__tagHelperExecutionContext).Wait(); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/AttributeTargetingTagHelpers.cshtml b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/AttributeTargetingTagHelpers.cshtml new file mode 100644 index 0000000000..29eb377ba0 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/AttributeTargetingTagHelpers.cshtml @@ -0,0 +1,8 @@ +@addTagHelper "*, something" + +

+

HelloWorld

+ + + +

\ No newline at end of file